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.
- Transaction Created Event
- Transaction Accepted Event
- Transaction Declined Event
- Transaction Expired Event
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:
Property | Is Required |
---|---|
Status | Yes |
Application Name | Yes |
User ID | Yes |
User display name | Yes |
Transaction ID | Yes |
Contract Key | Yes |
Deeplink URL | Yes |
Transaction Expiration | No |
Example Implementation
An application server implementation would look like:
- Golang
- C#
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
}
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Realtime.Backend.Go.Api;
using Realtime.Backend.Go.Client;
using Realtime.Backend.Go.Model;
namespace ImplementSDK
{
internal class TransactionCreate
{
const string RTASFrontEndURL = ""; // Load from config file. This value should be provided by SecuritySide team.
const string ApplicationName = "Your application"; // Value representing the application name.
const string RTASAddress = ""; // Load from config file. This value should be provided by SecuritySide team.
const string ClientID = ""; // Load from config file. This value should be provided by SecuritySide team.
const string ClientSecret = ""; // Load from config file. This value should be provided by SecuritySide team.
private static Configuration RTASClient = InitializeRTAS(RTASAddress, ClientID, ClientSecret);
private static Configuration InitializeRTAS(string rtasAddress, string clientID, string clientSecret)
{
var configuration = new Configuration();
configuration.BasePath = rtasAddress;
var token = new LoginApi(rtasAddress).Login(new LoginRequest(clientID, clientSecret));
configuration.DefaultHeader = new Dictionary<string, string>()
{
{ "Authorization", "Bearer " + token._Token }
};
Task.Run(() => TokenRenew(configuration, 30)); // The token renews every 30 minutes. This token is used in order to authenticate the application server with the RTAS application.
return configuration;
}
// Method used to renew the RTAS application token to be able to communicate with the RTAS services.
private static void TokenRenew(Configuration configuration, int timeInternalMinutes)
{
var startTimeSpan = TimeSpan.Zero;
var periodTimeSpan = TimeSpan.FromMinutes(timeInternalMinutes);
var timer = new System.Threading.Timer((e) =>
{
var token = new TokenRenewApi(configuration).RenewToken();
configuration.DefaultHeader = new Dictionary<string, string>()
{
{ "Authorization", "Bearer " + token._Token }
};
}, null, startTimeSpan, periodTimeSpan);
}
private class TransactionFlowResponse
{
[JsonProperty("transaction_id")] public string TransactionID;
[JsonProperty("deep_link_url")] public string DeepLinkURL;
[JsonProperty("rtas_token")] public string RTASToken;
[JsonProperty("rtas_url")] public string RTASURL;
}
private static string TransactionFlowController()
{
const string userId = "";
const string userName = "";
// Call TF SDK to create a registration code for the user
const string deeplinkUrl = ""; // Call TF SDK method to retrieve a valid deeplink URL using the TF SDK response
const string transactionID = ""; // Transaction ID returned by TF SDK
const long transactionExpiration = 0; // Transaction Expiration returned by TF SDK (OPTIONAL VALUE: not every transaction has expiration)
const string contractKey = ""; // Contract key returned by the TF SDK.
var rtasToken = RTASCreateTransactionFlowToken(transactionID);
// Send transaction created RTAS event here
RTASSendTransactionStatusCreated(transactionID, userId, userName, contractKey, deeplinkUrl, transactionExpiration);
var res = JsonConvert.SerializeObject(new TransactionFlowResponse
{
TransactionID = transactionID,
DeepLinkURL = deeplinkUrl,
RTASToken = rtasToken,
RTASURL = RTASFrontEndURL
});
return res;
}
// Returns the token and the token timestamp
private static string RTASCreateTransactionFlowToken(string transactionID)
{
var token = new EndUserTokenApi(RTASClient).TransactionEndUserToken(
new EndUserTokenRequest
{
TransactionId = transactionID
});
return token._Token;
}
private static void RTASSendTransactionStatusCreated(string transactionID, string userId, string userName, string contractKey, string deepLinkUrl, long transactionExpiration)
{
var body = new TransactionCallbackRequestV2()
{
TransactionStatus = TransactionStatusV2.TransactionCreatedV2,
TransactionId = transactionID,
TransactionExpiration = transactionExpiration,
UserId = userId,
UserDisplayName = userName,
ContractKey = contractKey,
DeeplinkUrl = deepLinkUrl,
ApplicationName = ApplicationName,
// signalr connection properties
CloseConnectionOnReceive = false,
StoreInCache = true,
CacheExpiresAfterMinutes = 10
};
new TransactionCallbackApi(RTASClient).TransactionCallbackV2(body);
}
}
}
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 RTAS UI module.
Web
@securityside/rtas-ui to see library specifics
<!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>RTAS - Transaction</title>
</head>
<body>
<div id="rtas"></div>
<script src="https://cdn.securityside.com/packages/realtime-auth/rtas-ui/0.0.3/index.umd.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 rtasui.RTASUI()
.setTargetElement("#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/rtas-ui/0.0.3/index.umd.js"></script>
</body>
</html>
- Android (Kotlin)
- Android (Java)
- iOS (Swift)
package com.securityside.realtime.kt
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Button
import androidx.fragment.app.Fragment
import com.securityside.realtime.R
class WebViewFragmentKt : Fragment() {
// URL for RTAS endpoint
private val URL = "https://signalr.rtas.trustfactor.cloud"
// Authentication token
private val TOKEN = "YOUR_AUTH_TOKEN_HERE";
private val TAG = WebViewFragmentKt::class.java.simpleName
private lateinit var mWebView: WebView
private lateinit var mBtnDecision: Button
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_web_view_kt, container, false)
}
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mWebView = view.findViewById(R.id.webView) // Initialize the WebView element
mBtnDecision = view.findViewById(R.id.btnDecision) // Initialize the Decision button
mWebView.settings.javaScriptEnabled = true
mWebView.settings.domStorageEnabled = true
mWebView.settings.allowFileAccess = false
mWebView.settings.allowContentAccess = false
setListeners() // Load WebView content
}
private fun setListeners() {
// Set click listener for the Registration button
mBtnDecision.setOnClickListener {
try {
// Add a JavaScript interface for Transaction Decision
mWebView.addJavascriptInterface(
WebAppInterfaceTransactionDecisionKt(requireActivity(), URL, TOKEN),
"WebAppInterfaceTransactionDecisionKt"
)
mWebView.webViewClient = object: WebViewClient() {
override fun onPageFinished(view: WebView ? , url : String ? ) {
super.onPageFinished(view, url)
// Add and execute JavaScript code for Transaction Decision
val javascriptCode = """
javascript: (function() {
var url = WebAppInterfaceTransactionDecisionKt.getUrl();
var token = WebAppInterfaceTransactionDecisionKt.getToken();
new rtasui.RTASUI()
.setTargetElement("#rtas")
.setLanguage("PT_PT")
.setToken(token)
.setUrl(url)
.setOnErrorCallback(function(errorData) {
WebAppInterfaceTransactionDecisionKt.onError(JSON.stringify(errorData));
})
.setOnConnectionEstablishedCallback(function() {
WebAppInterfaceTransactionDecisionKt.onConnectionEstablished();
})
.setTransactionType()
.setOnTransactionPendingCallback(function(data) {
WebAppInterfaceTransactionDecisionKt.onPending(JSON.stringify(data));
})
.setOnTransactionExpiredCallback(function(data) {
WebAppInterfaceTransactionDecisionKt.onExpired(JSON.stringify(data));
})
.setOnTransactionDeclinedCallback(function(data) {
WebAppInterfaceTransactionDecisionKt.onDeclined(JSON.stringify(data));
})
.setOnTransactionAcceptedCallback(function(data) {
WebAppInterfaceTransactionDecisionKt.onAccepted(JSON.stringify(data));
})
.build();
})();
""".trimIndent()
// Execute the JavaScript code using evaluateJavascript
mWebView.evaluateJavascript(javascriptCode) {
value - >
if (value != null) {
Log.d("WebView", "JavaScript executed: $value")
} else {
Log.d("WebView", "JavaScript execution returned null")
}
}
}
}
// Load the decision HTML page from the assets folder
mWebView.loadUrl("file:///android_asset/sample.html")
} catch (exception: Exception) {
Log.e(TAG, exception.message ?: "Exception")
}
}
}
}
package com.securityside.realtime;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class WebViewFragment extends Fragment {
// 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";
private static final String TAG = WebViewFragment.class.getSimpleName();
private WebView mWebView;
private Button mBtnRegistration, mBtnDecision;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_web_view, container, false);
}
@SuppressLint("SetJavaScriptEnabled")
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mWebView = view.findViewById(R.id.webView); // Initialize the WebView element
mBtnDecision = view.findViewById(R.id.btnDecision); // Initialize the Decision button
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.getSettings().setDomStorageEnabled(true);
mWebView.getSettings().setAllowFileAccess(false);
mWebView.getSettings().setAllowContentAccess(false);
mWebView.getSettings().setAllowFileAccessFromFileURLs(false);
mWebView.getSettings().setAllowUniversalAccessFromFileURLs(false);
setListeners(); // Load WebView content
}
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(getActivity(), 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 rtasui.RTASUI()\n" +
" .setTargetElement(\"#rtas\")\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("file:///android_asset/sample.html");
} catch (Exception exception) {
Log.e(TAG, exception.getMessage());
}
});
}
}
//
// TrustFactorRealTime.swift
// SwiftDemo
//
// Created by pedro.costa on 22/04/2021.
//
import UIKit
import WebKit
protocol TrustFactorRealTimeDelegateProtocol: AnyObject {
func onConnectionClose()
func onErrorCallback(data: Any?)
func onConnectionEstablishedCallback()
func onTransactionPending(data: Any?)
func onTransactionAccepted(data: Any?)
func onTransactionExpired(data: Any?)
func onTransactionDeclined(data: Any?)
}
private enum MessageHandler: String, CaseIterable {
case onConnectionClose = "onConnectionClose"
case onErrorCallback = "onErrorCallback"
case onConnectionEstablishedCallback = "onConnectionEstablishedCallback"
case onTransactionAccepted = "onTransactionAccepted"
case onTransactionExpired = "onTransactionExpired"
case onTransactionDeclined = "onTransactionDeclined"
case onTransactionPending = "onTransactionPending"
}
class TrustFactorRealTime: NSObject, WKUIDelegate {
struct Configs {
enum Action {
case transaction,
registration
}
var action: Action
var url: URL
var token: String
var language: String?
init(action: Action, language: String? = nil, url: URL, token: String) {
self.action = action
self.url = url
self.token = token
self.language = language
}
}
private var configs: TrustFactorRealTime.Configs
private(set) var webView: WKWebView?
// weak to prevent memory leaks
weak var delegate: TrustFactorRealTimeDelegateProtocol?
required init(configs: TrustFactorRealTime.Configs) throws {
self.configs = configs
}
func start() -> WKWebView {
stop() // make sure we stop previous attempts
let configuration = WKWebViewConfiguration()
// common
for handler in MessageHandler.allCases {
configuration.userContentController.add(self, name: handler.rawValue)
}
// Build the javascript to run when the page is loaded
// callback parameters can be converted to a JSON string by using JSON.stringify (except for .setOnErrorCallback)
var js = """
new rtasui.RTASUI()
.setUrl(\"\(configs.url.absoluteString)\")
.setToken(\"\(configs.token)\")
.setTargetElement(\"#rtas\")
.setOnConnectionCloseCallback((error) => {window.webkit.messageHandlers.onConnectionClose.postMessage(error);})
.setOnErrorCallback((error) => {window.webkit.messageHandlers.onErrorCallback.postMessage(error);})
.setOnConnectionEstablishedCallback((connection) => {window.webkit.messageHandlers.onConnectionEstablishedCallback.postMessage({});})
"""
if let language = configs.language {
js += ".setLanguage(\"\(language)\")"
}
if configs.action == .transaction {
js += """
.setTransactionType()
.setOnTransactionAcceptedCallback((data) => {window.webkit.messageHandlers.onTransactionAccepted.postMessage(data);})
.setOnTransactionPendingCallback((data) => {window.webkit.messageHandlers.onTransactionPending.postMessage(data);})
.setOnTransactionExpiredCallback((data) => {window.webkit.messageHandlers.onTransactionExpired.postMessage(data);})
.setOnTransactionDeclinedCallback((data) => {window.webkit.messageHandlers.onTransactionDeclined.postMessage(data);})
"""
}
js += ".build();"
let request = URLRequest(url: URL(string: "https://your_backend_url.com")!)
configuration.userContentController.addUserScript(WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: true))
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.navigationDelegate = self
webView.uiDelegate = self
webView.load(request)
self.webView = webView
return webView
}
func stop() {
// prevent memory leaks
if #available(iOS 14.0, *) {
webView?.configuration.userContentController.removeAllScriptMessageHandlers()
} else {
for messageHandler in MessageHandler.allCases {
webView?.configuration.userContentController.removeScriptMessageHandler(forName: messageHandler.rawValue)
}
}
}
}
//MARK: - Message handlers
extension TrustFactorRealTime: WKScriptMessageHandler {
// receive the callbacks from javascript and call the native methods
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// there's no need to continue if there is no delegate
guard let delegate = delegate else { return }
switch MessageHandler(rawValue: message.name) {
case .onConnectionClose:
delegate.onConnectionClose()
case .onErrorCallback:
delegate.onErrorCallback(data: message.body)
case .onConnectionEstablishedCallback:
delegate.onConnectionEstablishedCallback()
case .onTransactionAccepted:
delegate.onTransactionAccepted(data: message.body)
case .onTransactionPending:
delegate.onTransactionPending(data: message.body)
case .onTransactionExpired:
delegate.onTransactionExpired(data: message.body)
case .onTransactionDeclined:
delegate.onTransactionDeclined(data: message.body)
default:
break
}
}
}
//MARK: - Navigation Delegate
extension TrustFactorRealTime: WKNavigationDelegate {
// Open links outside the current app
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard case .linkActivated = navigationAction.navigationType,
let url = navigationAction.request.url
else {
decisionHandler(.allow)
return
}
decisionHandler(.cancel)
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
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:
Property | Is Required |
---|---|
Status | Yes |
Application Name | Yes |
User ID | Yes |
User display name | Yes |
Transaction ID | Yes |
Contract Key | Yes |
Device Key | Yes |
Authentications | No |
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:
Property | Is Required |
---|---|
Status | Yes |
Application Name | Yes |
User ID | Yes |
User display name | Yes |
Transaction ID | Yes |
Contract Key | Yes |
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:
- Golang
- C#
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
}
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Realtime.Backend.Go.Api;
using Realtime.Backend.Go.Client;
using Realtime.Backend.Go.Model;
using TrustFactorSDK.V2.Callbacks;
using TrustFactorSDK.V2.Constants;
namespace ImplementSDK
{
internal class TransactionCallback
{
const string ApplicationName = "Your application"; // Value representing the application name.
const string RTASAddress = ""; // Load from config file. This value should be provided by SecuritySide team.
const string ClientID = ""; // Load from config file. This value should be provided by SecuritySide team.
const string ClientSecret = ""; // Load from config file. This value should be provided by SecuritySide team.
private static Configuration RTASClient = InitializeRTAS(RTASAddress, ClientID, ClientSecret);
private static Configuration InitializeRTAS(string rtasAddress, string clientID, string clientSecret)
{
var configuration = new Configuration();
configuration.BasePath = rtasAddress;
var token = new LoginApi(rtasAddress).Login(new LoginRequest(clientID, clientSecret));
configuration.DefaultHeader = new Dictionary<string, string>()
{
{ "Authorization", "Bearer " + token._Token }
};
Task.Run(() => TokenRenew(configuration, 30)); // The token renews every 30 minutes. This token is used in order to authenticate the application server with the RTAS application.
return configuration;
}
// Method used to renew the RTAS application token to be able to communicate with the RTAS services.
private static void TokenRenew(Configuration configuration, int timeInternalMinutes)
{
var startTimeSpan = TimeSpan.Zero;
var periodTimeSpan = TimeSpan.FromMinutes(timeInternalMinutes);
new System.Threading.Timer((e) =>
{
var token = new TokenRenewApi(configuration).RenewToken();
configuration.DefaultHeader = new Dictionary<string, string>()
{
{ "Authorization", "Bearer " + token._Token }
};
}, null, startTimeSpan, periodTimeSpan);
}
private static void TFTransactionDecisionCallbackController()
{
var cb = new Transaction();
RTASNotifyTransactionDecision(cb);
}
private static void RTASNotifyTransactionDecision(Transaction cb)
{
// Get the username from the user id in the callback model
const string userName = "John Doe";
List<AuthenticationsMechanisms> authentications = new List<AuthenticationsMechanisms>();
foreach (var auth in cb.Authentications)
{
authentications.Add(TFSDKAuthenticationsToRTASAuthentications(auth));
}
var body = new TransactionCallbackRequestV2()
{
TransactionStatus = TFSDKStatusToRTASStatus(cb.Status),
TransactionId = cb.TransactionID,
UserId = cb.AppUserUniqueID,
UserDisplayName = userName,
ContractKey = cb.ContractKey,
DeviceKey = cb.DeviceKey.ToString(),
ApplicationName = ApplicationName,
Authentications = authentications,
// signalr connection properties
CloseConnectionOnReceive = true,
StoreInCache = true,
CacheExpiresAfterMinutes = 10
};
new TransactionCallbackApi(RTASClient).TransactionCallbackV2(body);
}
private static TransactionStatusV2 TFSDKStatusToRTASStatus(TrustFactorSDK.V2.Constants.TransactionStatus status)
{
switch (status)
{
case TrustFactorSDK.V2.Constants.TransactionStatus.TransactionExpired:
return TransactionStatusV2.TransactionExpiredV2;
case TrustFactorSDK.V2.Constants.TransactionStatus.TransactionAccepted:
return TransactionStatusV2.TransactionAcceptedV2;
case TrustFactorSDK.V2.Constants.TransactionStatus.TransactionDeclined:
return TransactionStatusV2.TransactionDeclinedV2;
}
return TransactionStatusV2.TransactionExpiredV2;
}
private static AuthenticationsMechanisms TFSDKAuthenticationsToRTASAuthentications(AuthenticationMechanism auth)
{
switch (auth)
{
case AuthenticationMechanism.AuthenticationPassword:
return AuthenticationsMechanisms.Password;
case AuthenticationMechanism.AuthenticationBiometrics:
return AuthenticationsMechanisms.Biometrics;
case AuthenticationMechanism.AuthenticationDefault:
return AuthenticationsMechanisms.DefaultAuthentication;
}
return AuthenticationsMechanisms.DefaultAuthentication;
}
}
}