iOS Integration Guide

This guide is for developers integrating ZenKey with their iOS applications.

1.0 Background

ZenKey is a secure bridge between your users and the apps and services you provide. As the joint undertaking of the four major US wireless carriers — AT&T, Sprint, Verizon, and T-Mobile — ZenKey leverages encryption technologies in a user's mobile phone and mobile network. The platform packages multiple factors of authentication into a streamlined experience for app and website providers, taking advantage of the unique capabilities and insights of the wireless carriers. It then applies those capabilities to provide an easy and secure way to register, login, and perform other types of authorizations within apps and services. The result for you is a better user experience and a more secure link to users.

1.1 OpenID Connect

ZenKey makes integration easy by following the OpenID Connect (OIDC) authentication protocol. OpenID Connect is based on the OAuth 2.0 specification. It uses JSON Web Tokens (JWTs) obtained using OAuth 2.0 flows. The ZenKey SDK uses OIDC to support developers creating experiences in web and native applications. You can read more about OIDC here.

1.2 Authorization Flow on a Primary Device

Before a user can sign into your app using ZenKey, they must first install their carrier’s version of the ZenKey app on their primary device. After completing a simple initial setup, they may now use ZenKey to log in to third-party applications. Pressing the ZenKey button in a third-party app or website from their primary device starts the authentication process, prompting the user to set up their device up as a primary device if they have yet to do so. To learn more about what the authorization flow looks like in greater detail, please see Section 3.3: Authorization User Experience in our guide Enrolling and Managing Users.

Note: This primary device is also the device with which users can authenticate requests from other devices, such as desktops and tablets (see Section 1.3: Authorization on Secondary Devices).

Primary Flow

Step 1:   The user's wireless carrier is determined by the mccmnc — a six-digit number representing the user’s mobile country code (mcc) and mobile network code (mnc) — whose value allows for SIM and user authentication. The carrier returns an authorization code via your Redirect URI (see Section 4.1 Configure Your Redirect URI).

Step 2:   Your app or website makes an authorization code request to the local ZenKey app.

Step 3:   If the user consents to share information, your backend server issues a token request for user info and other resources.

1.3 Authorization on Secondary Devices

Users can also authenticate with ZenKey on devices other than their primary device, such as a tablet or laptop. These secondary devices rely on the user to complete the authentication process.

Users pressing the ZenKey button on a secondary device will see a visual and numeric code as a part of the secondary device authorization process. This code allows the user to associate that secondary device with their primary device.

Secondary Device Flow

Step 1:   The SDK presents the carrier Discovery UI. If the user is authorizing a secondary device from an app on a tablet, the SDK will use a webview for this step.

Step 2:   The user then scans the visual code or enters the numeric code into the ZenKey app on their primary device.

Step 3:   Once the user approves the request in the ZenKey app on their primary device, the carrier Discovery UI returns a login_hint_token to your app’s Redirect URI. This login_hint_token is only returned to a secondary device for use during the authentication request.

Step 4:   To perform SIM and user authentication, your app makes an authorization code request to the appropriate carrier and receives the auth code in its Redirect URI.

Step 5:   If the user consents to share information, your backend server issues a token request for user info and other resources.

1.4 User Data

To create a secure experience, user info and attributes are only shared via a web request from your secure backend to the secure backend of your user's carrier. User information is also only shared with you upon consent. Users are free to choose whether to share their data and specifically what data to share with you.

2.0 Getting Started

Before integrating the ZenKey SDK in your application, there are a few things you should do:

  • Log in to the Service Provider Portal.

  • Register your application and obtain a valid client_id and client_secret.

  • Consider whether you will need a custom Redirect URI. When a user authorizes your application, the ZenKey SDK will send an authorization code request to the appropriate carrier, then redirect the user back to the app via your Redirect URI. Your Redirect URI may also be used for callbacks to several ZenKey services. When you configure your Redirect URI in the Service Provider Portal, its scheme must resemble one of the following formats:

    • {client_id}://com.xci.provider.sdk
    • https://URL/URI
    • {custom}://
  • Identify which scopes, or user attributes, to capture when a user authorizes your app. During enrollment, users provide the ZenKey SDK with basic personal information (e.g. name, email, address) while other scopes originate from the carrier without user input (e.g. phone number). When users sign into your app with ZenKey, they can then elect to share that data with you. Besides the mandatory .openid scope, all other scopes are optional. Visit the Service Provider Portal to select which data attributes you wish to request through ZenKey.

3.0 Add the ZenKey SDK

From the Service Provider Portal, add the ZenKey SDK to your project (components here). To integrate ZenKey with your application project, you may use CocoaPods or add the SDK source to your Xcode project manually. Use of Carthage is not currently supported.

3.1 Using CocoaPods

You can include the ZenKey SDK in your project as a CocoaPod. Add the following code to your Podfile:

    pod 'ZenKeySDK'

3.2 Adding the SDK Source Manually to the Project Directory

To add the SDK source manually to your Xcode project:

  1. Move the ZenKey SDK source directly to the project directory.
  2. Add ZenKeySDK.xcodeproj to your application's Xcode project.
  3. After adding the project, confirm that your deployment target is greater than or equal to the SDK deployment target (in Xcode: Project > Info Panel).
  4. View your project's "Embedded Binaries" under your target's "General" panel. Add the ZenKeySDK framework. Be sure to select the corresponding framework for the platform you are targeting (the iOS framework for an iOS target).
  5. Build and run the project to ensure that everything is working correctly.

4.0 Configure Your Client ID

Open your project’s Info.plist and follow the steps below. Once complete, your Info.plist should resemble the sample shown here:

Plist Example
  1. First, copy your client_id from the ZenKey Service Provider Portal dashboard.

  2. Next, open your application’s Info.plist. Add a ZenKeyClientId key, then paste your client_id as the string value:

  1. Now add CFBundleURLTypes — the list of URL schemes supported by the app — as a key:

After adding this, Xcode will create an array with a "Type 0" dictionary containing a CFBundleURLName key.

  1. Add CFBundleTypeRole and CFBundleURLSchemes to the "Type 0" dictionary. If your project already has a CFBundleURLTypes key with an existing Type dictionary, this new data may still be added at any index.

  2. Add your client_id a second time, also as a string, under CFBundleURLSchemes:


4.1 Configure Your Redirect URI

You also need to configure the Redirect URI to be used. The Redirect URI is used for callbacks to the SDK from several ZenKey services.

If you wish to create a custom Redirect URI, access the Service Provider Portal and follow the instructions. When you configure your Redirect URI in the Service Provider Portal, remember that its scheme must resemble one of the following formats:

  • {client_id}://com.xci.provider.sdk
  • https://URL/URI
  • {custom}://

NOTE: For security purposes, ZenKey recommends specifying your Redirect URI as a universal link. This requires having the appropriately configured app association and entitlements. To learn more, please refer to Apple’s documentation.

To apply your custom Redirect URI, specify the custom scheme, host, and path in the Info.plist file.

    <string>{your universal link's host}</string>    
    <string>{your universal link's full path}</string>     

5.0 Instantiate ZenKey

To support the ZenKey SDK within your application, you must instantiate ZenKey in the application delegate as follows:

import ZenKeySDK
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication,
     didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

            didFinishLaunchingWithOptions: launchOptions

        // Perform additional application setup.

        return true

    func application(_ app: UIApplication,
                     open url: URL,
                     options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {

        guard !ZenKeyAppDelegate.shared.application(app, open: url, options: options) else {
            return true
        // Perform any other URL processing your app may need.
        return true


  • To enable logging for debugging, include the zenKeyOptions parameter and specify a logLevel (see Section 8.1: Debugging).

  • Apps using SceneDelegates or multiple windows are not currently supported. If your app is limited to a single scene, you can call ZenKeyAppDelegate.shared.application(open:) from within your single SceneDelegate in scene(openURLContexts:) instead of calling it from your AppDelegate as shown above.

6.0 Request Authorization Code

A ZenKey authorization can provide your application with the means to secure a user's registration, login, or authorization to processing an important transaction. The SDK provides the branded button ZenKeyAuthorizeButton to automatically submit a request for ZenKey authorization.

6.1 Add ZenKey Button

To add the ZenKey button to your application’s UI, create an instance of ZenKeyAuthorizeButton and add it to your UIView. Set a delegate to handle authorization.

import ZenKeySDK

class LoginViewController {
    let zenKeyButton = ZenKeyAuthorizeButton()

    override func viewDidLoad() {

        let scopes: [Scope] = [.openid, {desiredScope1}, {desiredScope2}, etc.]
        // Besides .openid, all scopes are optional. Please include only those scopes you truly need.

        zenKeyButton.scopes = scopes
        zenKeyButton.delegate = self

6.1.1 Dark Button

You can customize the appearance of the button. A dark button style is appropriate to use with light backgrounds. By default, the ZenKey button uses the dark style. The dark button style looks like this:

Verify Button Dark
6.1.2 Light Button

A light button style is appropriate to use with dark backgrounds. You can change the style by setting the button's style property as follows: = .light

The light button style looks like this:

Verify Button Light
6.1.3 Custom Button or View

Instead of the default ZenKeyAuthorizeButton, you can invoke ZenKey with your own custom button or view (for example, if your login UX is presented in a WebView). See the implementation details in Section 7.0: Request Authorization Code Manually.

6.2 Receive Callbacks

In order to receive the responses from a ZenKey request, implement ZenKeyAuthorizeButtonDelegate and assign delegate on the button (as shown in Section 6.1: Add ZenKey Button).

extension LoginViewController: ZenKeyAuthorizeButtonDelegate {

    func buttonWillBeginAuthorizing(_ button: ZenKeyAuthorizeButton) {
        // Perform any UI updates like showing an activity indicator.

    func buttonDidFinish(
        _ button: ZenKeyAuthorizeButton,
        withResult result: AuthorizationResult) {

        // Handle the outcome of the request:
        switch result {
        case .code(let authorizedResponse):
            let code = authorizedResponse.code
            let mccmnc = authorizedResponse.mccmnc
            let redirectURI = authorizedResponse.redirectURI
            let codeVerifier = authorizedResponse.codeVerifier
            let nonce = authorizedResponse.nonce
            let correlationId = authorizedResponse.correlationId // string
            let context = authorizedResponse.context // string
            // Pass these identifiers to your secure server to perform a token request.
        case .error(let authorizationError):
            // There was an error with the authorization request
        case .cancelled:
            // The user cancelled their request in the ZenKey application.

6.3 Request Parameters

There are several parameters that you can configure on your authorization button, as noted in this section.

6.3.1 Scopes

Select each of the userinfo scopes to be added to the authorization request.

Note: The .openid scope is required.

    let scopes: [Scope] = [.openid, .email, .name, .phone, .postalCode, .last_4_social]

For more info, see Scope.swift.

6.3.2 Additional Parameters

Other parameters you may also configure in your request include:

  • state - An optional variable you may pass during the discovery process and will receive along with the authorization code.

  • nonce - Any string whose value you provide in the authorization code request that will later be returned in the ID Token. The ID Token is returned by the .openid scope from the authorization request and is the primary extension of the OAuth 2.0 spec that OpenID provides.

  • context - While context is not required for basic sign-in requests, if you wish to give users more information about an interaction, you may do so by submitting a string along with the authorization request. For example, a bank transfer may prompt the user: "Do you want to authorize a $200 transfer to your checking account?". Note that the maximum size for the context parameter is 280 characters. Strings any larger will result in an OIDC error (e.g. invalid_request).

For more information about each of these parameters and instructions on how to use them, view the documentation in ZenKeyAuthorizeButton.swift. There is also more information on the enumerated values in PromptValue.swift.

6.3.3 Proof Key for Code Exchange

Proof Key for Code Exchange (PKCE) is a security extension to OAuth 2.0 for public clients on mobile devices. It helps prevent the interception of the authorization code on its return trip over an insecure transport protocol (e.g. deep linking between applications).

To support PKCE, the ZenKey SDK generates values for codeVerifier, codeChallengeMethod, and codeChallenge, using each as follows:

  1. Authorization. The ZenKey SDK passes a codeChallengeMethod and codeChallenge in the authorization request, then includes the codeVerifier in the AuthorizedResponse.

  2. Code Verifier to Your Server. Your app gets the codeVerifier string from the AuthorizedResponse and securely transmits it to your backend in order to include the codeVerifier in its token request.

  3. Token Request. Your secure server requests an Access Token from the carrier with the additional codeVerifier parameter.

  4. Access Token Sent. The carrier applies the codeChallengeMethod to the received codeVerifier, then compares the resulting codeChallenge to the codeChallenge received during the authorization request. If the two values match, the carrier grants an Access Token. If not, the server denies the request and throws an error.

6.3.4 Implementing Proof Key for Code Exchange (PKCE)

In light of the flow above, to implement PKCE, your app must be able to:

  1. Receive the codeVerifier value in the AuthorizedResponse;

  2. Securely transmit the codeVerifier to your backend after a successful authorization request;

  3. Configure your server to receive the codeVerifier via the authorization endpoint;

  4. Include the codeVerifier value in the token request to ensure the request was authored from the same source and not intercepted.

6.3.5 PKCE Errors
  1. No Code Verifier. If the server requires PKCE, but the client does not send a codeVerifier, the authorization endpoint will send an error set to invalid_request. The error_description or response of error_uri will likely explain the error (e.g.codeVerifier required).

  2. Incorrect Code Verifier. Since ZenKey sends a hashed version of the codeVerifier in the authorization request, when the carrier receives this codeVerifier in the token request they can re-hash it and validate whether the two values match. If not, you will receive a request_denied error.

7.0 Request Authorization Code Manually

You can perform a manual authorization request by configuring an AuthorizationService. Pass the code and associated identifiers to your secure server to complete the token request flow.

import ZenKeySDK

class LoginViewController {

    let authService = AuthorizationService()

    func loginWithZenKey() {
        // In response to some UI, perform an authorization using the AuthorizationService.
        let scopes: [Scope] = [.openid, .email, .name, .phone, .postalCode, .last_4_social]
            scopes: scopes,
            fromViewController: self) { result in

            switch result {
            case .code(let authorizedResponse):
                let code = authorizedResponse.code
                let mccmnc = authorizedResponse.mccmnc
                let redirectURI = authorizedResponse.redirectURI
                let codeVerifier = authorizedResponse.codeVerifier
                let nonce = authorizedResponse.nonce
                let correlationId = authorizedResponse.correlationId // string
                let context = authorizedResponse.context // string
                // Pass these identifiers to your secure server to perform a token request
            case .error:
                // Error is returned identity provider
            case .cancelled:
                // The user cancelled request in ZenKey application.

For example, if your app presents its login flow in a WKWebView, you might use the ZenKey button asset from our Server and Web SDK inside your web content, then a WKNavigationDelegate to detect taps on that button before calling the loginWithZenKey() function in the above sample code.

8.0 Error Handling

AuthorizationError.swift defines the code, description and errorType to help the developer debug the error or present a description to the user. The errorType is of type ErrorType which identifies a class of error during the Authorization flow, such asinvalidRequest or requestDenied. When creating a recovery suggestion or diagnosing an issue, the error's code and description can help provide context and a possible remedy.

The following table summarizes the AuthorizationError error types and potential recovery suggestions for each.

Error Type (Case) Possible Cause How to Remedy
invalidRequest The request made is invalid. Check the parameters passed to the authorization call.
requestDenied The request was denied by the user or carrier. Display an appropriate feedback message to the user.
requestTimeout The request has timed out. Display an appropriate feedback message, such as "Unable to reach the server, please try again" or "Poor network connection."
serverError There was an error on the server. Please try again later.
networkFailure There was a problem communicating over the network. Advise the user to check their connection and try again.
configurationError There is an error configuring the SDK. Check your local code configuration with the configuration on the Service Provider Portal.
discoveryStateError There is an inconsistency with the user's state. Try to perform the authorization request again.
unknownError An unknown error has occurred. If the problem persists, contact support.

8.1 Debugging

To enable logging for debugging, you may pass a .logLevel key to the zenKeyOptions parameter in your ZenKeyAppDelegate. Doing so adds further detail when diagnosing warnings and errors that may occur during the flow of execution.

            didFinishLaunchingWithOptions: launchOptions,
            /// the launchOptions received by your application's app delegate
            zenKeyOptions: BuildInfo.zenKeyOptions
            /// zenKeyOptions: ZenKey specific options. Pass a log level to ZenKey launch options to enable logging during debugging.

For instance, you might use NetworkLog.logLevel = .warn to log network messages. Other logging options include verbose, debug, info, error, and fatal.

public struct Log {
    static private(set) var logLevel: Level = .off

    public enum Level: Int {
        case off, error, warn, info, verbose

        var name: String {
            switch self {
            case .off:
                return ""
            case .warn:
                return "warn"
            case .error:
                return "error"
            case .info:
                return "info"
            case .verbose:
                return "verbose"

    static func configureLogger(level: Level) {
        logLevel = level

9.0 Token Request

With the user’s consent in the form of an authorization code, your secure server will request an access token from the token_endpoint discovered earlier. Information on setting up your secure server can be found in our Server and Web Integration Guide.

To configure this request:

  1. Base64 encode your client_id and client_secret:

Note: When you retrieve your client_secret from the ZenKey Service Provider Portal, for security purposes we ask that you only store your client_secret on your secure backend and allow as few trusted personnel as possible access to it.

  1. Insert this encoded value in your Authorization header:

         'Authorization': "Basic {encoded_value_here}"
  2. Specify the Content-Type as URL-encoded:

         Content-Type: application/x-www-form-urlencoded
  3. Then include the following parameters in the body of your token request:


Here are all the components working together:

POST /token HTTP/1.1
Authorization: Basic {encoded_value_here}
Content-Type: application/x-www-form-urlencoded


9.1 Token Response

In addition to an Access Token, the token response will return an ID Token which includes the sub — an identifier that uniquely pairs a single user with a particular client_id. When you receive the sub, you will store this unique ID in your user database for reference. Do not transmit this sub to your client app.

Sample Token Response:

HTTP/1.1 200 OK
Content-type: application/json
Cache-control: no-cache

    "access_token": "....",
    "token_type": "Bearer",
    "refresh_token": "8xLOxBtZp8",
    "expires_in": 3600,
    "id_token": "..."

9.2 Decode Your ID Token

The ID Token will need to be decoded. Once you decode the JWT payload, you will see it contains claims about the authentication of your end user. As a best practice, you should always validate the ID Token after you have decoded it. Specifically, you should verify the signature and claims contained within the ID Token. For more information about each parameter, refer to our Server and Web Integration Guide.

Sample Decoded ID Token:


    "sub": "mccmnc-client0001",  
    "aud": "ccid-sp00001",
    "nonce": "n-0S6_WzA2Mj",
    "exp": 1311281970,
    "iat": 1311280970,
    "auth_time": 1311280969,
    "acr": "a3",

10.0 Request User Info

After you exchange the authorization code for an authorization token on your secure server, you will be able to access the ZenKey userinfo_endpoint, which will pass information through your server's authenticated endpoints as defined by your application. Therefore, upon receiving an Access Token you may then request user info by issuing a GET request from your backend to the userinfo_endpoint discovered earlier.

    GET /userinfo HTTP/1.1
    Authorization: bearer {ACCESS_TOKEN}

The response will contain JSON with the user information requested by your scopes and approved by the user.

    "sub": "mccmnc-123456789",  
    "name": "Jane Doe",  
    "given_name": "Jane",  
    "family_name": "Doe",  
    "email": "",  
    "postal_code": "90210-3456",  
    "phone_number": "+13101234567",  

11.0 Account Migration

Note: Support for account migration is currently unavailable. The following section is for informational purposes only.

When users change carriers, ZenKey provides you with the support you need. This section describes how the migration process works and best practices for when a user ports his/her account from one carrier to another. In order to see how account migration works, let us imagine a user with the following traits:

  • phone=1234567890
  • mccmnc=310010
  • sub= 310010-{carrierid}

Note: The mccmnc variable is a concatenation of Mobile Country Code (MCC) and Mobile Network Code (MNC). This six-digit number resides on every SIM card. The sub variable is a pairwise identifier that ties a particular user to a particular client_id.

With this in mind, imagine:

  1. The user above visits your app associated with client_id=ccid-SP0001.

  2. Your app with client_id=ccid-SP0001 detects the user as SUB=001001-B and stores/federates the user as verify=001001-B.

11.1 Migration Flow

  1. Now the user migrates phone number 1234567890 from Mobile Network Operator #1 (MNO1) to Mobile Network Operator #2 (MNO2), keeping their device but changing SIM and mccmnc.

  2. The user migrates quickly (e.g., within 15 minutes), so when they try to use your app with client_id=ccid-SP0001, you trigger a login prompt with the phone number.

  3. The SDK in your app submits a discovery request with the new mccmnc to retrieve the OpenID configuration for MNO2.

  4. The SDK in your app constructs the authentication URL, opening in the device browser to MNO2's web authentication endpoint.

  5. MNO2 sees the mobile user agent and posts a banner encouraging the user to download the ZenKey app.

  6. The user downloads, installs, and launches the ZenKey application for MNO2 .

  7. MNO2 notices that the user is not yet registered for the new phone number and: a. Asks: “Would you like to register a new ZenKey carrier account?” or “Would you like to PORT your prior ZenKey carrier account from your previous MNO?” b. Upon seeing that the device has a recently migrated phone line, offers a migration option.

  8. The user selects the option to migrate the existing ZenKey carrier account and is redirected in a WebView to MNO1’s authentication endpoint. (Note: port_data is a scope reserved for carriers only. It informs the carrier to render to the user a confirmation for porting the user identity account to a new carrier. Only carriers using ZenKey may access this scope.) Because the MNO1 authentication request contains the port_data scope, MNO1 knows the authentication is for migration.

  9. MNO1 completes the user authentication before returning the user to MNO2: a. MNO1 may require the user to perform multiple recovery methods because EAP-AKA (SIM) authentication will not work for the user. b. MNO1 signs a port token for each Service Provider using the slow rotating key present in the OpenID configuration reference.

  10. The user completes MNO2 account setup steps: a. MNO2 prepopulates the registration page with the previous name, email, address, etc. b. The user chooses a new PIN. c. MNO2 stores port tokens for each of the previous Service Providers used at MNO1.

  11. MNO2 asks if the user wants to port his/her previously-defined consents for each service provider application, and then returns the user to the Service Provider application with an authentication code.

  12. MNO2 returns an access_token and id_token from the token_endpoint. The access_token is the bearer token when making requests to the userinfo_endpoint. The id_token is a JSON Web Token (JWT) that contains claims about the authentication of an end user by an authorization server, such as sub=mncmcc-002002-Z.

  13. But your backend does not recognize this suband so uses the ISS (i.e. discovery endpoint) to access MNO1’s port_token signing key and verify the signature of the port_token. The port_token is always encoded. However once decoded, its contents will contain the new sub:

        "iss": "",
        "sub": "mncmcc-002002-Z",
        "iat": 1516239022,
        "aud": client_id
  1. Your backend updates the user's sub in its database with the new value. For example, mncmcc-001001-B would become mncmcc-002002-Z.

11.2 Migration Best Practices

When you first authenticate a user, the first id_token should contain a single subject claim. Store this as a reference rather than a phone number or email address, since these profile attributes are liable to change. If you receive a new id_token that contains an AKA claim and an unrecognized sub, you should:

  1. Open the AKA port_token.

  2. Verify the port_token issuer is a trusted carrier. (The Service Provider Portal will contain a list of valid iss URLs.)

  3. Use the port_token :iss value to extract the OpenID configuration` of the old MNO.

  4. Use the OpenID configuration to extract the JWKs for the old MNO.

  5. Use the key ID (KID) in the port_token to identify which JWK key to use to verify the token's signature.

  6. If you have recorded a user with the old subject, update the references to the new subject from the new carrier.

Note: Because a user may choose not to port his/her ZenKey account, or to change carriers by getting a new phone number with the new carrier, you should host methods to update the ZenKey references.


For technical questions, contact support.


Copyright © 2020 ZenKey, LLC.

Revision History

Date Published Document Version ZenKey SDK Version Description
5.01.20 0.0.03 1.0.0 Removed Apache Legal information.
3.02.20 0.0.02 1.0.0 Added .last_4_social scope. Added nonce, acrValues, correlationId, and context to Authorization Response.
1.31.20 0.0.01 1.0.0 First published version.

Last Update: Document Version 0.0.03, May 01, 2020