Skip to main content

Transaction Flow

There are 3 major steps that your application should implement in order to have the transaction flow completely implemented using RTAS:

  • Create Token (application-server) - Creates a RTAS token for the front-end to listen to events, and sends the registration created event.
  • Listen RTAS Events (front-end) - Front-end implementation for each RTAS event
  • Handle Callbacks (application-server) - Handles and sends transaction events to the front-end user

There are four events for the RTAS transaction flow.

Create Token

Application server should call RTAS using the RTAS OpenAPI client to get a transaction token. This transaction token should be forwarded to the front-end module that will use it in order to establish a connection to RTAS Servers.

When a new transaction is created, the application server should notify the RTAS Server that a new transaction was created.

Transaction Created Event

This event serves to provide some information to the frontend about the transaction, such as the transaction ID and other properties for a simpler implementation of the UI.

The OpenAPI client provided only has one API for the transaction callbacks. This means that there are properties that are shared between the events and properties that are unique to the event. These are the properties of the registration created event:

PropertyIs Required
StatusYes
Application NameYes
User IDYes
User display nameYes
Transaction IDYes
Contract KeyYes
Deeplink URLYes
Transaction ExpirationNo

Example Implementation

An application server implementation would look like:

package main

import (
"encoding/json"
"net/http"
"context"

rtas "securityside.com/rtas/go-sdk"
)

const (
RTASFrontEndURL = "" // Load from config file. This value should be provided by SecuritySide team.
ApplicationName = "Your application" // Value representing the application name.
RTASAddress = "" // Load from config file. This value should be provided by SecuritySide team.
ClientID = "" // Load from config file. This value should be provided by SecuritySide team.
ClientSecret = "" // Load from config file. This value should be provided by SecuritySide team.
)

var (
RTASClient, _ = initializeRTAS(RTASAddress, ClientID, ClientSecret)
)

func initializeRTAS(rtasAddress, clientID, clientSecret string) (*rtas.APIClient, error) {
rtasCnf := rtas.NewConfiguration()
rtasCnf.Servers = rtas.ServerConfigurations{{URL: rtasAddress}}
apiClient := rtas.NewAPIClient(rtasCnf)

token, resp, err := apiClient.LoginApi.LoginExecute(apiClient.LoginApi.Login(context.Background()).Body(*rtas.NewLoginRequest(clientID, clientSecret)))
if err != nil {
return nil, err
}
defer resp.Body.Close()

rtasCnf.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token.Token))
go tokenRenew(apiClient, 30*time.Minute) // The token renews every 30 minutes. This token is used in order to authenticate the application server with the RTAS application.

return apiClient, nil
}

func tokenRenew(apiClient *rtas.APIClient, timerInterval time.Duration) {
ticker := time.NewTicker(timerInterval)
for range ticker.C {
token, resp, err := apiClient.TokenRenewApi.RenewTokenExecute(apiClient.TokenRenewApi.RenewToken(context.Background()))
if err != nil {
panic(err.Error())
}
_ = resp.Body.Close()

apiClient.GetConfig().AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token.Token))
}
}

type transactionFlowResponse struct {
TransactionID string `json:"transaction_id"`
DeepLinkURL string `json:"deep_link_url"`
RTASToken string `json:"rtas_token"`
RTASURL string `json:"rtas_url"`
}

func transactionFlowController(w http.ResponseWriter, r *http.Request) {
const (
userID = "john.doe"
userName = "John Doe"
)

// Call TF SDK to create a transaction for the user
const (
transactionID = "" // Transaction ID returned by TF SDK
transactionExpiration = 0 // Transaction Expiration returned by TF SDK (OPTIONAL VALUE: not every transaction has expiration)
deepLinkURL = "" // Deeplink to decide transaction. Retrieve this value by calling a TF SDK method.
contractKey = "" // Contract key returned by the TF SDK.
)

rtasToken, err := rtasCreateTransactionFlowToken(r.Context(), transactionID)
if err != nil {
panic(err)
}

// Send transaction created RTAS event here
if err := rtasSendTransactionStatusCreated(transactionID, userID, userName, contractKey, deepLinkURL, &transactionExpiration); err != nil{
panic(err)
}

resBytes, err := json.Marshal(transactionFlowResponse{
TransactionID: transactionID,
DeepLinkURL: deepLinkURL,
RTASToken: rtasToken,
RTASURL: RTASFrontEndURL,
})
if err != nil {
panic(err)
}

w.WriteHeader(http.StatusOK)
if _, err := w.Write(resBytes); err != nil {
panic(err)
}
}

func rtasCreateTransactionFlowToken(ctx context.Context, transactionID string) (string, error) {
token, resp, err := RTASClient.EndUserTokenApi.TransactionEndUserTokenExecute(RTASClient.EndUserTokenApi.TransactionEndUserToken(ctx).Body(*rtas.NewEndUserTokenRequest(transactionID)))
if err != nil {
return "", err
}
defer resp.Body.Close()

return token.Token, nil
}

func rtasSendTransactionStatusCreated(transactionID, userID, userDisplayName, contractKey, deepLinkURL string, transactionExpiration *int64) error {
callbackRequestV2 := rtas.NewTransactionCallbackRequestV2(transactionID, rtas.TRANSACTION_CREATED_V2)
callbackRequestV2.TransactionExpiration = transactionExpiration
callbackRequestV2.SetUserId(userID)
callbackRequestV2.SetContractKey(contractKey)
callbackRequestV2.SetDeeplinkUrl(deepLinkURL)
callbackRequestV2.SetUserDisplayName(userDisplayName)
callbackRequestV2.SetApplicationName(ApplicationName)

// signalr connection properties
callbackRequestV2.SetCloseConnectionOnReceive(false)
callbackRequestV2.SetStoreInCache(true)
callbackRequestV2.SetCacheExpiresAfterMinutes(10)

_, resp, err := RTASClient.TransactionCallbackApi.TransactionCallbackV2Execute(RTASClient.TransactionCallbackApi.TransactionCallbackV2(context.Background()).Body(*callbackRequestV2))
if err != nil {
return err
}
defer resp.Body.Close()

return nil
}

Listen RTAS Events

In order to listen for RTAS Events the end-user should receive a RTAS Token and Endpoint URL from the application server so it can initialize the TF-Realtime module.

Web

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"></meta>
<meta http-equiv="X-UA-Compatible" content="IE=edge"></meta>
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
<title>TF-Realtime - Transaction</title>
</head>
<body>
<div class="rtas"></div>

<script src="https://cdn.securityside.com/packages/realtime-auth/tf-realtime/9.0.2/index.js"></script>
<script>

// Make a request that initialize transaction flow
fetch("https://your_backend_url.com/transfer/create", {
method: "POST",
data: {
// Your payload
"source_account": "GB33BUKB20201566644543",
"destination_account": "PT50000201234321598790154",
"amount": 1,
"currency": "usd",
}
})
.then((response) => response.json())
.then((responseData) => {

// responseData: {
// "rtas_url": "https://signalr-ha.dev.securityside.com",
// "rtas_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJncm91cHNpZCI6ImFwcDp0Zi1kZXYtYmFja29mZml...",
// }

new TFRealTime()
.setTargetElement("div.rtas")
.setUrl(responseData.rtas_url)
.setToken(responseData.rtas_token)
.setOnErrorCallback((errorData) => console.log("OnError -> ", errorData))
.setOnConnectionEstablishedCallback(() => console.log("-= OnConnectionEstablished =-"))
.setLanguage("PT_PT")
.setTransactionType()
.setOnTransactionPendingCallback((data) => {
console.log("OnTransactionPending => ", data);
// data: {
// "application_name": "Your Application",
// "contract_key": "jGa/YXvUIi38xwwZUNETGyoesAmSjlCa2g9K/h/NQR8=",
// "deeplink_url": "https://open.dev.trustfactor.securityside.com/transaction?contractKey=...",
// "transaction_expiration": 1678108194,
// "transaction_id": "9bd7c8e246a64d0bac2ca7e32ef10f83",
// "user_display_name": "John Doe",
// "user_id": "john.doe"
// }
})
.setOnTransactionAcceptedCallback((data) => {
console.log("OnTransactionAccepted => ", data);
// data: {
// "application_name": "Your Application",
// "contract_key": "jGa/YXvUIi38xwwZUNETGyoesAmSjlCa2g9K/h/NQR8=",
// "device_key": "vUdD4bmV2+BKSDidO7CN44R2085nnDbz1oMcaCw3Cuw=",
// "transaction_id": "9bd7c8e246a64d0bac2ca7e32ef10f83",
// "user_display_name": "John Doe",
// "user_id": "john.doe"
// }
})
.setOnTransactionDeclinedCallback((data) => {
console.log("OnTransactionDeclined => ", data);
// data: {
// "application_name": "Your Application",
// "contract_key": "jGa/YXvUIi38xwwZUNETGyoesAmSjlCa2g9K/h/NQR8=",
// "device_key": "vUdD4bmV2+BKSDidO7CN44R2085nnDbz1oMcaCw3Cuw=",
// "transaction_id": "9bd7c8e246a64d0bac2ca7e32ef10f83",
// "user_display_name": "John Doe",
// "user_id": "john.doe"
// }
})
.setOnTransactionExpiredCallback((data) => {
console.log("OnTransactionExpired => ", data);
// data: {
// "application_name": "Your Application",
// "contract_key": "jGa/YXvUIi38xwwZUNETGyoesAmSjlCa2g9K/h/NQR8=",
// "transaction_id": "9bd7c8e246a64d0bac2ca7e32ef10f83",
// "user_display_name": "John Doe",
// "user_id": "john.doe"
// }
})
.build();

})
.catch((error) => {
console.error("Error:", error);
});

</script>
</body>
</html>

Mobile

In order to properly implement RTAS on a mobile application, the application server should expose an endpoint serving a static HTML page with the following content:

    <!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="rtas"></div>
<script src="https://cdn.securityside.com/packages/realtime-auth/tf-realtime/9.0.2/index.js"></script>
</body>
</html>
package com.securityside.realtime;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.util.Log;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;

/**
* MainActivity is the main activity for the Android application.
* It uses a WebView to load and interact with web content that includes JavaScript code
* for real-time registration and transaction decisions. This activity also includes
* click listeners for two buttons: one for registration and one for decision-making.
*/
public class MainActivity extends AppCompatActivity {
// URL for RTAS endpoint
private static final String URL = "https://signalr.rtas.trustfactor.cloud";
// Authentication token
private static final String TOKEN = "YOUR_AUTH_TOKEN_HERE";

// TAG for logging
private static final String TAG = MainActivity.class.getSimpleName();

// UI elements
private WebView mWebView;
private Button mBtnDecision;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setView(); // Initialize UI elements
setListeners(); // Set click listeners for buttons
}

/**
* Initializes the UI elements, including the WebView and buttons.
* Also, enables JavaScript in the WebView.
*/
@SuppressLint("SetJavaScriptEnabled")
private void setView() {
mWebView = findViewById(R.id.webView); // Initialize the WebView element
mBtnDecision = findViewById(R.id.btnDecision); // Initialize the Decision button
mWebView.getSettings().setJavaScriptEnabled(true); // Enable JavaScript in WebView
// This setting enables the use of DOM storage, which is typically needed for modern web applications. It allows websites to store data locally in the browser, improving performance.
mWebView.getSettings().setDomStorageEnabled(false);
// By setting this to false, you are disabling the ability for websites displayed in the WebView to access the device's file system. This can help prevent unauthorized access to sensitive files.
mWebView.getSettings().setAllowFileAccess(false);
// This setting disables web content from accessing the device's content provider. It restricts access to device content and enhances security.
mWebView.getSettings().setAllowContentAccess(false);
// When set to false, this setting prevents websites from accessing files via "file://" URLs. This is a good security measure to prevent web content from reading local files.
mWebView.getSettings().setAllowFileAccessFromFileURLs(false);
// Disabling universal access from file URLs ensures that web content cannot have unrestricted access to resources on the local file system. This setting is important for security.
mWebView.getSettings().setAllowUniversalAccessFromFileURLs(false);
}

/**
* Sets click listeners for the Registration and Decision buttons.
* When clicked, these buttons load different JavaScript interfaces and HTML content
* into the WebView for real-time authentication and transaction decisions.
*/
private void setListeners() {
// Set click listener for the Decision button
mBtnDecision.setOnClickListener(v -> {
try {
// Add a JavaScript interface for Transaction Decision
mWebView.addJavascriptInterface(new WebAppInterfaceTransactionDecision(MainActivity.this, URL, TOKEN), "WebAppInterfaceTransactionDecision");
mWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// Add and execute JavaScript code for Transaction Decision
String javascriptCode =
"javascript:(function () {" +
" var url = WebAppInterfaceTransactionDecision.getUrl();" +
" var token = WebAppInterfaceTransactionDecision.getToken();" +
// JavaScript code for Transaction Decision
" new TFRealTime()\n" +
" .setTargetElement(\"div.se\")\n" +
" .setLanguage(\"PT_PT\")\n" +
" .setToken(token)\n" +
" .setUrl(url)\n" +
" .setOnErrorCallback((errorData) => {WebAppInterfaceTransactionDecision.onError(JSON.stringify(errorData));})\n" +
" .setOnConnectionEstablishedCallback(() => {WebAppInterfaceTransactionDecision.OnConnectionEstablished();})\n" +
" .setTransactionType()\n" +
" .setOnTransactionPendingCallback((data) => {WebAppInterfaceTransactionDecision.onPending(JSON.stringify(data));})\n" +
" .setOnTransactionExpiredCallback((data) => {WebAppInterfaceTransactionDecision.onExpired(JSON.stringify(data));})\n" +
" .setOnTransactionDeclinedCallback((data) => {WebAppInterfaceTransactionDecision.onDeclined(JSON.stringify(data));})\n" +
" .setOnTransactionAcceptedCallback((data) => {WebAppInterfaceTransactionDecision.onAccepted(JSON.stringify(data));})\n" +
" .build();\n" +
" })();";
// Execute the JavaScript code using evaluateJavascript
mWebView.evaluateJavascript(javascriptCode, value -> Log.d("WebView", value));
}
});
// Load the decision HTML page from the assets folder
mWebView.loadUrl("https://your_backend_url.com");
} catch (Exception exception) {
Log.e(TAG, exception.getMessage());
}
});
}
}

The application should inject javascript after the document is loaded on the webview in order to properly initialize the RTAS client and create the WebSocket connection.

After this, the end-user should see an animation while the transaction is still pending for decision.

Handle Callbacks

Handle the TrustFactor transactions callbacks.

Transaction Decided by the user

The transaction decision by the users represents two events:

  • Transaction Accepted
  • Transaction Declined

Meaning the user either accepts or declines the transaction. There is a third option that is the user lets the transaction expire. More information about how to handle transaction expiration event can be found here.

The OpenAPI client provided only has one API for the transaction callbacks. This means that there are properties that are shared between the events and properties that are unique to the event. These are the properties of the registration created event:

PropertyIs Required
StatusYes
Application NameYes
User IDYes
User display nameYes
Transaction IDYes
Contract KeyYes
Device KeyYes
AuthenticationsNo

Transaction Expired Event

This event occurs when the user does not make a decision in the transaction expiration time. This event is sent automatically to the TrustFactor callback.

The OpenAPI client provided only has one API for the transaction callbacks. This means that there are properties that are shared between the events and properties that are unique to the event. These are the properties of the registration created event:

PropertyIs Required
StatusYes
Application NameYes
User IDYes
User display nameYes
Transaction IDYes
Contract KeyYes

Example Implementation

The application server should send the final status of the transaction by implementing the TF TransactionDecision callback.

In order to notify the end-user about the final status of the transaction, your code should look like:

package main

import (
"net/http"
"context"

tfsdk "securityside.com/tf/go-sdk"
rtas "securityside.com/rtas/go-sdk"
)

const (
ApplicationName = "Your application" // Value representing the application name.
RTASAddress = "" // Load from config file. This value should be provided by SecuritySide team.
ClientID = "" // Load from config file. This value should be provided by SecuritySide team.
ClientSecret = "" // Load from config file. This value should be provided by SecuritySide team.
)

var (
RTASClient, _ = initializeRTAS(RTASAddress, ClientID, ClientSecret)
)

func initializeRTAS(rtasAddress, clientID, clientSecret string) (*rtas.APIClient, error) {
rtasCnf := rtas.NewConfiguration()
rtasCnf.Servers = rtas.ServerConfigurations{{URL: rtasAddress}}
apiClient := rtas.NewAPIClient(rtasCnf)

token, resp, err := apiClient.LoginApi.LoginExecute(apiClient.LoginApi.Login(context.Background()).Body(*rtas.NewLoginRequest(clientID, clientSecret)))
if err != nil {
return nil, err
}
defer resp.Body.Close()

rtasCnf.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token.Token))
go tokenRenew(apiClient, 30*time.Minute) // The token renews every 30 minutes. This token is used in order to authenticate the application server with the RTAS application.

return apiClient, nil
}

func tokenRenew(apiClient *rtas.APIClient, timerInterval time.Duration) {
ticker := time.NewTicker(timerInterval)
for range ticker.C {
token, resp, err := apiClient.TokenRenewApi.RenewTokenExecute(apiClient.TokenRenewApi.RenewToken(context.Background()))
if err != nil {
panic(err.Error())
}
_ = resp.Body.Close()

apiClient.GetConfig().AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token.Token))
}
}

func tfTransactionDecisionCallbackController(w http.ResponseWriter, r *http.Request) error {
// Handle TF Callback
cb := tfsdk.Transaction{}

if err := rtasNotifyTransactionDecision(cb); err != nil {
panic(err)
}

w.WriteHeader(http.StatusOK)

return nil
}

func rtasNotifyTransactionDecision(cb tfsdk.Transaction,) error {
const (
// Get the username from the user id in the callback model
userName = "John Doe"
)
tfSDKStatusToRTASStatus := func(sdkStatus tfsdk.TransactionStatus) rtas.TransactionStatusV2 {
switch sdkStatus {
case tfsdk.TransactionExpired:
return rtas.TRANSACTION_EXPIRED_V2
case tfsdk.TransactionAccepted:
return rtas.TRANSACTION_ACCEPTED_V2
case tfsdk.TransactionDeclined:
return rtas.TRANSACTION_DECLINED_V2
default:
return ""
}
}

tfSDKAuthenticationsToRTASAuthentications := func(sdkAuthentication tfsdk.AuthenticationMechanism) rtas.AuthenticationsMechanisms {
switch sdkAuthentication {
case tfsdk.AuthenticationPassword:
return rtas.PASSWORD
case tfsdk.AuthenticationBiometrics:
return rtas.BIOMETRICS
case tfsdk.AuthenticationDefault:
return rtas.DEFAULT_AUTHENTICATION
default:
return ""
}
}

// Expired callback does not have device key and authentications
callbackRequestV2 := rtas.NewTransactionCallbackRequestV2(cb.TransactionID, tfSDKStatusToRTASStatus(cb.Status))
// Property 'DevicePub' is null when the transaction expires
if cb.DevicePub != nil {
callbackRequestV2.SetDeviceKey(cb.DevicePub.ToString())
}
callbackRequestV2.SetUserId(cb.AppUserUniqueID)
callbackRequestV2.SetUserDisplayName(userName)
callbackRequestV2.SetApplicationName(ApplicationName)
callbackRequestV2.SetContractKey(cb.ContractKey.ToString())
var authentications []rtas.AuthenticationsMechanisms
for index := range cb.Authentications {
authentications = append(authentications, tfSDKAuthenticationsToRTASAuthentications(cb.Authentications[index]))
}
callbackRequestV2.SetAuthentications(authentications)

// signalr connection properties
callbackRequestV2.SetCloseConnectionOnReceive(true)
callbackRequestV2.SetStoreInCache(true)
callbackRequestV2.SetCacheExpiresAfterMinutes(10)

_, resp, err := RTASClient.TransactionCallbackApi.TransactionCallbackV2Execute(RTASClient.TransactionCallbackApi.TransactionCallbackV2(context.Background()).Body(*callbackRequestV2))
if err != nil {
return err
}
defer resp.Body.Close()

return nil
}