Server implementation


Know the PingID SDK using the Moderno sample app

The PingID SDK example customer server (demo server) demonstrates how to integrate PingID SDK capabilities into an existing customer server logic. This example server simulates a fictional company (“Moderno”) which allows users to transfer money within the user’s account. The following flows are demonstrated in the example:

  1. User login:

    The user enters a user name and a password. The example customer server simulates first factor validation (placeholder for 1st factor) and, assuming the user credentials are correct, the example customer server executes MFA with PingID SDK, by authenticating the user with the PingID SDK server.

  2. Step up:

    When the user tries to perform a high value transaction such as transfer funds, the example customer server authenticates users with the PingID SDK server by sending them a push notification to their trusted mobile device, which includes transaction information.

The example customer server can be divided into 2 sections:

  1. The “Moderno” section, which contains the “Moderno” logic.
  2. The PingID SDK section, which contains the PingID SDK integration logic. This can be found in the com.pingidentity.pingidsdk package. In addition, the PingID SDK API model can be found in the com.pingidentity.pingidsdk.api package.

Getting started

Set up an authentication server using the PingID SDK sample authentication server

Software prerequisites

  • Mac OS X or Linux
  • Oracle Java 8: Download and install java 8 (Java SE Development Kit 8u121).
  • JCE for Java 8 (Java Cryptography Extension) to support extended encryption policy:
    • Download JCE.

    • Go to the Java installation on your machine, and in a terminal window enter the following command:

      cd $JAVA_HOME/jre/lib/security

    • Create backup files:

      sudo mkdir backup_security_jars

      sudo mv US_export_policy.jar backup_security_jars

      sudo mv local_policy.jar backup_security_jars

    • Extract the JCE files you downloaded, into the directory you’re in ($JAVA_HOME/jre/lib/security).

  • Apache-tomcat-7.0.7:
    • Download Tomcat 7.

    • Create a folder for Tomcat, and install it there, e.g. ~/dev/apache-tomcat-7.0.77

    • Go to Tomcat’s bin folder and set the execution permissions, e.g.:

      chmod +x *.sh

1. PingID SDK properties file and application configuration

  • Create the folder /env/moderno-props/

  • Request creation of the app in PingOne admin console, as decribed in Initial account configurations.

  • Request the following app resources from the admin:

    • PingID SDK settings file (pingidsdk.properties)
    • Application ID
  • Copy the pingidsdk.properties file to /env/moderno-props/.
  • Add a new line containing the application ID, to the pingidsdk.properties file:

    app_id=<application id from PingOne admin web console>

  • Optional property configurations

    • PingID SDK integration with PingFederate

      Implementations of PingID SDK that are integrated with PingFederate use definitions from the properties file. All of the PingFederate property settings begin with the “pingfed_” prefix, and must be configured manually by editing the properties file.

      • The pingfed_base_endpoint parameter is mandatory for integration with PingFederate.
      • All other pingfed_ parameters are optional.

      See PingID SDK adapter for PingFederate for more details on PingID SDK integration with PingFederate.

  • Properties file example

    The following properties file includes examples of a PingID SDK implementation, including integration with PingFederate. Each parameter is described in a preceding comment.

    # This section is generated by downloading the properties file from the admin web console
    #Auto-Generated from PingOne, downloaded by id=[<user id>] email=[<user email address>]
    #Tue Aug 28 03:51:20 MDT 2018
    api_key=<api key generated in the organization's properties file>
    token=<token key generated in the organization's properties file>
    pingidsdk_url=https://sdk.pingid.com/pingid
    account_id=<account id generated in the organization's properties file>
    
    # The following entry must be updated manually. 
    # Look up the application id in the admin web console.
    app_id=<application id from PingOne admin web console>
    
    # When using the QR code based authentication and
    # the authentication device is configured for multiple users,
    # configure the following option to determine whether to 
    # display the user selection list on the web screen or mobile:
    # true:  user selection list will appear on the web screen
    # false: user selection list will appear on the mobile
    qr_code_web_user_selection=true
    
    # PingFederate Parameters
    
    # The PingFederate authorization base end point parameter is mandatory
    # for PingID SDK integration with PingFederate,
    # for example: pingfed_base_endpoint=https://10.8.2.26:9032
    pingfed_base_endpoint=<PingFederate authorization base end point>
    
    # All other PingFederate parameters are optional
    
    # Application context to support additional logic.
    # The value in this parameter will be passed to the 
    # pingIdSdkAdapterContext dynamic parameter.
    # It can be a single value or formatted as a JSON object
    pingfed_adapter_context={"transaction":"money transfer", "currency":"USD"}
    
    # Set the following "skip screens" parameters to "true" or "false"
    # to override display of the HTML template's configured message screens:
    
    # The true/false value is passed to the pingIdSdkSkipSuccessScreens parameter in the HTML template
    pingfed_skip_success_screens=false
    
    # The true/false value is passed to the pingIdSdkSkipErrorScreens parameter in the HTML template
    pingfed_skip_error_screens=false
    
    # The true/false value is passed to pingIdSdkSkipTimeoutScreens parameter in the HTML template
    pingfed_skip_timeout_screens=false
    
    # The pingfed_base64_private_key is the private key that is used for
    # signing the request query parameter supported by PingFederate.
    # (see: https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.6.1).
    # In the PingFederate admin console (OAUTH Server setting for Moderno client),
    # the "REQUIRE SIGNED REQUEST" checkbox must be checked
    # and the key string entered in the JSON Web Key Set (JWKS) field
    # in the following format:
    #   { "keys": [{"kty":"RSA","n":"<pingfed_base64_private_key string>"} ] }
    # Refer to the entries for Require Signed Request and JSON Web Key Set (JWKS)
    # in the “Configure an OAuth client” topic of the PingFederate documentation:
    # https://documentation.pingidentity.com/pingfederate/pf/index.shtml#adminGuide/configuringClient.html
    #
    # IMPORTANT:
    # If the pingfed_base64_private_key is provided,
    # at least one dynamic parameter must be configured
    # otherwise the redirect will fail.
    #
    # The following example is the key supplied with the Moderno sample app:
    pingfed_base64_private_key=MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdaeZWrs6Mek8fJpNa+k0NVZQAnsZhcAepg/yEN01dHzSmexr73zRnLOEEN3R6PxiGHp9Iqf75Dbc8WchiSF/a6VLcFeVy8XDvyTlhPrscnUrnkmCClG1fNZcEAprz3v7MTEbR8oJ/Ag2pBXV4qVYqfFkfY2dia4QXMQtrR7OfoBnjFDiYIVQx0mQ/Kfbj0GNa9BdFAaakQE80AigdPOZvwj6FotRJDUBRlJxeFs/YxiWA3gExb9tXtiTPhqlYMY0+amY2CgacsD81BYA/ECiGeg04ABayEPRTC/z1i7gxJrLqzgba43PjEPo6KQfb7Pwxnwrx1ipI+/lgWnOC619xAgMBAAECggEAXd0SJ/dNWuJS/oq092GbHqcD6ktlIFOE/AkXGNn08yBSiq0voE1cgszUy5vSxixPcjQzJHPkwqJc5rnNKFU0RbKaTPD/Z34ReaXdrGsSmavY4Fj+jvzrbOxkC/Aqzm3fqK5Ba6maRsuRDPFifneTLsmuYo8aee/IfKhvBdjMvkMwE8dgQOF+rCo+PTM7LZRdpXCprPTfM38wXv4tqgLYMgZuHortJrSUHMwApTC4eVJf77nud7RPySfw0WRV5j+3hLoCeDlIlZpdoHB4LYxRCCpb3xvXMPramE1+4SR8+GcWI/IpXZo7FtGd7YfWNsEGB2JsqBXhYkbwiPWD1SU0AQKBgQD3aP/Sdh7GmuZSoSfQlSkFgUPxYuFuwmg5hX0sl0tqdP5WtZQP1QrKdA/SttFmIJEoO+GxvT6TEtoM7/WHfUwEZCl9YwHusZYuWqkRITpR0d6dR0HN1ailtpyygkJCvGPmpUG35KyAtvJOe2/rDOY8Ndd5BL5mJv/Q1MCmMSDaQQKBgQDlGdd5UuTcD7Z3OAeektfzjBBx+m2Ub6XnrrLdDyE9bNKqWpNKMUOR8mpk21QRFk3yyrtdzcjtG6Dfluk3zE4EbiM/NxUYtjsWcEkYybRZRGkghLCzNFkWyiL8XN2A36L+lypP1IDcQYlWa2aQc5dL0MNgjSh8V8sYGUnCdYFZMQKBgCk6ZQ8EQgKkg2cXldPrV0ekj9bkvF5BpS6YsRBCMf4OeuemIIM25DJyfsSFVIWpPacvu19sAsZtaMqpHggy9/zvV/6eBg9ppnYZA31oRRinWWpqGmwbg5wBv0D79eC7Fga0WSw0vg5A7kXnyfGoV6GG814mTTgmnZCGM4g+PZRBAoGBAIhJCR9JKo+mYTsmf5HbdM+SwK2s9cbZa+4YxJowR+vMcr2YSApYqAOIG7aHqsjHZfNKmo9AvYgfaWHY2j+xL6/+/MS8NbaPoTLF9RXj139K7ObFSIAyYdTmxk0tyS7C2qlsr2Tx2v4rllFsboT73kLhm3gyQKVHbK+XS4N8d0fhAoGBAKPQXYBaO/ZhYE9iYareEBXI8DMYPnPBH27lsCo3a++H0EkyBdL/W4ztIAoSFeAkfnc6AShUMqt/R23l5hWpkZM4V+0i6mTzTC8w0xmOb2A6ZrqM/o0wjrjHXdRDb/vBCHrYxjY//yx7VMs7aHjpSHOgkRnxD/o2IEQ3+Vk/tVIQ
    
    # If you use the private key mentioned above, configure the following JWKS for the Moderno client (OAuth Server):
    # {   "keys": [{"kty":"RSA","n":"vgiCKcwOGiH4wlkkJFGatocBcxnGBoN0pqJAG1A-smdq_qi0Xhpq5JAXpwLlKawt_XtRQ7NL1s_digij0Z3sSy56s7K3GOI9XNAfwByRI2ke2V0O7OXopOFK96-wEF1n0za3_8R9Odaec_bZFfVZWhvrzIehTnkeJu1rVV5iHjOVvKw02nOvvBkqEQ2MC-ZrkDxc6He2XPo4USU2WXICZw-VoHnjXvMlMp2vSWaD8agNhESKQ-pyMWD751t1OdKfxQ7l9Nluqw64IICfgHAw56T8yM3we2Wi1DwQmr0FPnnXn9WckyrEtxscnCc2rXXMohx5jpSwYm2i_08dpt1vyw","e":"AQAB","d":"ezGc4GfOxJY9ZBDR41Gb0LSY9ftBazuRRe1tTcepvZJZxJmtxEuv_FiFXNn9ohRjBC8Lsa3sfmfnIPMU8HFGnD6XgSNcm58fVKn_y1U5qhEd1KAFawJo3WrIZOXaZ-GFy2FvyzmgTPybYGYouZmCwW3UYZWTtwHB0E6eBIvZY-XCUnvGxw2d0e3z1CHGlC76puohDAiS66RR6ixtAEjb3LGQ2tDmLH3douApRvMZyRq6P51IgqxniKVqUfHeYmRts0CO3eLQ__hwOfh6DKxJuVub5CjT_Ctnw-U1__83lqv0pKScqpgr_DmCcFNlsxyS1-bld7AlSaUpl6pyle50cQ","p":"7pkPH3bSeIpnnrzMMGFYU03yaUvUFkFbpa9pI7Md7w8KA_obr0d9XvW09SKhAh0Nfbty6iUxXwOukJDS1fn0HY2sitX_a3bRBJsZDbr6FfVDSLo9oY4N36nia5eDaQAj4l_Til_ytU1U6HdbL5uzO4r-cGFMRO7u-4qv-wNmPF0","q":"y-Suuw_M6ElmNviukn-n84QYRwpIHcAoHSSw7j1Bn-K4cfD6Nctkyy-5-vSHS5mAnqQsq5SvWTZ24aKAketi9gEqQfejtIrRqZ_vy0vHs0xMY7z5UfInpJu1VFXysUkHHLzWTlC9LCDeWZZXv0EVMFqUnywqGwMFEFe8LUDSWkc","dp":"Pt7cCklwGk02ErDjCRiNgp32c2KExzx_nsAC72JCT_zhgbzE0MIAuiX6geGXFYx8QpZDKImWYdgBhpxywel0NtlfDtJm1HVhjFomFuUfAuw2x5IzrT9WTopgXEN3YBKmYf8oKb0woi93YS4aPAhGCTmN5CTvSPbAyE16I6V4qg0","dq":"W75lhRRh_i9wx9F0M8UvHFrW1bhhpMlfA2YMGtcDXV8V4WcuHuCLVQX_qFU5P6IthrSWyQnZMMpaCcMmGQKSf-EcU5yLU-SW3L1zl8VccWgmg0Z35RCSF_ZcTDt0PaN_iMXLpGeeB2ouwp7skslFMIHBmDG2LHtaXvUrXi1puWc","qi":"CV7_xGnNz1XC7AKWV_OOj5YiCCbE5PgOtbBWM7OvXRM_MQrt2ejYmxNYADJ7jjjnYWp_pCL8M5R1DFs8X-A6uYCFIGNpE5_hHAgnBuuQEbhM6DPvCagdMjyjTC6ScAeQ2S9fi52MelCY9XAsQBCwGLDfyo7rBrVd7Tv9iT7l_ig"} ] }
    
    # The private key signing algorithm for PingFederate.
    # Possible values: RS256, RS384 
    # or any other signing algorithm supported in PingFederate
    pingfed_sign_alg=RS256
    
    # Determine whether user approval is required once the user successfully
    # scanned the QR code in PingFederate
    pingfed_qr_code_user_approval_required=true
    
    # The client context sent to the application, once 
    # the user successfully scanned the QR code in PingFederate
    pingfed_qr_code_client_context={"msg":"Approve Sign on to Moderno!","transactionType":"QRCODE_AUTHENTICATION"}
    
    # The push message title (QR code context) for PingFederate
    pingfed_qr_code_push_message_title=Moderno (QR Code)
    
    # The push message body (QR code context) for PingFederate
    pingfed_qr_code_push_message_body=Moderno QR Code Authentication Request
    
    # Dynamic application ID which overrides the application ID in PingFederate
    pingfed_application_id=<application id from PingOne admin web console>
    
    # Client context sent to the application (non-QR code)
    pingfed_client_context={"msg":"Approve Sign on to Moderno","transactionType":"AUTHENTICATION"}
    
    # The client context push message title (non-QR code context)
    pingfed_push_message_title=Moderno
    
    # The client context push message body (non-QR code context)
    pingfed_push_message_body=Moderno Authentication Request
    
    # When using PingFederate and QR code based authentication,
    # and the authentication device is configured for multiple users,
    # configure the following option to determine whether to 
    # display the user selection list on the web screen or mobile
    # true:  user selection list will appear on the web screen
    # false: user selection list will appear on the mobile
    pingfed_qr_code_web_user_selection=true
    
    # Determine whether dynamic parameter data is sent as claims,
    # OR as a single JSON ("pingIdSdkData" claim)
    pingfed_send_dynamic_data_as_request_claims=true
    
    # If dynamic data is in JSON format, determine whether it will be base64 encoded
    pingfed_send_dynamic_data_without_encoding=true

2. Deploy the PingIDSdkDemoServer application:

2a. Deploy the PingIDSdkDemoServer application to Tomcat
  • Extract the customer-server.war from the Server Sample Code folder of the PingID SDK zip package that you downloaded earlier.

  • Copy the customer-server.war to your Tomcat webapps folder, e.g.:

    cp customer-server.war ~/dev/apache-tomcat-7.0.73/webapps/.

2b. Build the PingIDSdkDemoServer application from sources

If you want to build the PingIDSdkDemoServer application from the sources, you should first install Apache Maven (3.2.2), and then download and build the PingIDSdkDemoServer application.

  1. Install Apache Maven:

    • Download Apache Maven (3.2.2).

    • Extract the zipped file into the ~/dev/apache-maven-x.x.x folder

    • Add Maven to the search path: export PATH=~/dev/apache-maven-x.x.x/bin:$PATH

    • Set the M2_HOME environment variable:

      • Create a new /etc/launchd.conf file OR edit the existing file.

      • Add the following line to /etc/launchd.conf:

        setenv M2_HOME /Users/<<your user name>>/dev/apache-maven-x.x.x

        (x.x.x=Maven version you downloaded)

    • Add Maven to the path:

      • Create the file /etc/paths.d/m2_bin

      • Add the line:

        ~/dev/apache-maven-x.x.x/bin

    • To make sure Maven uses the correct java version, create a script that sets JAVA_HOME:

      • In the terminal enter “echo $JAVA_HOME” and copy the returned path.

      • Launch the AppleScript Editor, and enter the following lines:

        do shell script "launchctl setenv JAVA_HOME /Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/"

        do shell script "launchctl setenv M2_HOME ~/dev/apache-maven-x.x.x"

      • Save as file format: Application.

      • Open System Preferences > Users & Groups > Login Items tab > add your new application.

  2. Download and build the PingIDSdkDemoServer application from sources:

    • Unzip the PingIDSdkDemoServer-x.x.zip.

    • Open a terminal and go to the folder of the unzipped file.

      cd CustomerServer  
      mvn clean package  
      cd target
      `
      • Rename the file sample-customer-server-*.war to customer-server.war::

        mv sample-customer-server-*.war customer-server.war

      • Copy customer-server.war to the Tomcat webapps folder, eg.:

        cp customer-server.war ~/dev/apache-tomcat-7.0.73/webapps/

2c. Run Tomcat
  • Open a terminal.

  • Go to the Tomcat bin folder, e.g.:

    cd ~/dev/apache-tomcat-7.0.73/bin

  • Launch Tomcat:

    ./startup.sh

Customer server example structure

The com.pingidentity.pingidsdk package contains the classes which demonstrate the logic which communicates with the PingID SDK server. This package contains 3 classes:

  1. PingIDSdkHelper: Class which contains the logic for authenticating a user and creating a registration token for the user.
  2. PingIDSdkAPI: Class which represents the layer which is responsible for sending requests to the PingID SDK server.
  3. PIngIDSdkExcpetion: Wrapper for exceptions returned from the PingID SDK server.

The com.pingidentity.pingidsdk.api package contains the PingID SDK resource classes.

The rest of the packages contains the “Moderno” example’s specific customer server logic.

Integrate PingID SDK logic into your customer server code using this example:

You may have your own logic for authenticating a user, for example, a user may enter their user name and password in order to authenticate.

PingID SDK authentication can be served as a second factor authentication, or even as a first factor. It is up to each customer server to decide when, and if, to authenticate a user using PingID SDK. In this example, the user first needs to pass the first factor authentication, and then to be authenticated with PingID SDK. It is important to understand that a user must have at least one paired device in order to authenticate with PingID SDK

In general, the customer server logic uses the PingIDSdkHelper for integration with PingID SDK. It initializes this helper with the account id, the account secret key, the account token and the application id.

You cannot send any requests to PingID SDK without the the account id, the account secret key, and the account token. Most of the requests also require the application id.

Let’s observe how this customer server uses PingID SDK:

  1. The customer server validates that the user passes the first factor authentication. So if your customer server validates the user credentials, you can first validate your user credentials and only then authenticate the user with PingID SDK. As already mentioned, each customer server can decide when and if to authenticate the user with PingID SDK server.

  2. The customer server checks if the user exists in PingID SDK server DB, and creates the user if they don’t exist.

    It uses the following method defined in the PingIDSdkHelper class:

    public User getUserOrCreateIfUserNotExist(String username) throws PIngIDSdkException{
    		User user = getUserFromPingIDSdk(username);
     
    		if (user == null) {
    			user = addUserToPingIDSdk(username);
    		}
    		return user;
    

}

A user who does not exist in the PingID SDK server DB cannot pair any device or authenticate. In order to check if the user exists in the PingID SDK DB, the example uses the following method:

<pre><code class="hljs">private <span class="hljs-keyword">User</span> getUserFromPingIDSdk(String username) throws PIngIDSdkException {
    String url = String.format(USERS_GET_URL, accountId, applicationId, username);
    // the apiHelper <span class="hljs-keyword">is</span> an instance <span class="hljs-keyword">of</span> PingIDSdkAPI <span class="hljs-keyword">class</span> which <span class="hljs-keyword">is</span> a layer which sends requests <span class="hljs-keyword">to</span>
    // PingID SDK <span class="hljs-keyword">server</span>
    <span class="hljs-keyword">User</span> <span class="hljs-keyword">user</span> = apiHelper.<span class="hljs-keyword">get</span>(<span class="hljs-keyword">User</span>.<span class="hljs-keyword">class</span>, url);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">user</span>;
}</code></pre>

In order to add a user to the PingID SDK DB, the example uses the following method:

<pre><code class="hljs">private <span class="hljs-keyword">User</span> addUserToPingIDSdk(String username) throws PIngIDSdkException {
		String url = String.format(USERS_POST_URL, accountId);
		<span class="hljs-keyword">User</span> <span class="hljs-keyword">user</span> = <span class="hljs-built_in">new</span> <span class="hljs-keyword">User</span>();
		<span class="hljs-keyword">user</span>.setUsername(username);
		<span class="hljs-keyword">user</span>.setFirstName(""); // optional (<span class="hljs-keyword">in</span> this example, <span class="hljs-keyword">no</span> first <span class="hljs-type">name</span>)
		<span class="hljs-keyword">user</span>.setLastName(""); // optional (<span class="hljs-keyword">in</span> this example, <span class="hljs-keyword">no</span> last <span class="hljs-type">name</span>)
		<span class="hljs-keyword">User</span> createdUser = apiHelper.post(<span class="hljs-keyword">User</span>.<span class="hljs-keyword">class</span>, url, <span class="hljs-keyword">user</span>);		
}</code></pre>
  1. The customer server checks if the user is active; that is, if the user has at least one paired device with this application. It uses the following method, defined in the PingIDSdkHelper class:

    public boolean isUserActive(User user) {
    	return user.getStatus() != null && user.getStatus().equals(UserStatus.ACTIVE);
    }
  2. If the user is not active, the customer server creates a registration token for the user. However, the customer server can create a registration token for the user only if the customer server receives the request from a mobile application which is already integrated with PingID SDK.

    Such a request contains a payload which is generated in the mobile PingID SDK component.

    When integrating your customer server code with PingID SDK, you must distinguish between calls which contain this payload, and those which do not.

    As mentioned, it is impossible to create a registration token for a user without this payload, for example, if the request originates in the web.

    This customer server example returns a dedicated status if there is no payload. It is up to each customer server to decide how to deal with an inactive user if the request does not contain a payload. For example, your customer server logic can show a message encouraging the user to install the upgraded mobile application, which is already integrated with PingID SDK.

    The following example demonstrates a user who is not active, and the request contains a payload. The customer server creates a registration token, using the following method, defined in the PingIDSdkHelper class:

    private RegistrationToken createRegistrationToken(String username, String pingIdPayloadMobile) throws PIngIDSdkException {
    		String url = String.format(REGISTRATIONS_TOKENS_POST_URL, accountId, applicationId,username);
    		RegistrationToken regToken = new RegistrationToken();
    		regToken.setPayload(pingIdPayloadMobile);
    		RegistrationToken createdRegToken = apiHelper.post(RegistrationToken.class, url, regToken);
    }

    The returned RegToken object contains:

    • The registration token.
    • The server payload. The token and the server payload must be returned in the response from the customer server to the mobile application, so that the PingID SDK component which is integrated into your mobile application will be able continue the user device pairing process.

    This approach is referred to as “Automatic Pairing”, which means the device pairing takes place without user awareness.

    Another approach is to pair the user manually. Pairing the user manually is not demonstrated in this customer server example.

  3. In the following example, if the user is already active (the user has at least one paired device), the customer server authenticates the user with PingID SDK. It uses the following method, defined in the PingIDSdk class:

    private Authentication authenticateWithPingIDSdk(String username, String pingIdPayloadMobile, String deviceId, String customizedPushTitle, String customizedPushBody, String clientContext) throws PIngIDSdkException {
    		String url = String.format(AUTHENTICATIONS_POST_URL, accountId, applicationId,
    				username);
    		Authentication authentication = new Authentication();
    		authentication.setDeviceId(deviceId); // may be null
    		authentication.setPayload(pingIdPayloadMobile); // null if the call is not from the mobile application
    		authentication.setPushMessageTitle(customizedPushTitle);
    		authentication.setPushMessageBody(customizedPushBody);
    		authentication.setClientContext(clientContext);
    		authentication.setAuthenticationType(AuthenticationType.AUTHENTICATE);
    		Authentication createdAuthentication = apiHelper.post(Authentication.class, url, authentication);
    		return createdAuthentication;
    	}
    • The username is mandatory.
    • If the request is not from the mobile application, the payload should be null.
    • The device ID is optional. You may authenticate the user with a specific device if you wish.
  1. The following example demonstrates how to get the user devices, using the following method defined in the PingIDSdkHelper class:

    public List<Device> getUserDevices(String username) throws PIngIDSdkException {
    		String url = String.format(USERS_GET_URL, accountId, applicationId, username);
    		UserWithDevices userWithDevices = apiHelper.get(UserWithDevices.class, url,"devices");
    		return userWithDevices.getDevices();
    }

Best practices for security

Ping Identity does not control the channel between the customer server and the customer mobile application, but uses it to transfer data from the PingID SDK component in the customer mobile application to the PingID SDK server. Therefore our clients are advised to use best practices in order to secure the channel between the customer server and customer mobile application, so confidentiality of the PingID SDK payloads won't be compromised.

Ping Identity strongly recommends that implementers use Transport Layer Security (TLS) to protect the network traffic between their mobile applications and their customer servers. Implementers should ideally only allow the use of TLS 1.2 and later, disabling support for older protocols, and should avoid the use of 3DES or RC4-based cipher suites. Implementers are encouraged to support DHE (Diffie Hellman Ephemeral) cipher suites and, if possible, use them exclusively, since these cipher suites provide perfect forward security.

For more information on TLS implementation best practices, see: https://www.owasp.org/index.php/Transport_Layer_Protection_Cheat_Sheet.

Notes

  1. PingID SDK authentication call is asynchronous. In most cases, creating an authentication call returns an authentication object with the “IN PROGRESS” status. In the following example, PingIDSdkHelper polls the PingID SDK server until it receives a final status.

    public Authentication authenticate(String username, String pingIdPayloadMobile, String deviceId, String customizedPushTitle, String customizedPushBody, String clientContext)
    			throws PIngIDSdkException {
    		// Step (1) authenticate the user with PingID SDK
    		Authentication authentication = authenticateWithPingIDSdk(username, pingIdPayloadMobile, deviceId, customizedPushTitle, customizedPushBody, clientContext);
    		// Step (2) handle the returned authentication results.
    		// if the authentication is still in progress , the customer server
    		// should poll the "PingID SDK" server
    		// (sending "get" authentication requests) until final status is
    		// returned.
    		// Each customer server can handle it in a different way.
    		// For example, the customer server can return the call and poll in a
    		// different thread.
    		// In this example, the customer server polls until a final status is
    		// returned
    		if (authentication.getStatus() == AuthenticationStatus.IN_PROGRESS) {
    			authentication = pollPingIDSdkUntilFinalStatus(username, authentication.getId());
    		}
    		return authentication;
    }
    private Authentication pollPingIDSdkUntilFinalStatus(String username, String authenticationId) throws PIngIDSdkException {
    		String url = String.format(AUTHENTICATIONS_GET_URL, accountId, applicationId, username, authenticationId);
    		while (true) {
    			Authentication authentication = apiHelper.get(Authentication.class, url);
    			if (authentication.getStatus() != AuthenticationStatus.IN_PROGRESS) {
    				logger.info(String.format("Authentication Status: %s for username: %s", authentication.getStatus(), username));
    				return authentication;
    			}
    			try {
    				Thread.sleep(1000);
    			} catch (InterruptedException e) {
    				logger.info("unexpected Interrupt...");
    			}
    		}
    	}
  2. The authentication object status indicates the current authentication status. Your customer server should observe the status and act accordingly. For example, AuthenticationStatus.APPROVED means that the user is authenticated successfully.

    Review the documentation for a full understanding of the various possible statuses.

Implement the SDK in your code

This section describes the steps to integrate PingID SDK in a customer application server:

  1. Integrate the server side
  2. Integrate pairing and authentication flows
  3. Signatures in PingID SDK

Integrate the server side

Authorization header in the request

The authorization header contains information about the client and a sent request. The authorization header is in JWT format. A JWT is represented as a sequence of URL-safe parts separated by period (’.’) characters.

A response of each authenticated request will be signed by the PingID SDK service. We recommend verifying the signature of a response, as described in Signatures in PingID SDK.

Integrate pairing and authentication flows

Determine which type of pairing you will allow your users. Pairing types are described in greater detail in Registration process.

This section presents examples of the following flows:

  • Automatic pairing integration
  • Manual pairing integration
  • Authentication

Automatic pairing integration

Choose this kind of pairing if you would like the registration process to be done seamlessly behind the scenes.

We suggest the following set and order of actions for the server side.

  • Automatic pairing pseudocode:

    1.  GET user resource by user name from PingID SDK service
    2.  if user in PingID SDK service doesn't exist
    3.    create user resource in  PingID SDK service 
    4.  fi
    5.  if user status is NOT active
    6.    if accessing device is without PingID SDK mobile payload
    7.       show user the message "Please install the latest version of the app and log in"
    8.       exit
    9.    fi
    6.    create RegistrationToken resource
    7.    add payload from RegistrationToken resource to server response
    8.    exit
    9.  fi
      
  • Automatic pairing: Java sample:

    protected Status authenticate() throws Exception {
        
        // Authenticate with the customer server (1st factor). this is for the demonstration.
        // Usually, authentication with PingID SDK is a second factor authentication  
        // (that is, the user is first authenticated with the user credentials, and then as a second factor, authenticated with PingID SDK).
        Status status = firstFactorAuthentication();
        if (status != Status.OK) {
          return status;
        }
    
        logger.info(String.format("User passed 1st factor authentication. username: %s", username));
    
        /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        // This section demonstrates how this customer server integrates its logic with the PingID SDK solution //
        /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    
        try {
          // At this stage, the user has passed the customer server first factor authentication
    
          // Step (1): Check if the user exists in PingID SDK service.
          // If the user does not exist, create the user in the PingID SDK server
          User user = getUserOrCreateIfUserNotExist(username);
    
          // At this stage, the user exists in PingID SDK Server DB
    
          // Step (2): Check if the user is active. If the user has at least one paired device, the user is active.
          boolean isUserActive = isUserActive(user);
    
          // Automatic Pairing: If the user is not active, create a Registration Token and return it to the caller.
          if (!isUserActive) {
    
            // case 1: call is from the mobile application. (pingIdPayloadMobile is not null or empty)
            // The pingIdPayloadMobile is the payload which is generated by the mobile PingID SDK 
            // component. Each request which is originated from a mobile application (integrated with PingID 
            // SDK) must contain this payload.
            if (pingIdPayloadMobile != null && !pingIdPayloadMobile.trim().isEmpty()) {
              RegistrationToken regToken = createRegistrationToken(user, pingIdPayloadMobile);
    
              // The response must contain the Registration Token ID and the server payload.
              // The mobile application will pair the user device using this data.
              // In this example, the authentication response is defined at class level
              authenticationResponse.setRegistrationToken(regToken.getId());
              authenticationResponse.setPingIdPayload(regToken.getPayload());
              return status;
            }
    
            // case 2: call is not from the mobile application.
            // It is impossible to create a Registration Token if the caller is not the mobile application.
            return Status.USER_NOT_ACTIVE;
          }
    
          // isUserActive == true. If the program reached this line, the user is active.
    
          // Step (3) authenticate the user with PingID SDK
          // Add logic here to continue to the authentication steps with PingID SDK... 
          // (see the authenticate() example in the Authentication section below)
        return status;
    }
      
    private Status firstFactorAuthentication() {
        return Status.OK; // Each customer server may implement a first factor authentication (if needed)
    }
      
    private User getUserOrCreateIfUserNotExist(String username) throws PIngIDSdkException{
        User user = getUserFromPingIDSdk(username);
    
        if (user == null) {
          user = addUserToPingIDSdk(username);
        }
        return user;
    }
      
    private User getUserFromPingIDSdk(String username) throws PIngIDSdkException {
        String url = String.format("/accounts/%s/applications/%s/users/%s", accountId, applicationId, username);
        // apiHelper is a helper class with the purpose of sending requests to PingID SDK service 
        User user = apiHelper.get(User.class, url);
        return user;
    }
    
    private User addUserToPingIDSdk(String username) throws PIngIDSdkException {
        String url = String.format("/accounts/%s/users", accountId);
        User user = new User();
        user.setUsername(username);
        User createdUser = apiHelper.post(User.class, url, user);
        return createdUser;
    }
      
    private boolean isUserActive(User user) {
        return user.getStatus() != null && user.getStatus().equals(UserStatus.ACTIVE);
    }
      
    private RegistrationToken createRegistrationToken(String username, String pingIdPayloadMobile) throws Exception {
        String url = String.format("/accounts/%s/applications/%s/users/%s/registrationtokens", accountId, applicationId,username);
        RegistrationToken regToken = new RegistrationToken();
        regToken.setPayload(pingIdPayloadMobile);
        RegistrationToken createdRegToken = apiHelper.post(RegistrationToken.class, url, regToken);
        return createdRegToken;
    }

Manual pairing integration

Manual pairing integration is separated into 2 parts:

  • Creation of a Pairing Key resource
  • Validation of the pairing key, and subsequent actions of the pairing process
Creation of a Pairing Key resource:
  • Manual pairing: pseudocode for creating a pairing key:

    1. Create a Pairing Key resource
    2. Add the data to the pairing key.
       This data contains information that allows the client to validate a specific user.
    3. POST Pairing Key resource to PingID SDK service
      
  • Creating a pairing key: Java sample:

    protected Status processRequest() {
        try {
           status = createPairingKey();
        } catch (Exception e) {
            status = Status.FAILED_TO_CREATE_PAIRING_KEY;
        } 
        return status;
    }
    
    private Status createPairingKey() throws Exception {
        // A pairing key may have pairing data (this is not mandatory):
        // Pairing data can be anything which the customer server needs in order
        // to validate the pairing key.
        // In this specific example, the customer server creates a list of the users for whom the
        // pairing key is valid 
        // (the getGroupUsers() is not described here since this is just an example.)
        // Each customer server can create the pairing data as it needs
        List<String> users = getGroupUsers();
        Gson gson = new Gson();
        String pairingData = gson.toJson(users.toArray());
        
        PairingKey createdPairingKey = PingidSdkAPIUtils.createPairingKey(accountId, applicationId,
     pairingData);
        return Status.OK;
    }
Validating a Pairing Key resource:
  • Manual pairing: pseudocode for validating a pairing key:

    1.  GET Pairing Key resource by pairing_key
    2.  if resource doesn't exist
    3.    show user an error "Contact your administrator to receive a paring key for 2FA"
    4.    exit
    5.  fi
    6.  get field 'data' from the Pairing Key resource
    7.  run a customer's validation logic
          the customer's server should check that user matches the pairing key
    8.  if the user is NOT validated
    9.    show the user an error "Contact your administrator. Your paring key is invalid"
    10.   exit
    11. fi
    12. Code from automatic pairing flow can be added here.
      
  • Manual pairing: Java sample:

    protected Status pair() {
                // checks if the user successfully passed first factor authentication.
                // In this example, the customer server validates that the user credentials are correct
                // before trying to pair the user
                if (!userFirstFactorAuthenticated()) {
                   return Status.NOT_AUTHENTICATED;
                } 
                
                
                // retrieve the pairing key from the server 
                PairingKey pairingKeyInServer =
             PingidSdkAPIUtils.getPairingKey(accountId, applicationId,
             pairingKey);
             
                // the provided pairing key does not exist
                if (pairingKeyInServer == null) {
                  return Status.PAIRING_KEY_NOT_EXIST;
                }
                
                // The customer server can validate that this pairing key is valid
                // for the user.
                // For example, when creating the pairing key, the customer server can set
                // the pairingKey.pairingData member to be the users list for whom the pairing key is valid.
                // for example: user1,user2,user3.
                // When validating the pairing key, the customer server can check if pairingData contains the user.
                // Note: this is just an example. The pairingData can contain any data and it can be null  
                if (!isPairingKeyValidForUser(pairingKeyInServer)) {
                    return Status.INVALID_PAIRING_KEY;
                } 
                
                // In this stage, the pairing key is considered valid and the user pairing process begins:
                User user = createUserIfNotExist();
                // Check if the user is active. If the user is active, exit this flow with the relevant status.
                boolean isUserActive = isUserActive(user);
                if (isUserActive) {
                  return Status.USER_ALREADY_ACTIVE;
                } 
                
                // user pairing can take place only if the request contains the mobile payload
                // that is, the request originated from the mobile  
                if (pingIdPayloadMobile != null && ! pingIdPayloadMobile.trim().isEmpty()) {
                   String url = String.format("/accounts/%s/applications/%s/users/%s/registrationtokens", accountId, applicationId,username);
                   RegistrationToken regToken = new RegistrationToken();
              regToken.setPayload(pingIdPayloadMobile);
                 // set the pairing key
              regToken.setPairingKey(pairingKey);
                   RegistrationToken createdRegToken = apiHelper.post(RegistrationToken.class, url, regToken);
                
                   // The response must contain the RegToken itself & the server payload.
                   // The customer mobile application will pair the user device using this data.
                   // In this example, the authenticarion response is defined at class level
                   pairResponse.setRegistrationToken(regToken.getId()); 
                   pairResponse.setPingIdPayload(regToken.getPayload());
                   return Status.OK;
                }   
                // call is not from the customer mobile application. pairing cannot take place
                return Status.USER_NOT_ACTIVE;
            }

Authentication

  • Authentication pseudocode:

1.  GET User resource by user name
2.  if user doesn't exist or user status is NOT active
3.     commands here depend on which pairing flow was selected
4.  fi
5. create authentication resource
6. analyse received authentication resource
7. if status of Authentication resource is IN_PROGRESS
8. loop
9.   GET Authentication by authentication id
10.   if (status of Authentication resource is NOT IN_PROGRESS)
11.      exit from the loop
12.   fi
13. loop end
14. add the server payload from Authentication resource to the server response.
15. client should decide what to do next after analyzing Authentication response
  
  • Authentication: Java sample:
protected Status authenticate() {
	   
    // Step (1) 1st factor. The customer server should authenticate the user.
    Status status = firstFactorAuthentication();
    if (status != Status.OK) {
        return status;
    }
   
    // Step (2) Steps here depends on which pairing flow you want to choose.
   
    // Step (3) The user should be active (if we reach this line). It means the customer
    // server may authenticate the user with PingID SDK
   
    Authentication authentication = authenticateWithPingIDSdk();
   
    // Step (4) handle the returned authentication results.
          
    // If the authentication is still in progress, the customer server should poll the PingID SDK service
    // (sending "GET" authentication requests) until final status is returned.
    // Each customer server can handle it in a different way.
    // In this sample, the customer server polls until a final status is returned
    if (authentication.getStatus() == AuthenticationStatus.IN_PROGRESS) {
        authentication = pollPingIdUntilFinalStatus(authentication.getId());
    }
   
    // The response must contains the server payload (if exists) (so that the mobile PingID SDK can handle it)
    // Any customer server implementation must return the server payload (if it exists)in the response 
    authenticationResponse.setPingIdPayload(authentication.getPayload());
   
    // convert the authentication status to the customer server status.
    status = convertPingIdStatus(authentication.getStatus());
   
    return status;
}
   
private Authentication pollPingIdUntilFinalStatus(String authenticationId) {
    String accountId = PingidSdkConfiguration.instance().getAccountId();
    String applicationId = PingidSdkConfiguration.instance().getAppId();
    String url = String.format("/accounts/%s/applications/%s/users/%s/authentications/%s", accountId, applicationId, username, authenticationId);
    while (true) {
        Authentication authentication = PingidSdkAPI.instance().get(Authentication.class, accountId, url);
        if (authentication.getStatus() != AuthenticationStatus.IN_PROGRESS) {
            return authentication;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
    }
}
   
public static Status convertPingIdStatus(AuthenticationStatus authenticationStatus) {
    switch (authenticationStatus) {
        case APPROVED:
            return Status.OK;
        case LOCKED:
        case OTP_IS_BLOCKED:
        case REJECTED:
            return Status.AUTHENTICATION_DENIED;
        case OTP:
            return Status.OTP;
        case IN_PROGRESS:
            return Status.AUTHENTICATION_IN_PROGRESS;
        case SELECT_DEVICE:
            return Status.SELECT_DEVICE;
        case IGNORED_DEVICE:
            return Status.IGNORED_DEVICE;
        default:
            return Status.FAILED;
    }
}

Signatures in PingID SDK

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

A request to PingID SDK server is valid only when it 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.

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:

    Example:

    GET:sdk.pingid.com:/pingid/v1/accounts/130d6e82-df53-43d7-bc0b-0ffe03133f11/applications/c0a658e0-47dc-4cb4-80d7-1a59a6a8a620/users/tom:expand=devices:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855:

    Where:

    HTTPRequestMethod = GET

    Host = sdk.pingid.com or sdk.pingid.com.eu or sdk.pingid.com.au

    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:
    • sdk.pingid.com
    • sdk.pingid.com.eu
    • sdk.pingid.com.au
    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:

    https://sdk.pingid.com/pingid/v1/accounts/B636AA08-5F68-4CE8-9AE5-30C46C9A298D/users/y1?expand=devices

    The canonical URI should be constructed as follows:

    /pingid/v1/accounts/B636AA08-5F68-4CE8-9AE5-30C46C9A298D/users/y1

    CanonicalQueryString

    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: https://sdk.pingid.com/pingid/1/accounts/B636AA08-5F68-4CE8-9AE5-30C46C9A298D/users/y1?expand=devices

    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:Host:CanonicalURI:HashedRequestPayload:

    rather than : HTTPRequestMethod:Host:CanonicalURI:CanonicalQueryString:HashedRequestPayload:

    HashedRequestPayload

    The HashedRequestPayload is calculated as:

    HexEncode(Hash(RequestPayload))

    Where:

    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 mentioned in the previous step, represented in HEX encoding. For example:

      public String calculateSHA256(String canonicalString) throws SignatureException, NoSuchAlgorithmException
      { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(canonicalString.getBytes(StandardCharsets.UTF_8)); return Hex.encodeHexString(hash); }

      The JWT payload should have the following JSON format:

      {
          data: "<digest of the canonical string>"
      }
    • JWT headers:

      The JWT headers section should follow the following JSON format:

      {
          "alg": "HS256" ,
          "typ": "JWT" ,
          "account_id":"<account_id>",
          "token": "<account token>",
          "jwt_version":"v4",
          "expires": <request expiration date>,
          "X-Request-ID": <request unique id>
      }
      Component Description
      alg Specifies HS256 (HMAC SHA256) as the signing algorithm.
      typ This value identifies that the format of the message is JWT.
      account_id The ID of the PingID SDK tenant.
      token The account token which appears in the account settings file, which should be downloaded from the admin console. 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.

      Optional.

      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.

      Optional.

    • 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 console. 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();
      	        jws.setAlgorithmHeaderValue("HS256");
      	        
      	        // 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.setKey(signingKey);
      	     
      	        jws.setHeader("typ","JWT");
      	        jws.setHeader("account_id", accountId);
      	        jws.setHeader("token", token);
      	        jws.setHeader("jwt_version","v4");
      	        LocalDateTime now = LocalDateTime.now(Clock.systemUTC());
      	        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.setPayload(jo.toJSONString());
      	        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 {
        		logger.info("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);
        		logger.info("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 {
        	            jws.setCompactSerialization(strData);
        	            // 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.setKey(signingKey);
        	            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");
        	        }
        }