Transaction Signing with TOTP

Prompts the user to approve a financial transaction using a challenge-based TOTP.

About client-facing steps

A journey is a sequence of steps that are executed when it's invoked by a client application (known as the "client"). Some steps require involvement from the client, such as to collect user input. Other steps, like validating a token, are executed in the backend by the Mosaic journey engine alone.

When invoked, the journey begins executing the steps while the client waits for further instructions. When a journey reaches a client-facing step, the journey asks the client for the required input and then waits for the client to respond. The client presents their own UI if user interaction is required, and returns input to the journey. The journey then proceeds in a similar manner until it's completed.

Description

This step is used to authenticate a financial transaction by prompting the user to approve it through a challenge-based TOTP.

Important

To use Transaction Signing with TOTP, it is mandatory to have an authenticator registered. You can integrate an authenticator into your app by embedding an authenticator directly into your iOS and Android applications, to allow users to generate and approve TOTP codes seamlessly within your app environment.

The challenge is generated using the details from the approval_data JSON object and is sent to the client. The client side displays this challenge to the user and requests a TOTP code as input, which can occur in two flow variations:

Web to offline authenticator:

  1. Your client presents the challenge and approval data on the web interface.
  2. The user takes the challenge (e.g., copies a code displayed on the web interface), provides it as input in the offline authenticator mobile app where they registered their TOTP. The app generates a TOTP code based on this challenge, which the user then manually inputs into the web client interface.

Online authenticator:

  1. Your client presents the approval data within the same application where the TOTP is generated.
  2. The user approves the transaction within the app, and the app automatically generates the code based on the received challenge, and sends it to the server.

After receiving the TOTP code, the server validates it using the end-transaction API. If the code is incorrect, an assertion is sent back to the client, informing the user of the error and allowing them to retry.

Configuration

Field Description
External User ID Mandatory expression that yields the main user identifier used in orchestration. It is unique at the tenant level.
Approval Data A JSON object containing a flat list of claims used to derive the TOTP challenge. This object is dynamically generated based on transaction details, such as amount, currency, and transaction ID.

Examples

The following examples demonstrate various authentication scenarios, highlighting how to integrate and utilize TOTP-based transaction signing in different application contexts:

  • Web to mobile authenticator scenario : an example showing how to use a separate, offline mobile code generator to verify high-value transactions in a banking web application.
  • In-app mobile authentication scenario : an example showing how to silently use TOTP-based transaction signing in a banking mobile application, where the TOTP code is generated and submitted automatically in the background.

Web to mobile authenticator scenario

In a banking application, the Transaction Signing with TOTP step can be used to verify high-value transfers. When a user initiates a transfer exceeding a set threshold on the web interface, the application triggers this step to validate the transaction using a mobile app integrated with the same service.

The backend sends a JSON object containing the transaction details (approval_data) and a 6-digit challenge code (transaction_challenge) to the client-side application. This information should be displayed on the web interface for the user. Below is an example of the JSON data received:

Copy
Copied
{
  "data": {
    "transaction_challenge": "123456",
    "approval_data": {
      "transactionId": "TX12345",
      "amount": "1500.00",
      "currency": "USD"
    }
  }
}

Upon receiving the JSON data, the client displays the transaction details (approval_data) and the challenge code (transaction_challenge) to the user. The user opens the mobile banking app and inputs the challenge code from the web interface into the app.

The mobile app generates a TOTP code based on this challenge and automatically sends it to the server, or the user manually enters it into the web client interface.

The following is an example of the server request for validation:

Copy
Copied
tsPlatform.ido.submitClientResponse(
   ClientResponseOptionType.ClientInput,
   {
     "totp_code": "654321"
   }
)

If the submitted TOTP code is invalid, the IdoServiceResponse.errorData field will return the error code InvalidCredentials. The client should then prompt the user to retry. Note that the user has a limited number of attempts before the transaction journey is rejected.

In-app mobile authentication scenario

In this scenario the flow occurs in a mobile app, where the TOTP code is generated and submitted in the background without requiring manual input. This provides a more seamless experience, while still maintaining security.

When the user initiates the transfer, the backend sends the transaction details (approval_data) and a challenge code (transaction_challenge) to the app. The user reviews the transaction details, and upon approval, the app automatically generates the TOTP code based on the challenge and sends it to the server. In this case, the user doesn’t need to manually input the TOTP code, as the app automatically sends it to the server after approval.

Here’s an example for generating and submitting the TOTP code:

KotlinSwift
Copy
Copied
//data class representing the totp code
data class TOTPInput(val totp_code: String)

...

private un updateUI() {
        val approvalData = idoResponse.responseData?.optJSONObject("approval_data")
        binding.tvTransactionId = approvalData.optString("transaction_id")
        binding.tvTransactionAmount = approvalData.optString("transaction_amount")
        binding.btnApprove.setOnClickListener{
            generateTransactionSigningTOTP()
        }
    }

private fun generateTransactionSigningTOTP() {
        val challenge = idoResponse.responseData?.optString("transaction_challenge")
        val uuid = SharedPrefs.readSharedPrefs(requireContext(), KEY_UUID, null)
        challenge?.let {
            uuid?.let { uuid
                TSAuthentication.generateTOTPWithChallenge(requireContext(), uuid, challenge, object :
                    ITSTOTPGenerateTOTPCallback {
                    override fun onFailed(error: TSTOTPError) {
                        showError("TOTP transaction signing error: " + error.errorMsg)
                    }

                    override fun onSuccess(totpCode: String) {
                        TSIdo.submitClientResponse(TSIdoClientResponseOptionType.ClientInput.type, TOTPInput(totpCode), callback)
                    }
                })
            }

        }
    }
Copy
Copied
// Get UUID stored on the device on TOTP registration
guard let uuid = UserDefaults.standard.string(forKey: "totp_uuid") else {
    debugPrint("[DEBUG]: missing TOTP registration UUID")
    return
}

// Get challenge from the server response data object
let challenge = response.data["transaction_challenge"] as? String

// Call Authentication SDK to generate a TOTP code
TSAuthentication.shared.generateTOTPCodeWithChallenge(UUID: uuid, challenge: challenge) { [weak self] response in
    switch response {
    case .success(let result):
        let code = result.code
        // Submit a given TOTP code to the server
        TSIdo.submitClientResponse(clientResponseOptionId: .clientInput, data: ["totp_code": code])
    case .failure(let error):
        debugPrint("[DEBUG]: error: \(error)")
    }
}