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_portal>

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:

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

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

    private User addUserToPingIDSdk(String username) throws PIngIDSdkException {
    		String url = String.format(USERS_POST_URL, accountId);
    		User user = new User();
    		user.setUsername(username);
    		user.setFirstName(""); // optional (in this example, no first name)
    		user.setLastName(""); // optional (in this example, no last name)
    		User createdUser = apiHelper.post(User.class, url, user);		
    }
  3. 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);
    }
  4. 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.

  5. 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 erver 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. Finally, if possible, the hosted application should implement certificate pinning or public key pinning, to make it more difficult for attackers to perform man-in-the-middle attacks. 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 considered as a valid request 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.

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

    rather than : HTTPRequestMethod:CanonicalURI:CanonicalQueryString:CanonicalHeaders: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 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>",
          "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 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 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");
        	        }
        }

Error handling in PingID SDK

Errors generated by an API should return a response payload formatted as per the schema below. Errors generated by a PingID SDK API should allow the developer to resolve specific errors programmatically.

An error response consists of:

  • A highlevel error code that must be handled by the customer server.
  • (Optional) A details element that contains specific information on how to resolve the fault.

Error representation

Attribute Required Description

id

Yes

A unique identifier that is stored in log files that can be used to track the error received on the customer server with server-side activity for troubleshooting purposes.

code

Yes

General fault code which the customer server must handle to provide all exception handling routines and to localize messages to end users.

Top level error handling must be implemented by the customer server developer. The top level error codes are listed below.

message

Yes

Short description of the error. This message is intended to assist the developer with debugging, and is returned in English only.

target

No

The item that caused the error. For example, a form field id or an attribute inside a JSON object.

details

No

Additional details about the error. This provides further information to the user about the error, and provides the customer server developer more detailed error messages to help resolve the error, and to display to the end users.

Errors described inside the “details” object are optional, and may be implemented by the customer server or API developer.

See the “details” object structure below.

“Details” object representation

The “details” object is array of objects. Each object may contain the following attributes:

Attribute Required Description

code

Yes

Refer to the detailed error codes listed below.

message

No

Short description of the error. This message is intended to assist the developer with debugging, and is returned in English only.

target

No

The item that caused the error. For example, a form field id or an attribute inside a JSON object.

Top level error codes

These codes must be handled by all customer servers. Where not specified, a response code of 400 should be returned.

Code HTTP status code Description

INVALID _DATA

400 (Bad Request)

A valid request was received, however the request could not be completed as there were one or more validation errors with the contents of the request.

INVALID_REQUEST

400 (Bad Request)

The request was invalid and could not be executed. For example a POST request was made without a body.

NOT_FOUND

404 Not Found

The requested resource does not exist.

REQUEST_FAILED

400 (Bad Request)

A valid request was received, however the operation could not be completed due to an issue during the request.

UNAUTHORIZED

401 Unauthorized

The requestor does not have valid authentication credentials to complete the request.

UNEXPECTED_ERROR

500 Internal Server Error

An unexpected server error occurred.

Detailed error codes

These codes are optional for a customer server to handle, and are “child” errors of top level error codes nested in the “details” attribute of the error. These values provide the developer with hints on how to resolve errors.

Code Description

APPLICATION_DISABLED

This application is marked as disabled.

APPLICATION_SHARED_WITH_NON_EXIST_ID

A customer tried to share an application between 2 tenants, but the provided application id doesn’t exist.

DEVICE_BLOCKED

This internal error code appears in 2 cases:

  1. When a customer server is trying to create a registration token for a blocked device.
  2. When a user tries to authenticate from an accessing device, but this device is marked as blocked.

DEVICE_IGNORED

This internal error code appears in 2 cases:

  1. When a customer server is trying to create a registration token for an ignored device.
  2. When a user tries to authenticate from an accessing device, but this device is marked as ignored.

EMAIL_NOT_ENABLED

This error may occur in 2 scenarios:

  1. A user tried to pair an email device when email pairing is disabled for the application.
  2. The admin disabled email as an alternate authentication method. After this change it can take up to 2 minutes until all users are unpaired from the email devices. If during these 2 minutes users attempt to authenticate using their email devices, this error will be triggered.

EMPTY_VALUE

NULL value supplied

FCM_FAIL_TO_CONNECT

Credentials for the FCM service are not valid.

INVALID_ACCOUNT

Request is made on / by an account that is not verified or has been suspended.

INVALID_PATTERN

Received a value, but this value does not match the pattern.

INVALID_PRODUCTION_CERTIFICATE

A customer tried to upload an invalid Production iOS Certificate for Apple Push Notification Service to the PingID SDK server.

INVALID_SANDBOX_CERTIFICATE

A customer tried to upload an invalid Sandbox iOS Certificate for Apple Push Notification Service to the PingID SDK server.

INVALID_USER_STATUS

A customer tried to create a registration token via the REST API for an active user.

INVALID_VALUE

Value is invalid due to validation rules.

NOT_FOUND

Request references a resource that does not exist.

OUT_OF_RANGE

Value is out of range.

PRODUCTION_CERTIFICATE_EXPIRED

A customer tried to upload an expired Production iOS Certificate for Apple Push Notification Service to the PingID SDK server.

PRODUCTION_CERTIFICATE_FAIL_TO_CONNECT

There is a problem with the connection to Apple Push Notification Server (Production). There is the possibility that the certificate was revoked.

REQUIRED_VALUE

Required attribute was omitted in request.

RESOURCE_ALREADY_EXISTS

A customer server tried to create a resource that already exists.

RESOURCE_IN_USE

User tried to use a resource, but this resource is already in use.

RETRY_LIMIT_EXCEEDED

User reached the maximum permitted number of incorrect retry attempts, for example, by entering 3 incorrect OTPs in succession.

SANDBOX_CERTIFICATE_EXPIRED

A customer tried to upload an expired Sandbox iOS Certificate for Apple Push Notification Service to the PingID SDK server.

SANDBOX_CERTIFICATE_FAIL_TO_CONNECT

There is a problem with the connection to Apple Push Notification Server (Sandbox). There is the possibility that the certificate was revoked.

SERVICE_ERROR

An error with the service occurred.

SIZE_LIMIT_EXCEEDED

Value is too large for the attribute.

SMS_NOT_ENABLED

A user tried to pair an SMS device, or authenticate with an SMS device, when SMS pairing and authentication is disabled for the application. Refer to Update a PingID SDK app’s configuration for details.

SMS_QUOTA_EXCEEDED

This internal error indicates that the SMS daily quota was exceeded for the account or for the user.

UNINSTALLED_APPLICATION

A user reinstalled an app on a device, and then using that device, attempted to authenticate another device.

UNIQUE_VALUE_REQUIRED

Value should be unique.

USER_DISABLED

User marked as suspended.

Error handling examples

General example:

{
"id" : "abcd123qwe",
"code" : "INVALID_DATA",
"message" : "The data provided was invalid",
"details" : [
 {
  "code" : "EMPTY_VALUE",
  "target" : "givenName",
  "message" : "Given name can not be empty."
 },
 {
  "code" : "OUT_OF_RANGE",
  "target" : "age",
  "message" : "Age must be between 1 and 150.",
  "innerError" : {
   "rangeMinimumValue" : 1,
   "rangeMaximumValue" : 150
  }
 }]
}

Example of a returned error, in a case where an application was disabled:

The following authentication request:

curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ \ 
	"authenticationType": "AUTHENTICATE" }' \
	 'https://sdk.pingid.com/pingid/v1/accounts/bb09a7a1-b359-418c-9c66-d8b91d83fda4/applications/48cf7277-a5ea-48ea-b526-c4784230c396/users/john.galt/authentications'

Returns the following “REQUEST_FAILED” status:

{
  "message": "Application disabled",
  "target": "application",
  "id": "webs_a14fae49-f82d-4e72-8e00-8d2ae11610af",
  "details": [
    {
      "message": "Application disabled",
      "code": "APPLICATION_DISABLED"
    }
  ],
  "code": "REQUEST_FAILED"
}