WebAuthn quick start: iOS SDK

Add strong authentication with Passkeys to your native iOS application, while providing a native experience. This describes how to use the iOS SDK to register credentials and use them to authenticate for login and transaction approval scenarios. For transaction approval flows, transactions are signed per PSD2.0 SCA.

How it works

This SDK implements Apple's public-private key authentication for passkeys. It allows you to add FIDO2-based biometric authentication to your iOS app, while providing your users with a native experience instead of a browser-based one.

With passkeys, credentials are securely stored by the device in the iCloud keychain. These credentials must be associated with your domain, so they can be shared between your mobile app and your website (if you have one). The Transmit iOS SDK also cryptographically binds the credentials to the device itself, which ensures that it can only be used by the device that registered it.

Benefits

The SDK offers many advantages over the APIs, including:

  • Orchestration functionality to support decisions and complex flows
  • Client-side WebAuthn calls, along with data processing before and after
  • Stores local state, including registration status of devices, known users, etc.
  • Simplifies all calls to the Transmit Service, reducing unnecessary complexity
  • Automatically handles tracking of cross-device flows, using simple event handlers

Requirements

The requirements for passkey authentication include:

  • iOS 15.0+ (or iOS 16.0+ for passkeys)
  • Xcode 13.0+ (or Xcode 14.0+ for passkeys)
  • Device with registered biometrics (e.g., FaceID or TouchID)
  • Device registered with the user's Apple ID
  • Device with iCloud KeyChain turned on

Sample flows

The sample flows demonstrate possible ways of implementing biometric authentication using Transmit iOS SDK. In the diagrams below, Transmit APIs are shown in pink along with the relevant integration step.

Registration

This flow represents a new user logging in from a device that supports passkeys. It assumes that you've collected and verified the username before initiating registration. The username should be a representation of the user in your system, such as their email, phone number, or another verifiable username known to the user (like account ID). Transmit APIs are shown in pink along with the relevant integration steps.

The SDK is initialized, and the device’s public key is returned so it can be added to the session. An authorized session is then created to establish a secure context for registration. For a better UX, the SDK prepares for registration by fetching the challenge from the Transmit Server and storing it locally. Once the user is ready to register their device, the SDK creates credentials, which may require the user to verify (e.g., via TouchID). Once completed, an authorization code is returned to the client to exchange for user tokens via their backend. Once tokens are validated, the user's device is registered.

Authentication

This flow represents an existing user authenticating using a registered device. It assumes that you've collected the username before initiating authentication.

Note

This integration flow also applies to transaction signing, except that the authentication challenge is derived from the transaction details, and these details must be validated in the ID token. Transmit APIs are shown in pink along with the relevant integration steps.

The SDK is initialized. For a better UX, the SDK prepares for authentication by fetching the challenge from the Transmit Server and storing it locally. Once the user is ready to authenticate, the SDK retrieves credentials, which may require the user to verify (e.g., via TouchID). Once completed, an authorization code is returned to the client to exchange for user tokens. Your client backend exchanges the auth code for ID and access tokens. Once tokens are validated, the user is logged in.

Step 1: Configure your app

To integrate with Transmit, you'll need to configure an application. From the Applications page, create a new application or use an existing one.

From the application settings:

  • For Client type , select native
  • For Redirect URI , enter your website URL. This is a mandatory field, but it isn't used for this flow.
  • Obtain your client ID and secret for API calls, which are autogenerated upon app creation.
  • Enable public sign-up if you manage users using an external system (e.g., external identity provider) or if you want to quickly test WebAuthn registration and authentication without first logging in using a different authentication method.

Step 2: Configure auth method

From the Authentication page, configure the WebAuthn login method for your application:

  • For WebAuthn RP ID , add your full website's domain (e.g., www.example.com ). This is the domain that will be associated with your credentials in Step 3.
  • For WebAuthn RP Origin , use https://YOUR_DOMAIN , where YOUR_DOMAIN is your website's domain that you configured as the RP ID (e.g., https://www.example.com ). This is the origin that will be provided when requesting registration and authentication with passkey credentials.

Step 3: Associate your domain

In order to support passkeys, Apple requires having a domain associated with the relevant credential type (as noted here). This is done by adding the associated domain file to your website, and the appropriate entitlement in your app.

To use Apple's sample code project, follow the steps described here. The domain should be set to your domain, and match the WebAuthn RP ID configured in Step 2. To learn more about the associated domain, click here.

Step 4: Install the SDK

CocoaPods

CocoaPods is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate TSAuthenticationSDK into your Xcode project using CocoaPods, specify it in your Podfile:

Copy
Copied
pod 'TSAuthenticationSDK', '~> 1.0.0'

Swift Package Manager

The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift compiler. It is in early development, but TSAuthenticationSDK does support its use on supported platforms.

Once you have your Swift package set up, adding TSAuthenticationSDK as a dependency is as easy as adding it to the dependencies value of your Package.swift.

Copy
Copied
dependencies: [
    .package(url: "https://github.com/TransmitSecurity/authentication-ios-sdk", .upToNextMajor(from: "1.0.0"))
]

Manually

If you prefer not to use any of the aforementioned dependency managers, you can integrate TSAuthenticationSDK into your project manually.

  • Download the TSAuthenticationSDK framework manually, open the new TSAuthenticationSDK folder, and drag the TSAuthenticationSDK.xcframework into the Project Frameworks directory of your application's Xcode project.
Note

The TSAuthenticationSDK.framework is automatically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device.

Step 5: Initialize the SDK

Configure the SDK by calling the initialize() SDK method using a snippet like the one below, where YOUR_DOMAIN is the associated domain used in Step 3 and CLIENT_ID is your client ID (obtained in Step 1).

If successful, the SDK returns the device's public key in the response (in response.publicKey). This corresponds to the key that cryptographically binds the device to the credentials. If it doesn't already exist, the SDK will generate one. All subsequent authentication requests will be signed using the private key, and the public key will be used to validate the signature. If credentials aren't already registered for this device, you'll need to pass the public key to your app backend so it can be sent to Transmit in Step 6.1.

Note

Make sure to add the import TSAuthenticationSDK at the top of the implementation class.

Copy
Copied
let config = TSConfiguration()
    config.domain = "YOUR_DOMAIN"
    TSAuthentication.shared.initialize(baseUrl: "https://webauthn.identity.security/v1", clientId: "CLIENT_ID", configuration: config) { response, error in
        if let error {
            print("SDK initialization failed \(String(describing: error.code)) \(String(describing: error.message))")
        } else {
            //Send the response.publicKey to the server
            print("SDK initialized")
        }
}

Step 6: Register credentials

To implement a credential registration flow, you'll need to:

  1. Create a secure context for registration
  2. Prepare registration for a better UX
  3. Perform the credential registration

1. Start an authorized session

From your backend, create an authorized authentication session for the user in order to provide the secure context required for WebAuthn registration. To do this, send the /auth-session/start-with-authorization request below from your client backend. The API returns the session ID of the created session, which you'll need for subsequent API calls.

In the request below, you should replace the following:

  • CLIENT_ID is the client ID retrieved from Step 1
  • USERNAME is a verified username, which should be a representation of the user in your system (such as their email, phone number, or another verifiable username known to the user like account ID).
  • TOKEN is either a client access token or a user access token depending on your login scenario . To quickly test a basic WebAuthn integration (without implementing any additional login methods or the full logic required for your use case), make sure public sign-up is enabled for your application and use a client access token .
Copy
Copied
const basePath = 'v1';
const resp = await fetch(
  `https://webauthn.identity.security/${basePath}/auth-session/start-with-authorization`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: 'Bearer TOKEN'  // Type of token depends on the use case, as explained above
    },
    body: JSON.stringify({
      client_id: 'CLIENT_ID',
      username: 'USERNAME',
      device_public_key: 'PUBLIC_KEY'
    })
  }
);

const data = await resp.json();
console.log(data);

2. Prepare registration

As soon as the username is known, call the prepareWebauthnRegistration() SDK call as shown below. Pass the username, and the session ID returned upon session creation. This call will generate the challenge required for registration.

Note

Make sure to add the import TSAuthenticationSDK at the top of the implementation class.

Copy
Copied
  TSAuthentication.shared.prepareWebauthnRegistration(username: "USERNAME", authSessionId: "AUTH_SESSION_ID") { [weak self] success, error in
              if let error {
                  //Handle error
              } else {
                  //Complete the registration flow
  } }

3. Complete registration

When the user is ready to proceed to the device registration (and only after calling prepareWebauthnRegistration()), call the executeWebAuthnRegistration() SDK method. This will prompt the user for biometrics. If successful, it returns a callback that resolves to the authorization code (response.authCode), which you'll exchange for a token via your backend in Step 9.

Note

Make sure to add the import TSAuthenticationSDK at the top of the implementation class.

Copy
Copied
  TSAuthentication.shared.executeWebauthnRegistration { [weak self] response, error in
      if let error {
        //Handle error
      } else {
        //User registered successfully. Use response.authCode for the token exchange
  } }

Step 7: Authenticate user

To implement an authentication flow, you'll need to:

  1. Prepare authentication
  2. Perform the authentication

1. Prepare authentication

When logging in users with registered devices, call the prepareWebauthnAuthentication() SDK call as soon as the username is known. This will generate the challenge required for authentication.

Note

Make sure to add the import TSAuthenticationSDK at the top of the implementation class.

For example:

Copy
Copied
  TSAuthentication.shared.prepareWebauthnAuthentication(username: "USERNAME") { [weak self] success, error in
          if let error {
              //Handle error
          } else {
              //Complete the authentication flow
  } }

2. Complete authentication

When the user is ready to proceed to authentication (and only after calling prepareWebauthnAuthentication()), call the executeWebAuthnAuthentication() SDK method. This will prompt the user for biometrics. If successful, this call returns a callback that resolves to the authorization code (response.authCode), which you'll exchange for a token via your backend in step 9.

Note

Make sure to add the import TSAuthenticationSDK at the top of the implementation class.

Copy
Copied
  TSAuthentication.shared.executeWebauthnAuthentication { [weak self] response, error in
      if let error {
          //Handle error
      } else {
        //User is authenticated. Use response.authCode for the token exchange
  } }

Step 8: Add transaction signing

To implement a transaction signing flow, you'll need to:

  1. Prepare transaction signing
  2. Perform the authentication

1. Prepare transaction signing

In a transaction signing flow, call the prepareWebauthnSignTransaction() SDK call when the user needs to approve a transaction. This will generate a challenge based on the transaction details.

The transactionData parameter contains the data that your customer should approve for a transaction signing flow. This should be the exact data that is displayed to the user or be derived from it (e.g. a hash). It can contain up to 10 key-value pairs, and only alphanumeric characters, underscores, hyphens, and periods. The data will be returned in the ID token upon successful authentication.

Note

Make sure to add the import TSAuthenticationSDK at the top of the implementation class.

For example:

Copy
Copied
  let approvalData = [ "payee": "Acme", "payment_method": "Acme card", "pay_amount": "200"]
      TSAuthentication.shared.prepareWebauthnSignTransaction(username: "USERNAME", approvalData: approvalData) { [weak self] success, error in
              if let error {
                //Handle error
              } else {
                //Complete the transaction signing flow
  } }

2. Complete transaction signing

When the user is ready to proceed to authentication (and only after calling prepareWebauthnSignTransaction()), call the executeWebAuthnSignTransaction() SDK method. This will prompt the user for biometrics. If successful, this call returns a callback that resolves to the authorization code (response.authCode), which you'll exchange for a token via your backend in step 9.

Note

Make sure to add the import TSAuthenticationSDK at the top of the implementation class.

Copy
Copied
   TSAuthentication.shared.executeWebauthnSignTransaction() { [weak self] response, error in
            if let error {
               //Handle error
            } else {
             //User has authenticated successfully. You can use the response.authCode for token exchange.
            }
}

Step 9: Get user tokens

In response to each successful authentication or registration, an authorization code is returned to your client application (frontend). After sending this code to your backend, exchange it for an ID and access token by sending a /oidc/token request like the one below from your backend . Replace placeholders with your redirect URI, and your client credentials that can be found in your application settings from the Transmit Admin Portal, along with the authorization code returned by the SDK upon completing authentication or registration (in response.authCode).

Copy
Copied
const formData = {
  client_id: 'CLIENT_ID',
  client_secret: 'CLIENT_SECRET',
  code: 'CODE',
  grant_type: 'authorization_code',
  redirect_uri: 'REDIRECT_URI'
};

const resp = await fetch(
  `https://api.transmitsecurity.io/oidc/token`,
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams(formData).toString()
  }
);

const data = await resp.text();
console.log(data);

Step 10: Validate tokens

A successful authentication returns ID and access tokens that should be validated as described here. For transaction signing flows, the ID token will include an approval_data claim that corresponds to the transactionData passed in step 8.1. This claim should be validated against the requested data, and what was presented to the user to approve.

Copy
Copied
{
  "tid": "8AEksjdhfkuwaefOrJ2",
  "email": "user@email.com",
  "groups": [],
  "new_user": false,
  "amr": [
    "webauthn"
  ],
  "roles": [],
  "auth_time": 1671471638,
  "at_hash": "f0MaRhWO9pLgFvuJVDhJqw",
  "aud": "83474278.8kjsdfuwe2.transmit",
  "exp": 1671475240,
  "iat": 1671471640,
  "iss": "https://userid.security",
  "approval_data": {
     "payment_amount": "200",
     "payee": "Acme",
     "payment_method": "Acme card"
   }
}

Next steps

Once you've tested your basic WebAuthn integration, you can complete your implementation. See Implement login scenarios for additional logic, like how to verify user identities before registering credentials.