Transaction Signing

View this page for | |

Once the keys are provisioned, the device is ready to perform a Transaction Signing operation (that is, to approve or decline an authentication request operation based on details sent by the HID authentication platform). How the application is notified that a transaction is to be signed depends on its deployment. One option is to get a push notification from the server.

Pending Transactions

For Client Initiated Backchannel Authentication (CIBA) integrations and more information about creating a transaction using a Web App/backend integration, refer to the bcauthorize endpoint in the HID authentication platform documentation.

The integrating application signs a transaction as follows:

  1. Create an instance of the Device (DeviceFactory.getDevice).
  2. Retrieve the transaction identifier (transactionId) for the transaction that will be processed. This identifier can be retrieved from the:

    • Push notification payload received by the application. This is the tds member of the payload

    • List of pending transactions for a specific container retrieved from the server (Container.retrieveTransactionsIds)

    • Scanning a QR code encoded with a userID-less transaction

  3. Get public information (ServerActionInfo) from the transaction identifier (transactionId) (Device.retrieveTransactionInfo).

    For a userID-less transaction, an InexplicitContainerException containing the list of possible userIDs is raised if several containers exist for the targeted server. The integrating application will need to select a userID from the getParameters list and call Device.retrieveTransactionInfo with the userID parameter.

    There is no communication with the server at this point.

    The returned ServerActionInfo instance provides the:

  4. Check if the Session Transport Key is protected by a password and prompt the user as required.
  5. Get transaction details from the server (ServerActionInfo.GetAction).
  6. Get the transaction details (Transaction.toString) and the list of allowed statuses (Transaction.getAllowedStatuses) that will be displayed to the end user so that they can decide which action to take (“approve” or “decline” the transaction).
  7. Display the transaction to the end user and retrieve the end user’s selection among the available statuses.
  8. Then request the end user to provide their Transaction Signing password and send the final status with optional mobile context data to the HID authentication platform (Transaction.setStatus).

    For integrations with the HID Authentication Service, see User Authentication with HID Approve and the bcauthorize endpoint.

Sample Transaction Signing on Android

Copy
var device : Device? = null
    // Get Device instance
    try {
        device = DeviceFactory.getDevice(ctx, connectionConfiguration)
    } catch (ex: Exception) {
        when(ex) {
            is UnsupportedDeviceException, is LostCredentialsException, is InternalException, is InvalidParameterException -> {
                ex.printStackTrace()
            }
        }
    }
    var txInfo : ServerActionInfo? = null
    // Get the public information of the transaction
    // the ID is obtainable  : either through the push message, or through the retrieveTransactionIds API of the container.
    try {
        txInfo = device?.retrieveActionInfo(txId!!.toCharArray())
    } catch (ex: Exception) {
        when(ex) {
            is InternalException, is InvalidContainerException, is InvalidParameterException -> {
                ex.printStackTrace()
            }
            else -> throw ex
        }
    }
    // The public information includes the container and the transaction protection key.
    // We can check whether password is needed by getting the key and policy
    // we assume it is not
    val container = txInfo?.container
    val txProtectKey = txInfo?.protectionKey

    // Retrieve the transaction details
    // we assume session key is not password protected (password null)
    var tx: Transaction? = null
    try {
        tx = txInfo?.getAction(null, null) as Transaction
    } catch (ex: Exception) {
        when(ex) {
            is PasswordExpiredException -> {
                ex.printStackTrace()
            }
            is AuthenticationException, is InvalidParameterException, is UnsupportedDeviceException, is  ServerOperationFailedException, is
            TransactionExpiredException, is RemoteException, is  LostCredentialsException, is  InternalException -> {
                ex.printStackTrace()
            }
            else -> throw ex
        }
    }
    // The transaction contains the list of allowed response status
    val allowedStatus = tx?.allowedStatuses

    // Display transaction details to end user and request status

    // Here we can check whether the signing key is protected by a password
    var signingKeyPolicy: ProtectionPolicy? = null
    var containerPassword: String? = null
    try {
        signingKeyPolicy = tx!!.signingKey.protectionPolicy
    } catch (ex: Exception) {
        when(ex) {
            is InternalException, is UnsupportedDeviceException, is UnsafeDeviceException -> {
                ex.printStackTrace()
            }
            else -> throw ex
        }
    }
    if (ProtectionPolicy.PolicyType.PASSWORD.toString() == signingKeyPolicy!!.type) {
        // Prompt the end-user for the signing key password
        containerPassword = userPassword
    }

    // We can now sign the transaction with a selected status and context data
    val status = allowedStatus?.get(0
    // optional parameter can be used to pass mobile context base64 data (otherwise leave empty or null)
    var params = arrayOfNulls<Parameter>(0)
    params = arrayOf(
        Parameter(SDKConstants.PARAM_TX_MOBILE_CONTEXT, sContextB64.toCharArray()))

    // we assume session key is not password protected (password null)
    try {
        result = tx?.setStatus(status, containerPassword?.toCharArray() ?: null, null, params)!!
    }
    catch (ex: Exception) {
        when(ex) {
            is AuthenticationException -> { // Password is incorrect
                ex.printStackTrace()
            }
            is PasswordExpiredException -> { // !!!  PasswordExpiredException if expired password is given (changePassword required). !!!
                ex.printStackTrace()
            }
            is TransactionExpiredException, is RemoteException, is LostCredentialsException,
            is InternalException ,is PasswordRequiredException, is FingerprintAuthenticationRequiredException,
            is ServerOperationFailedException, is InvalidParameterException -> {
                ex.printStackTrace()
            }
            else -> throw ex
        }
    }
Copy
// Get Device instance
    Device device = null;
    try {
        device = DeviceFactory.getDevice(this.ctx, connectionConfiguration);
    } catch (UnsupportedDeviceException | LostCredentialsException | InternalException | InvalidParameterException e) {
        e.printStackTrace();
    }

    // Get the public information of the transaction
    // the ID is obtainable  : either through the push message, or through the retrieveTransactionIds API of the container.
    ServerActionInfo txInfo = null;
    try {
        txInfo = device.retrieveActionInfo(txId.toCharArray());
    } catch (InternalException | InvalidContainerException | InvalidParameterException e) {
        e.printStackTrace();
    }

    // The public information includes the container and the transaction protection key.
    // We can check whether password is needed by getting the key and policy
    // we assume it is not
    Container container = txInfo.getContainer();
    Key txProtectKey = txInfo.getProtectionKey();

    // Retrieve the transaction details
    Transaction tx = null;
    try {
        tx = (Transaction) txInfo.getAction(null, null);
        Log.i(this.LOG_TAG, "Tx=" + tx.toString());
    } catch (PasswordExpiredException e) {
        e.printStackTrace();
    } catch (AuthenticationException | InvalidParameterException | UnsupportedDeviceException | ServerOperationFailedException |
        TransactionExpiredException | RemoteException | LostCredentialsException | InternalException e) {
        e.printStackTrace();
    }
    // The transaction contains the list of allowed response status
    String[] allowedStatus = tx.getAllowedStatuses();

    // Display transaction details to end user and request status

    // Here we can check whether the signing key is protected by a password
    ProtectionPolicy signingKeyPolicy = null;
    String containerPassword = null;
    try {
        signingKeyPolicy = tx.getSigningKey().getProtectionPolicy();
    } catch (InternalException | UnsupportedDeviceException | UnsafeDeviceException e) {
        e.printStackTrace();
    }
    if (ProtectionPolicy.PolicyType.PASSWORD.toString().equals(signingKeyPolicy.getType())) {
        // Prompt the end-user for the signing key password
        containerPassword = userPassword;
    }
    
    // optional parameter can be used to pass mobile context base64 data (otherwise leave empty or null)
    Parameter[] params = new Parameter[0];
    params = new Parameter[]{new Parameter(SDKConstants.PARAM_TX_MOBILE_CONTEXT, sContextB64.toCharArray())};
    
    // We can now sign the transaction with a selected status and context data
    String status = allowedStatus[0];

    // we assume session key is not password protected (password null)
    try {
        result = tx.setStatus(status, containerPassword.toCharArray(), null, params);
    } catch (AuthenticationException e) { // Password is incorrect
        e.printStackTrace();
    } catch (PasswordExpiredException e) {   // !!!  PasswordExpiredException if expired password is given (changePassword required). !!!
        e.printStackTrace();
    } catch (TransactionExpiredException | RemoteException | LostCredentialsException | InternalException | PasswordRequiredException | FingerprintAuthenticationRequiredException | ServerOperationFailedException | InvalidParameterException e) {
        e.printStackTrace();
    }

Direct Client Signature (DCS)

In combination with the HID Authentication Service, HID Approve SDK 5.14 introduces the Direct Client Signing (DCS) feature allowing to generate authentication requests for signature directly within the HID Approve SDK.

The symmetric workflow provides seamless integration for authentication for business applications and is a suitable and a more secure alternative for symmetric key-based OTPs. Depending on the use case, it could also eliminate the need for complex asynchronous integrations that use traditional pending transactions for authentication.

After the generation of a direct transaction, it can be signed the same way as standard pending transactions.

Optionally, the HID Approve SDK provides two verification methods of the authentication depending on the integration:

  • For immediate client-side verification in the integrating application, the OIDC ID Token can be used to perform additional verification. The ID token provides identity and authentication details with a digital signature to ensure authenticity and protect the integrity of the provided data.

    For further information, see HID Authentication Service documentation on OpenID Tokens.

  • For Client Initiated Backchannel Authentication (CIBA) integrations that might require additional verification of the authentication request signature, the newly created authentication request identifier will be returned after performing the cryptographic signature. For further information about CIBA feedback event polling, see Configuring the CIBA Feedback Mode.

For example:

  1. Create an instance of the Device (DeviceFactory.getDevice).
  2. Locate a specific container instance (Device.findContainers).

  3. Locate the asymmetric signing key to be used with the new transaction. This can be done by using the filter KEY_PROPERTY_USAGE with either KEY_PROPERTY_USAGE_AUTH or KEY_PROPERTY_USAGE_SIGN (Container.findKeys).

  4. Generate a new transaction for a specific container (Container.generateAuthenticationRequest).

  5. Check if the Session Transport Key is protected by a password and prompt the user as required.

  6. Prompt the end user to provide their Transaction Signing password if protected by a password, and send the final status to the HID Authentication Service with the selected status (Transaction.setStatus).

  7. The ID Token can be used for client-side verification as required (Transaction.getIdToken).

  8. For CIBA integrations, forward the created request ID to the back end for verification as required (Transaction.getRequestId).

Copy
var userPassword: String? = null
try {
    // Locate container's signing key for transaction
    val keyFilterAuth = arrayOf(
        Parameter(SDKConstants.KEY_PROPERTY_USAGE, SDKConstants.KEY_PROPERTY_USAGE_AUTH)
    )
    val authKeys = myContainer.findKeys(keyFilterAuth)

    // Arbitrarily using first key as example. With multiple keys, check label or id
    val key = authKeys[0]

    // Generate a new direct transaction.
    val txMessage = "User Login"
    val transaction = myContainer.generateAuthenticationRequest(txMessage, key.id)

    // Here we can check whether the transaction protection key is protected by a password
    val policy = myContainer.protectionPolicy
    if (policy is PasswordPolicy) {
        // Prompt the end-user for the transaction protection key password
        Log.d(LOG_TAG, "This is a password policy")
    }

    var containerPassword: String? = null

    if (ProtectionPolicy.PolicyType.PASSWORD.toString() == policy.type) {
        // Prompt the end-user for the signing key password
        containerPassword = userPassword
    }
    // We can now sign the transaction with any status
    val status = "approve"
    val succeeded = transaction.setStatus(status, containerPassword?.toCharArray(), null, arrayOf())

    // If farther verification needed by backend, forward the resulting CIBA authentication request id
    val authRequestId = transaction.requestId

    // the ID Token value associated with the signed transaction.
    val tokenId = transaction.idToken

    assertTrue(true)
} catch (t: Throwable) {
    fail()
    Log.e(LOG_TAG, "Operation failed")
}
    }
Copy
String userPassword = null;
try {
    // Locate container's signing key for transaction
    Parameter[] keyFilterAuth = new Parameter[]{
        new Parameter(SDKConstants.KEY_PROPERTY_USAGE, SDKConstants.KEY_PROPERTY_USAGE_AUTH)
    };
    Key[] authKeys = myContainer.findKeys(keyFilterAuth);

    // Arbitrarily using first key as example. With multiple keys, check label or id
    Key key = authKeys[0];

    // Generate a new direct transaction.
    String txMessage = "User Login";
    Transaction transaction = myContainer.generateAuthenticationRequest(txMessage,key.getId());

    // Here we can check whether the transaction protection key is protected by a password
    ProtectionPolicy policy = myContainer.getProtectionPolicy();
    if(policy instanceof PasswordPolicy){
        // Prompt the end-user for the transaction protection key password
        Log.d(LOG_TAG,"This is a password policy");
    }

    String containerPassword = null;

    if (ProtectionPolicy.PolicyType.PASSWORD.toString().equals(policy.getType())) {
        // Prompt the end-user for the signing key password
        containerPassword = userPassword;
    }
    // We can now sign the transaction with any status
    String status = "approve";
    boolean succeeded = transaction.setStatus(status, containerPassword.toCharArray(), null, new Parameter[0]);

    // If farther verification needed by backend, forward the resulting CIBA authentication request id
    String authRequestId  = transaction.getRequestId();
    // the ID Token value associated with the signed transaction.
    String tokenId  = transaction.getIdToken();
    assertTrue( true );
}catch (Throwable t) {
    fail();
    Log.e(LOG_TAG, "Operation failed");
}
Note: The DCS feature is only available with the HID Authentication Service. It is not supported by the ActivID Authentication Server or ActivID Appliance.

Canceling Pending Transactions

The HID Authentication Service now supports discarding or canceling pending transactions from the server without needing to sign the rejected transactions. For integrating applications, this can provide an improved user experience as the end-user’s signing key is not required to perform the cancel operation.

In addition, users can indicate an unwanted fraudulent or suspicious transaction via the application to the back end. Depending on the server authentication policy configuration, appropriate administrator-controlled counter measures can be invoked to reduce the risk of unauthorized access or potential push notification fatigue attacks on the end user.

For example, the authentication policy's disabledTimeReset parameter could be configured to prevent the creation of new transactions indefinitely or to block for a fixed 'cool-down' period. Any remaining pending transactions for the user would also be automatically revoked by the server.

Copy
// Retrieve pending transaction for this container. The ID can also be received through a push message.
val transactionIds = myContainer.retrieveTransactionsIds(null, null)

// Get the public information of the transaction
txInfo = myDevice!!.retrieveActionInfo((transactionIds[0]));

// Retrieve the transaction details
val tx = txInfo.getAction(null, null) as Transaction

// We can cancel the transaction or cancel with flag suspicious
// Optional message describing cancelation reason to be audited
val message = "Duplicate"
// Reason can be either USER_CANCEL or NOTIFY_SUSPICIOUS
val reason = CancelationReason.USER_CANCEL
tx.cancel(message, reason, null);
Copy
ServerActionInfo txInfo = null;

// Retrieve pending transaction for this container. The ID can also be received through a push message.
char[][] transactionIds = myContainer.retrieveTransactionsIds(null, null);

// Get the public information of the transaction
txInfo = device.retrieveActionInfo(transactionIds[0]);

// Retrieve the transaction details
Transaction tx = (Transaction) txInfo.getAction(null, null);

// We can cancel the transaction or cancel with flag suspicious
// Optional message describing cancelation reason to be audited
String message = "Duplicate";
// Reason can be either USER_CANCEL or NOTIFY_SUSPICIOUS
CancelationReason reason = CancelationReason.USER_CANCEL;
tx.cancel(message, reason, null);