Signatures in PingID SDK

This chapter describes how to construct a valid “authorization” header and how to verify a PingID SDK server response.

A request to PingID SDK server is considered as a valid request if, and only if:

  • The request contains an authorization header.

Any attempt to send requests without fulfilling this requirement, results in an “unauthorized” (401) response.

In addition, each response from PingID SDK server contains a dedicated X-PINGID-Signature header, which allows the sender to verify the PingID SDK server response.


In order to be able to construct an authorization header, one must know what the account API key and token are.These credentials are available in the account settings file. Refer to Integration overview.

Request Authorization Header

Each request to the PingID SDK server must contain an authorization header. This section describes, step by step, how to construct this request header. In general, the steps are:

  • Construct a canonical string.
  • Construct a signed JWT which contains the canonical string as its payload.
  • Add the signed JWT as the request authorization header with the PINGID-HMAC prefix.
  1. Construct a canonical string:

    The general canonical string format is: HTTPRequestMethod:Host:CanonicalURI:CanonicalQueryString:HashedRequestPayload:



    HTTPRequestMethod = GET

    Host = or or

    CanonicalURI = /pingid/v1/accounts/130d6e82-df53-43d7-bc0b-0ffe03133f11/applications/c0a658e0-47dc-4cb4-80d7-1a59a6a8a620/users/tom

    CanonicalQueryString = expand=devices

    HashedRequestPayload = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

    Canonical string components:

    Component Description
    HTTPRequestMethod The HTTP request method. This may be one of the following values:
    • GET
    • POST
    • DELETE
    • PUT
    • PATCH
    Host The PingID SDK server host, which may be one of the following:
    CanonicalURI The HTTP request URI.

    The canonical URI is the URI-encoded version of the absolute path component of the URI, excluding the host and the query string parameters (if any).

    For example:

    If the full URL is the following:

    The canonical URI should be constructed as follows:



    The request may optionally contain a query string. If the request doesn’t include a query string, the canonical string should not include this component.

    For example, in the following GET URL request:

    In this example, the canonical query string is expand=devices. If the GET request does not contain a query string, the canonical string format should be: HTTPRequestMethod:CanonicalURI:CanonicalHeaders:HashedRequestPayload:

    rather than : HTTPRequestMethod:CanonicalURI:CanonicalQueryString:CanonicalHeaders:HashedRequestPayload:


    The HashedRequestPayload is calculated as:



    Hash represents a function which produces a message digest, typically SHA-256. The PingID SDK server requires the SHA-256 algorithm.

    HexEncode represents a function that returns the base-16 encoding of the digest in lowercase characters.

    The following code sample demonstrates generating the Hashed request payload component:
    public String calculateSHA256(Object requestPayload) throws SignatureException, NoSuchAlgorithmException {
        if (requestPayload == null) {
            requestPayload = "";
        String objStr = getObjStr(requestPayload);
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(objStr.getBytes(StandardCharsets.UTF_8));
        return Hex.encodeHexString(hash);
    private String getObjStr(Object data) throws SignatureException {
        String objStr = "";
        if (data instanceof String) {
            return (String) data;
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            objStr = objectMapper.writeValueAsString(data);
        } catch (JsonProcessingException e) {
            logger.error("Error processing object mapper", e);
            throw new SignatureException();
        return objStr;
  2. Construct a signed JWT which contains the canonical string as its payload:

    The authorization header eventually forms a signed JWT. The JWT itself contains the following payload and headers:

    • JWT Payload:

      The JWT payload is the hashed canonical string represented in HEX encoding: HexEncode(Hash(CanonicalString)) mentioned in the previous step.

      The JWT payload should have the following JSON format:

          data: "<digest of the canonical request>"
    • JWT headers:

      The JWT headers section should follow the following JSON format:

          "alg": "HS256" ,
          "typ": "JWT" ,
          "signedHeaders": "<signed_headers>",
          "token": "<account token>",
          "expires": <request expiration date>,
          "X-Request-ID": <request unique id>
      Component Description
      alg Specifies HS256 as the signing algorithm.
      typ This value identifies that the format of the message is JWT.
      signed_headers JSON representation of the array of signed headers.
      Note: The signed headers must include "Date" header, and it must follow the order of the header values as they appear in the "Canonical headers component". Refer to the previous step to construct a canonical string.
      token The account token which appears in the account settings file, which should be downloaded from the admin web portal. Refer to Integration overview.
      jwt_version The current supported JWT version is v4.
      expires The date and time of the request's expiration in ISO 8601 format in UTC time zone, for example 2017-06-08T05:53:43Z.


      X-Request-ID The ID of the request, which must be unique for each request. If this header exists in the JWT, the expiration header ("expires") is mandatory.


    • JWT Signing:

      The sender must sign the JWT using the API key which appears in the account settings file which should be downloaded from the admin web portal (refer to Integration overview). The following code sample demonstrates how to construct and sign the JWT:

      public String jwtConstructionAndSigning (String hashedCanonicalRequest) throws Exception {
      	        logger.debug("Construct and Sign Jwt");
      	        JsonWebSignature jws = new JsonWebSignature();
      	        // The signing key is constructed as follows:
      	        // SecretKeySpec signingKey = new SecretKeySpec(Base64.getDecoder().decode(apiKey.getBytes()), "HS256");
      	        // the api key is taken from the account settings file
      	        jws.setHeader("account_id", accountId);
      	        jws.setHeader("token", token);
      	        LocalDateTime now =;
      	        LocalDateTime expires = now.plusMinutes(5);
      	        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
      	        String expiresAsISO = formatter.format(expires);
      	        jws.setHeader("expires", expiresAsISO);
      	        UUID requestId = UUID.randomUUID();
      	        jws.setHeader("X-Request-ID", requestId.toString());
      	        JSONObject jo = new JSONObject();
      	        jo.put("data", hashedCanonicalRequest);
      	        jws.setDoKeyValidation(false); // relaxes the key length requirement
      	        try {
      	            return new String(jws.getCompactSerialization().getBytes());
      	        } catch (JoseException e) {
      	            throw new SignatureException("Failed to generate HMAC : " + e.getMessage());
  3. Construct the authorization header:

    The authorization header value is the signed JWT, as constructed in the previous steps.

    Add the signed JWT as the request authorization header, with the PINGID-HMAC prefix.

    Code sample:

    request.addHeader("Authorization", PINGID-HMAC=<signedJWT>);

Response Verification

It is highly recommended that the sender verifies the response returned from the PingID SDK server. The PingID SDK server uses a dedicated response header: “X-PINGID-Signature”. This header value is a JWT, which is signed in a similar manner as explained above.

In order to construct the “X-PINGID-Signature” response header, the PingID SDK server follows these steps:

  1. PingID SDK digests the response body.

    • The PingID SDK uses SHA256 as the digest function.

    • The hashed response body is then encoded to a hexadecimal string.

    • The final hashed response body has the following structure:

      HexEncode (Hash(response body))

  2. The PingID SDK signs the hashed response body.

    PingID SDK creates a JWT with the following structure:

    • Headers:

      "alg": "HS256" ,
      "typ": "JWT"
    • JWT payload:

      data: "<Hashed response body>"
    • The final hashed response body has the following structure:

      HexEncode (Hash(response body))

  3. PingID SDK signs the JWT in the same manner as the sender signs the request authorization JWT.

  4. PingID SDK returns the signed JWT in the “X-PINGID-Signature” response header .

  5. X-PINGID-Signature” response header verification:

    The client should verify the response as follows:

    • Verify the signed JWT which is returned in the “X-PINGID-Signature” header.
    • Verify that the hashed response body is equal to the returned data value in the JWT payload. In order to do this comparison, follow these steps:
      • Retrieve the hashed response body from the JWT which exists in the JWT payload: The value of the “data” JSON member.

      • Hash the body of the response: The hashed response body = HexEncode (Hash(response body))

      • Compare the hashed response body in the JWT with the actual response body.

        The following code sample demonstrates the client response verification:

        void verifyResponse(HttpResponse clientResponse, String responseBody) throws SignatureException {
"start verifying response");
        		if(clientResponse.getFirstHeader("X-PINGID-Singature") == null){
        			logger.equals("invalid server response");
        			throw new SignatureException();
        		String tokenHeader = clientResponse.getFirstHeader("X-PINGID-Singature").getValue();
        		String jwtPayload = verifyJwt(tokenHeader);
        		String responseSignedPayload = "";
        		try {
        		    // see the calculateSHA256 implementation sample in the code sample above which
        		    // demonstrates how to construct a  JWT
        			responseSignedPayload = calculateSHA256(responseBody);
        		} catch (Exception e) {
        			logger.error("Failed create response payload", e);
        			throw new SignatureException();
        		comparePayloads(jwtPayload, responseSignedPayload);
"Response verified");
        String verifyJwt(String JWT) throws SignatureException {
        		logger.debug("Verify Jwt");
        		String jwtStr = "";
        		if (JWT == null || JWT.equalsIgnoreCase("null")) {
        			logger.error("Failed token header cannot be null");
        			throw new SignatureException("Header cannot be null");
        		try {
        			jwtStr = jwtVerify(JWT.getBytes()).getPayload();
        		} catch (Exception e) {
        			logger.error("Failed verify signature", e);
        			throw new SignatureException("Failed verify signature", e);
        		return new JsonParser().parse(jwtStr).getAsJsonObject().get("data").getAsString();
        JsonWebSignature jwtVerify(byte[] data) throws SignatureException {
        	        logger.debug("Jwt verify");
        	        if (data == null) {
        	            throw new SignatureException("payload is null");
        	        String strData = new String(data);
        	        JsonWebSignature jws = new JsonWebSignature();
        	        try {
        	            // The signing key is constructed as follows:
        	            // SecretKeySpec signingKey = new SecretKeySpec(Base64.getDecoder().decode(apiKey.getBytes()), "HS256");
        	            // the api key is taken from the account settings file
        	            jws.setDoKeyValidation(false); // relaxes the key length requirement
        	            return jws;
                             catch (Exception e) {
        	            throw new SignatureException();
        void comparePayloads(String reqHeaderPayload, String reqPayload) throws SignatureException {
        	        logger.debug("Compare payloads reqHeaderPayload=\"%s\" reqPayload=\"%s\"", reqHeaderPayload, reqPayload);
        	        if (reqHeaderPayload == null || reqPayload == null) {
        	            throw new SignatureException("Payload cannot be NULL");
        	        if (!reqHeaderPayload.equals(reqPayload)) {
        	            logger.error(String.format("reqHeaderPayload=\"%s\" reqPayload=\"%s\" are not equal", reqHeaderPayload, reqPayload));
        	            throw new SignatureException("Request and header payload are not equal");