Registration Flow
There are 3 major steps that your application should implement in order to have the registration 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 Callback (application-server) - Handles and sends registration events to the front-end user
There are two events for the RTAS registration flow.
Create Token
The application server calls RTAS using the RTAS OpenAPI client to get an authentication token. This authentication token is then forwarded to the front-end module along with the address to which the front-end connects to the RTAS.
RTAS token must be created using the unique_id
returned by the TF API.
After the registration token is created, the application server should notify the RTAS server that a new registration was created.
Registration Created event
This event serves to provide some information to the frontend about the new registration, such as the user ID, username and other properties for a simpler implementation of the UI.
The OpenAPI client provided only has one API for the registration callback. 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 |
QR Code | Yes |
QR Code expiration | Yes |
Deeplink URL | Yes |
Example Implementation
An application server implementation would look like:
- Golang
- C#
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
tfsdkclient "securityside.com/tf/go-sdk"
tfsdkrequests "securityside.com/tf/go-sdk/requests"
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 registrationFlowResponse struct {
QRCode string `json:"qr_code"`
DeepLinkURL string `json:"deep_link_url"`
RTASToken string `json:"rtas_token"`
RTASURL string `json:"rtas_url"`
}
func registrationFlowController(w http.ResponseWriter, r *http.Request) {
const (
userID = "john.doe"
userName = "John Doe"
)
// Call TF SDK to create a registration code for the user
registerResponse := tfsdkrequests.RegisterCodeV2Response{}
// Initialize Client with a ClientBuilder struct
client := tfsdkclient.Client{}
qrCodeByte, _ := client.GetRegistrationQRCode(context.Background(), registerResponse, 200)
qrCode := string(qrCodeByte)
deeplinkURL, _ := client.GenerateRegistrationDeeplinkURL(registerResponse)
rtasToken, err := rtasCreateRegistrationFlowToken(r.Context(), userID, registerResponse.UniqueID)
if err != nil {
panic(err)
}
// Send registration created RTAS event here
if err := rtasSendRegistrationCreatedCallback(r.Context(), userID, userName, qrCode, deeplinkURL, registerResponse.UniqueID, registerResponse.ExpiresAt); err != nil {
panic(err)
}
resBytes, err := json.Marshal(registrationFlowResponse{
QRCode: qrCode,
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 rtasCreateRegistrationFlowToken(ctx context.Context, userID, registrationCodeUniqueID string) (string, error) {
token, resp, err := RTASClient.EndUserRegistrationTokenApi.RegistrationEndUserTokenV2Execute(RTASClient.EndUserRegistrationTokenApi.RegistrationEndUserTokenV2(ctx).Body(*rtas.NewEndUserRegistrationTokenRequestV2(userID, registrationCodeUniqueID)))
if err != nil {
return "", err
}
defer resp.Body.Close()
return token.Token, nil
}
func rtasSendRegistrationCreatedCallback(ctx context.Context, userID, userName, qrCode, deepLinkURL, registrationCodeUniqueID string, qrCodeExpiration int64) error {
body := *rtas.NewRegistrationCallbackRequestV3(registrationCodeUniqueID, rtas.REGISTRATION_CREATED_V2, userID)
body.SetUserDisplayName(userName)
body.SetQrCode(qrCode)
body.SetQrCodeExpiration(qrCodeExpiration)
body.SetApplicationName(ApplicationName)
body.SetDeeplinkUrl(deepLinkURL)
// signalr connection properties
body.SetCloseConnectionOnReceive(false)
body.SetStoreInCache(true)
body.SetCacheExpiresAfterMinutes(15)
resp, err := RTASClient.RegistrationCallbackApi.RegistrationCallbackV3Execute(RTASClient.RegistrationCallbackApi.RegistrationCallbackV3(ctx).Body(body))
if err != nil {
var e rtas.GenericOpenAPIError
if errors.As(err, &e) {
fmt.Printf("RTAS Error\n%s\n%s\n", e.Error(), string(e.Body()))
return err
}
fmt.Printf("RTAS Error\n%s\n", err.Error())
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;
using TrustFactorSDK.V2;
using TrustFactorSDK.V2.Requests;
using TrustFactorSDK.V2.Util.Crypto;
namespace ImplementSDK
{
internal class RegistrationCreate
{
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 RegistrationFlowResponse
{
[JsonProperty("qr_code")] public string QRCode;
[JsonProperty("deep_link_url")] public string DeepLinkURL;
[JsonProperty("rtas_token")] public string RTASToken;
[JsonProperty("rtas_url")] public string RTASURL;
}
private string RegistrationFlowController()
{
const string userId = "";
const string userName = "";
// Call TF SDK to create a registration code for the user
RegisterCodeV2.Response registerResponse = new RegisterCodeV2.Response();
// Null values for demonstration purposes
var client = new Client(null, null, new PublicKey(""), new PrivateKey(""), "");
var qrCode = client.GetRegistrationQRCode(registerResponse, 200);
var deeplinkUrl = client.GenerateRegistrationDeeplinkURL(registerResponse);
var rtasToken = RtasCreateRegistrationFlowToken(userId, registerResponse.UniqueID);
RtasSendRegistrationCreatedCallback(userId, userName, qrCode, deeplinkUrl, registerResponse.UniqueID,
registerResponse.ExpiresAt);
var res = JsonConvert.SerializeObject(new RegistrationFlowResponse
{
QRCode = qrCode,
DeepLinkURL = deeplinkUrl,
RTASToken = rtasToken,
RTASURL = RTASFrontEndURL
});
return res;
}
// Returns the token
private static string RtasCreateRegistrationFlowToken(string userID, string registrationCodeUniqueId)
{
var token = new EndUserRegistrationTokenApi(RTASClient).RegistrationEndUserTokenV2(
new EndUserRegistrationTokenRequestV2()
{
UserId = userID,
RegistrationCodeUniqueId = registrationCodeUniqueId
});
return token._Token;
}
private static void RtasSendRegistrationCreatedCallback(string userId, string userName, string qrCode,
string deepLinkUrl ,string registrationCodeUniqueId, long qrCodeExpiration)
{
var body = new RegistrationCallbackRequestV3
{
Status = RegistrationStatusV2.RegistrationCreatedV2,
RegisterCodeUniqueId = registrationCodeUniqueId,
UserId = userId,
UserDisplayName = userName,
QrCode = qrCode,
QrCodeExpiration = qrCodeExpiration,
DeeplinkUrl = deepLinkUrl,
ApplicationName = ApplicationName,
// signalr connection properties
CloseConnectionOnReceive = false,
StoreInCache = true,
CacheExpiresAfterMinutes = 15
};
new RegistrationCallbackApi(RTASClient).RegistrationCallbackV3(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- Registration</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 registration flow
fetch("https://your_backend_url.com/tf/create-qr", { method: "POST" })
.then((response) => response.json())
.then((responseData) => {
// responseData: {
// "rtas_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJncm91cHNpZCI6ImFwcDp0Zi1kZXYtYmFja29mZmljZToyZmYyNjU4NC1mYWYxLTQwYmQtODg4My0xY2VhMjE1YTRlNWIsdXNlcjp0ZXN0ZV92YTQsdH...",
// "rtas_url": "https://signalr-ha.dev.securityside.com"
// }
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")
.setRegistrationType()
.setOnRegistrationPendingCallback((data) => {
console.log("OnRegistrationPending -> ", data);
// data: {
// "application_name": "Your application",
// "deeplink_url": "https://open.dev.trustfactor.securityside.com/register?payload=djIAAAAAAABkB...",
// "qr_code": "iVBORw0KGgoAAAANSUhEUgAAAfQAAAH0CAIAAABEtEjdAAAfr0lEQVR4nOzdT6gkV/k38Lm372RmMiZxM...",
// "qr_code_expiration": 1678104748,
// "user_display_name": "John Doe",
// "user_id": "john.doe"
// }
})
.setOnRegistrationSuccessCallback((data) => {
console.log("OnRegistrationSuccess -> ", data);
// data: {
// "application_name": "Your Application",
// "contract_key": "7D/Iw9i3NdYLbHfw0UsVk3LcJo8PpQXoOPIRBhvF4Bk=",
// "device_info": {
// "device_model": "TFVA",
// "manufacturer": "SecuritySide",
// "marketing_name": "SecuritySide TFVA",
// "name": "Virtual Agent #1",
// "operating_system": "TFVA",
// "os_version": "1.0"
// },
// "device_key": "N+RlRWij82dHZ4TJ/slTbRP+UltX9SGC5DMhZOULbP4=",
// "user_display_name": "John Doe",
// "user_id": "john.doe"
// }
})
.setOnRegistrationExpiredCallback((data) => {
console.log("OnRegistrationExpired -> ", data);
// data: {
// "application_name": "Your Application",
// "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 mBtnRegistration: 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
mBtnRegistration = view.findViewById(R.id.btnRegistration) // Initialize the Registration 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
mBtnRegistration.setOnClickListener {
try {
// Add a JavaScript interface for Registration
mWebView.addJavascriptInterface(
WebAppInterfaceRegistrationKt(requireActivity(), URL, TOKEN),
"WebAppInterfaceRegistrationKt"
)
mWebView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
// Add and execute JavaScript code for Registration
val javascriptCode = """
javascript:(function () {
var url = WebAppInterfaceRegistrationKt.getUrl();
var token = WebAppInterfaceRegistrationKt.getToken();
new rtasui.RTASUI()
.setTargetElement("#rtas")
.setLanguage("PT_PT")
.setToken(token)
.setUrl(url)
.setOnErrorCallback(function(errorData) {
WebAppInterfaceRegistrationKt.onError(JSON.stringify(errorData));
})
.setOnConnectionEstablishedCallback(function() {
WebAppInterfaceRegistrationKt.onConnectionEstablished();
})
.setRegistrationType()
.setOnRegistrationPendingCallback(function(data) {
WebAppInterfaceRegistrationKt.onPending(JSON.stringify(data));
})
.setOnRegistrationExpiredCallback(function(data) {
WebAppInterfaceRegistrationKt.onExpired(JSON.stringify(data));
})
.setOnRegistrationSuccessCallback(function(data) {
WebAppInterfaceRegistrationKt.onSuccess(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 registration 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
mBtnRegistration = view.findViewById(R.id.btnRegistration); // Initialize the Registration 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 Registration button
mBtnRegistration.setOnClickListener(v -> {
try {
// Add a JavaScript interface for Registration
mWebView.addJavascriptInterface(new WebAppInterfaceRegistration(getActivity(), URL, TOKEN), "WebAppInterfaceRegistration");
mWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// Add and execute JavaScript code for Registration
String javascriptCode =
"javascript:(function () {" +
" var url = WebAppInterfaceRegistration.getUrl();" +
" var token = WebAppInterfaceRegistration.getToken();" +
// JavaScript code for Registration
" new rtasui.RTASUI()\n" +
" .setTargetElement(\"#rtas\")\n" +
" .setLanguage(\"PT_PT\")\n" +
" .setToken(token)\n" +
" .setUrl(url)\n" +
" .setOnErrorCallback((errorData) => {WebAppInterfaceRegistration.onError(JSON.stringify(errorData));})\n" +
" .setOnConnectionEstablishedCallback(() => {WebAppInterfaceRegistration.OnConnectionEstablished();})\n" +
" .setRegistrationType()\n" +
" .setOnRegistrationPendingCallback((data) => {WebAppInterfaceRegistration.onPending(JSON.stringify(data));})\n" +
" .setOnRegistrationExpiredCallback((data) => {WebAppInterfaceRegistration.onExpired(JSON.stringify(data));})\n" +
" .setOnRegistrationSuccessCallback((data) => {WebAppInterfaceRegistration.onSuccess(JSON.stringify(data));})\n" +
" .build();\n" +
" })();";
// Execute the JavaScript code using evaluateJavascript
mWebView.evaluateJavascript(javascriptCode, value -> Log.d("WebView", value));
}
});
// Load the registration HTML page from the assets folder
mWebView.loadUrl("file:///android_asset/sample.html");
} catch (Exception exception) {
Log.e(TAG, exception.getMessage());
}
});
}
}
import UIKit
import WebKit
protocol TrustFactorRealTimeDelegateProtocol: AnyObject {
func onConnectionClose()
func onErrorCallback(data: Any?)
func onConnectionEstablishedCallback()
func onRegistrationPending(data: Any?)
func onRegistrationExpired(data: Any?)
func onRegistrationSuccess(data: Any?)
}
private enum MessageHandler: String, CaseIterable {
case onConnectionClose = "onConnectionClose"
case onErrorCallback = "onErrorCallback"
case onConnectionEstablishedCallback = "onConnectionEstablishedCallback"
case onRegistrationPending = "onRegistrationPending"
case onRegistrationExpired = "onRegistrationExpired"
case onRegistrationSuccess = "onRegistrationSuccess"
}
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 == .registration {
js += """
.setRegistrationType()
.setOnRegistrationPendingCallback((data) => {window.webkit.messageHandlers.onRegistrationPending.postMessage(data);})
.setOnRegistrationExpiredCallback((data) => {window.webkit.messageHandlers.onRegistrationExpired.postMessage(data);})
.setOnRegistrationSuccessCallback((data) => {window.webkit.messageHandlers.onRegistrationSuccess.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 .onRegistrationPending:
delegate.onRegistrationPending(data: message.body)
case .onRegistrationExpired:
delegate.onRegistrationExpired(data: message.body)
case .onRegistrationSuccess:
delegate.onRegistrationSuccess(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.
Handle Callback
Handle the TrustFactor registration callback.
Registration Success event
If the registration flow is completed (by registering a successfully TrustFactor contract), the application server should notify the RTAS Server using the callback received from TF to send a request to the RTAS server notifying the success of the operation.
The OpenAPI client provided only has one API for the registration callback. 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 |
Contract key | Yes |
Device Info | Yes |
Device Key | Yes |
Wrong Device Token | No |
Custom Data | No |
Wrong Device Token is short-lived token (ie: 2 minutes duration) that the application-server transmits to the front-end using the RTAS so the user can remove the newly registered device. This might be useful if the user registers the wrong device or someone else reads the QR Code from the screen of the legitimate user. The application server should have an endpoint validating this token that calls TF SDK ResetContract API.
Example Implementation
In order to notify the end-user about the success of the registration flow, your code should look like:
- Golang
- C#
package main
import (
"context"
"errors"
"fmt"
"net/http"
"time"
tfsdkcallback "securityside.com/tf/go-sdk/callback"
tfsdk "securityside.com/tf/go-sdk/models"
tfsdkcrypto "securityside.com/tf/go-sdk/util/crypto"
rtas "securityside.com/rtas/g-sdk/v4"
)
const (
RTASAddress = "" // Load from config file. This value should be provided by SecuritySide team.
ApplicationName = "Your application" // Value representing the application name.
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 tfRegisterCallbackController(w http.ResponseWriter, r *http.Request) error {
// Handle TF Callback
callback := tfsdkcallback.Register{
DeviceKey: tfsdkcrypto.PublicKey{},
ContractKey: tfsdkcrypto.PublicKey{},
DeviceInfo: tfsdk.DeviceInfo{},
DeviceIP: "",
DeviceLocation: nil,
AppUserUniqueID: "jonh.doe",
RegisterCodeUniqueID: nil,
}
const (
userName = "Jonh Doe" // This value should have a relation with AppUserUniqueID from the callback
)
var (
wrongDeviceToken = "" // Optional. The application server should expose an API that validates this token and allows the removal of the newly registered device.
)
if err := rtasSendRegistrationSuccessCallback(r.Context(), callback, userName, &wrongDeviceToken); err != nil {
panic(err)
}
w.WriteHeader(http.StatusOK)
return nil
}
func rtasSendRegistrationSuccessCallback(ctx context.Context, callback tfsdkcallback.Register, userName string, wrongDeviceToken *string) error {
tfSDKDeviceInfoToRTASDeviceInfo := func(deviceInfo tfsdk.DeviceInfo) *rtas.RegistrationCallbackRequestV2DeviceInfo {
registrationCallbackDeviceInfo := rtas.NewRegistrationCallbackRequestV2DeviceInfo(deviceInfo.Name, deviceInfo.Manufacturer, deviceInfo.Model, deviceInfo.OS, deviceInfo.OSVersion)
registrationCallbackDeviceInfo.MarketingName = &deviceInfo.MarketingName
return registrationCallbackDeviceInfo
}
body := *rtas.NewRegistrationCallbackRequestV3(*callback.RegisterCodeUniqueID, rtas.REGISTRATION_SUCCESS_V2, callback.AppUserUniqueID)
body.SetUserDisplayName(userName)
body.SetApplicationName(ApplicationName)
body.DeviceInfo = tfSDKDeviceInfoToRTASDeviceInfo(callback.DeviceInfo) // This method converts the TrustFactor SDK device into a RTAS device.
body.SetDeviceKey(callback.DeviceKey.ToString())
body.SetContractKey(callback.ContractKey.ToString())
body.WrongDeviceToken = wrongDeviceToken
// signalr connection properties
body.SetCloseConnectionOnReceive(true)
body.SetStoreInCache(true)
body.SetCacheExpiresAfterMinutes(15)
resp, err := RTASClient.RegistrationCallbackApi.RegistrationCallbackV3Execute(RTASClient.RegistrationCallbackApi.RegistrationCallbackV3(ctx).Body(body))
if err != nil {
var e rtas.GenericOpenAPIError
if errors.As(err, &e) {
fmt.Printf("RTAS Error\n%s\n%s\n", e.Error(), string(e.Body()))
return err
}
fmt.Printf("RTAS Error\n%s\n", err.Error())
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;
namespace ImplementSDK
{
internal class RegistrationCallback
{
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 TFRegisterCallbackController()
{
var callback = new Register(); // Handle TF callback
const string userName = "Jonh Doe"; // This value should have a relation with AppUserUniqueID from the callback
const string wrongDeviceToken = ""; // Optional. The application server should expose an API that validates this token and allows the removal of the newly registered device.
RtasSendRegistrationSuccessCallback(callback, userName, wrongDeviceToken);
}
private static void RtasSendRegistrationSuccessCallback(Register callback, string userName, string wrongDeviceToken)
{
var body = new RegistrationCallbackRequestV3
{
Status = RegistrationStatusV2.RegistrationSuccessV2,
RegisterCodeUniqueId = callback.RegisterCodeUniqueID,
UserId = callback.AppUserUniqueID,
UserDisplayName = userName,
ApplicationName = ApplicationName,
DeviceInfo = new RegistrationCallbackRequestV2DeviceInfo()
{
DeviceModel = callback.DeviceInfo.Model,
Manufacturer = callback.DeviceInfo.Manufacturer,
Name = callback.DeviceInfo.Name,
MarketingName = callback.DeviceInfo.MarketingName,
OperatingSystem = callback.DeviceInfo.OS,
OsVersion = callback.DeviceInfo.OSVersion
},
DeviceKey = callback.DeviceKey.ToString(),
ContractKey = callback.ContractKey.ToString(),
WrongDeviceToken = wrongDeviceToken,
// signalr connection properties
CloseConnectionOnReceive = true,
StoreInCache = true,
CacheExpiresAfterMinutes = 15
};
new RegistrationCallbackApi(RTASClient).Registrati˝onCallbackV3(body);
}
}
}