Orchestration guide: iOS SDK
This guide describes how to quickly integrate Identity Orchestration journeys into your iOS application.
Introduction
The Orchestration SDK enables client-side developers to build user experiences that interact with journeys.
Each journey contains a business workflow, such as login or secure user onboarding. It consists of backend steps, such as data processing or invoking external systems, and client-facing steps, such as a login form or document verification. The SDK allows the client application to present screens and prompt for user input, and then send this information back to the journey for further processing.
Whenever the client-side interaction requires additional Mosaic platform capabilities, such as passkeys or risk detection, the developer needs to activate the appropriate SDK module to perform the step and collect the required input. This allows separation of concerns between the journey workflow handling, and the specific capability used.
SDK lifecycle
Regardless of journey logic, the SDK lifecycle consists of the following:
initialize()
or initializeSDK()
(using .plist file).
𝟸. Activate startJourney()
and wait for the server response.𝟹. Check
TSIdoServiceResponse
and present the appropriate UX. If needed, operate additional Mosaic SDKs.𝟺. Return client input via
submitClientResponse()
(allows selecting a journey branch if supported by journey step).𝟻. Repeat 3 and 4 until receiving
success
or rejection
.
The above is reflected in the following workflow diagram:
Requirements
- iOS 13.0+
- Xcode 11.0+
Before you start
Before the SDK can start running journeys, make sure to:
☐ Coordinate with the journey developer (regarding journey ID, steps, expected data, etc).
☐ Obtain the Client ID that identifies the app, from the app settings in the Admin Portal
☐ Allow Mosaic IPs and domains on your network
Step 1: Installation
The Mosaic iOS SDK is broken down into several modules that can be consumed using Swift Package Manager or Cocoa pods. The Mosaic Identity Orchestration SDK is available here.
-
To use
Swift Package Manager
: install the SDK as a dependency in your
Package.swift
. -
To use
CocoaPods
: specify the SDK in your
Podfile
.
dependencies: [
.package(url: "https://github.com/TransmitSecurity/identityOrchestration-ios-sdk.git", .upToNextMajor(from: "1.0.0"))
]
pod 'IdentityOrchestration', '~> 1.0.1'
Note
As part of the project setup, you might need to set permissions, based on the specific module being used.
For example, to collect Face ID permissions, provide an explanation for using them. Open the Info.plist file as a Property List and add Privacy - Face ID Usage Description key and provide a reason, such as "This app uses Face ID to provide secure authentication"
Step 2: Initialize SDK
Initializing the SDK configures it to work with your client ID, which is the context under which the entire journey is executed. It also allows setting the base URL for the SDK to use, whether a Mosaic-provided path or your own proxy.
Initialize using PLIST configuration (recommended)
This is a more standardized initialization process that also allows the initialization of multiple SDKs in a consistent manner. To do this, create a plist file named TransmitSecurity.plist
in your Application with the format described below and add this file to your iOS project.
Here's an example of the plist file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>credentials</key>
<dict>
<key>baseUrl</key>
<string>[BASE_URL]</string> <!--Default is https://api.transmitsecurity.io.-->
<key>clientId</key>
<string>[CLIENT_ID]</string>
</dict>
</dict>
</plist>
Here's an example of SDK initialization. You also need to set your app as a delegate to be able to process server responses in an async manner:
do {
try TSIdo.initializeSDK()
TSIdo.delegate = self // Allows to process server responses in async manner
} catch {
debugPrint("[DEBUG]: \(error)")
}
Note
Make sure to add import IdentityOrchestration
at the top of the implementation class.
Initialize using SDK parameters
To do this, configure the SDK using the snippet below:
TSIdo.initialize(
clientId: Env.clientId,
options: .init(serverPath: Env.baseUrl)
)
TSIdo.delegate = self
Note
Make sure to add import IdentityOrchestration
at the top of the implementation class.
Step 3: Start journey
Starting a journey is usually a response to some user interaction, such as clicking a login or sign-up button. The start parameters indicate which journey to start, and optionally pass additional parameters if the journey expects them.
For example, if the client app uses email as the user identifier and caches it locally after sign-up, it can pass it to the journey as userEmail
. The journey can reference it using this expression @policy.request().params.userEmail
// Implement delegate method
func TSIdoDidReceiveResult(_ result: Result<TSIdoServiceResponse, TSIdoJourneyError>) {
switch result {
case .success(let response):
self.handleJourneyActionUI(response) // implemented in Select handler section
case .failure(let error):
debugPrint("TSIdo SDK error: \(error)")
}
}
// start journey
TSIdo.startJourney(journeyId: <journey name>,
options: .init(additionalParams: <additional-params>, // Additional params
flowId: <flow-id>))
Note
Make sure to add import IdentityOrchestration
at the top of the implementation class.
Step 4: Handle service response
After starting the journey, the client app waits for the TSIdoServiceResponse
. It will contains all the information the client needs in order to determine what to do next.
To handle the service response, the client app needs to:
- Select a handler based on the step
- Use the data provided by the journey
- Respond to the journey with requested input
1. Select handler
Upon receiving the TSIdoServiceResponse
, launch a handler that executes the relevant step by switching/selecting on the TSIdoServiceResponse.journeyStepId
parameter. It will either contain one of these enums or .custom("step-id")
where step-id is the Step ID configured in the Login Form or Get Information from Client step. Each handler should process the data, display whatever UI is needed, and call submitClientResponse()
when the interaction is concluded.
This is how the app code would dispatch handlers based on this property:
private func handleJourneyActionUI(for response: IdentityOrchestration.TSIdoServiceResponse) {
guard let responseStepId = response.journeyStepId else { debugPrint("[DEBUG] No step id found in response"); return }
guard let actionData = response.data else { debugPrint("[DEBUG] No data found in response object"); return }
switch responseStepId {
case .information:
// Show information screen
case .emailOTPAuthentication:
// handle email OTP step
case .custom("main_login_form"):
// handle login form with the ID main_login_form
case .custom("user_info_form"):
// handle collect onformation step with the ID user_info_form
default:
debugPrint("[DEBUG] Unsupported step: \(responseStepId)")
}
}
2. Use data sent from journey
Inside the handler function, the app code should render a UI that fits the specific step, and use information from the data
property if it was provided in the TSIdoServiceResponse
object. This data is described in each step guide.
Let's see an example for a Register Passkeys step. TSIdoServiceResponse
will include journeyStepId
set to .webAuthnRegistration
and a data
object containing the user identifier and other WebAuthn configuration. This data can be used for calling the webauthn (passkey) registration module, as shown below:
// The actionData is taken from the TSIdoServiceResponse object
let actionData = response.data
let username = actionData["user"] as? [String: any]
let allowCrossPlatformAuthenticators = actionData["allow_cross_platform_authenticators"] as? [Bool: any]
let registerAsDiscoverable = actionData["register_as_discoverable"] as? [Bool: any]
let displayName = actionData["display_name"] as? [String: any]
// Now activate the passkey registration module, and return a response
TSAuthentication.shared.registerWebAuthn(username: username, displayName: displayName) { result in
switch result {
case .success(let response):
TSIdo.submitClientResponse(clientResponseOptionId: .clientInput,
data: ["webauthn_encoded_result": response.result])
case .failure(let error):
TSIdo.submitClientResponse(clientResponseOptionId: .fail)
debugPrint("[DEBUG] Error: \(error)")
}
}
3. Respond with collected data
Once the user interaction is concluded, the handler should collect the response data and submit it using submitClientResponse()
. This call yields control back to the journey, and returns with a new TSIdoServiceResponse
once the journey reaches the next client-facing step.
The call parameters are the collected data, and a response option identifier. We’ll talk more about these below. For now, let's follow the Register Passkeys example of making this call.
let data = ["webauthn_encoded_result": encodedRegistrationResult]
TSIdo.submitClientResponse(
clientResponseOptionId: .clientInput,
data: data
)
Note
Make sure to add import IdentityOrchestration
at the top of the implementation class.
About response data schema
Each journey step expects specific fields to be sent back in the data object. The details of the expected fields are described in the relevant step guide. As seen above, when we use the example of the Register Passkeys step, the expected data is a field called webauthn_encoded_result
which is the output of the Passkey registration SDK.
For most journey steps, the client response schema is fixed. However, there are two steps that expect a dynamic schema. The Login Form step allows presenting multiple authentication options and collects input for one of them. The Get Information from Client step allows presenting an arbitrary data collection form to the end user. For both steps, the expected data schema is configured in the step. The client developer must coordinate with the journey author for the expected schema for each Step ID.
About alternate branches and response options
Certain steps support multiple response options. These options are passed in the TSIdoServiceResponse
object in the clientResponseOptions
property. These represents possible replies that the client can provide as a response to a journey step, and affect the branch that is taken when the step is completed.
Three of these replies have a standard type (per SDK):
-
clientInput
represents a typical reply to a step that has a main or single output path. See above the example for Register Passkeys is using this reply option. This would be the common reply option to most steps. -
cancel
chooses the cancel branch in actions that support it -
fail
chooses the failure branch in actions that support it
Apart from those, some journey steps support alternate (custom) branches. The Branch ID is configured by the journey author, and should be used to identify the branch when calling submitClientResponse()
. The example below submits a response to the Login Form step, where one of the alternate branches proceeds to passkey authentication. This branch also expects a specific schema (that can be customized by the journey author) - the default is getting the encoded result after activating the authentication SDK.
let data = ["webauthn_encoded_result": encodedAuthenticationResult]
TSIdo.submitClientResponse(
clientResponseOptionId: .custom("passkey"),
data: data
)
Step 5: Complete journey
The orchestration service signals journey completion by setting the journeyStepId
property to rejection
or success
. The specific enum for each SDK is described in the Step 3.1 above. If successful, the token
property contains a token as a proof of journey completion.
Next steps
The SDK also supports the features below. For more info, talk to your Transmit Security representative.
- Generating a debug pin for the journey debugger
- Local console logs
- Resources URIs as part of SDK initialization