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 Service Providers (SPs) 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

A user establishes a mobile device as their primary device by installing the carrier specific ZenKey app. After completing a simple initial setup, they are ready to use ZenKey with third-party applications. Pressing the ZenKey button in a third party app or website from their primary device starts the authentication process.

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:   A user's Service Provider's mobile app or website makes an authorization code request to the local ZenKey app.

Step 2:   The user's ZenKey app determines the appropriate wireless carrier to perform SIM and user authentication with. The carrier returns an authorization code via the SP's Redirect URI (see Section 4.0: Configure Client ID and Redirect URI).

Step 3:   Because the user has consented to share information, the SP's backend server makes a token request for user info or 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 TV. 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 user is taken to a website where they can select the appropriate carrier. This is known as the carrier Discovery UI website, and is where the user chooses the carrier associated with their primary device. 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 website gets redirected to perform authorization with a login_hint_token.

Step 4:   To perform SIM and user authentication, the Service Provider's backend server makes an authorization code request to the appropriate carrier, and receives the auth code in its Redirect URI.

Step 5:   Because the user has consented to share information, the Service Provider's 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 a Service Provider 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:

  • Login 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, ZenKey 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. By default, Redirect URIs issued by ZenKey will resemble {client_id}://com.xci.provider.sdk.

  • Identify which scopes, or user attributes, to capture when a user authorizes your app. During enrollment, users provide ZenKey with basic personal information (e.g. name, email, address). When they 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. But if you do include them, ZenKey will always encrypt, yet never store, the personal data of your users.

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.
  4. View your project's "Embedded Binaries" under your project'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 Client ID and Redirect URI

Now that you have installed the ZenKey SDK, you must add your ZenKey client_id to your iOS application's Info.plist. To do so, copy your client_id from the ZenKey Service Provider Portal dashboard.

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

    <key>ZenKeyClientId</key>
    <string>{your application's client id}</string>

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

    <key>CFBundleURLTypes</key>     

After adding this array, Xcode will create two keys, Type 0 and CFBundleURLName. 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. Once you have done this, add your ZenKey client_id a second time, also as a string, under CFBundleURLSchemes:

    <key>CFBundleURLSchemes</key>     
    <array>
    <string>{your application's client id}</string>
    </array>

With the steps above complete, your Info.plist should resemble the sample below:

Plist Example

4.1 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. You can use the default, pre-configured URI or create a custom redirect URI.

The default URI is {your client Id}://com.xci.provider.sdk. Use this URI by adding your client_id to your Info.plist as a Custom Scheme. To create a custom redirect URI, access the Service Provider Portal and follow the instructions.

NOTE: For security, 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 on the topic.

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

    <key>ZenKeyCustomHost</key>
    <string>{your universal link's host}</string>
    <key>ZenKeyCustomPath</key>
    <string>{your universal link's full path}</string>
    <key>ZenKeyCustomScheme</key>
    <string>https</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 {

        ZenKeyAppDelegate.shared.application(
            application,
            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
    }
}

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

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 ZenKeyAuthorizationButton 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 ZenKeyAuthorizationButton and add it to your UIView. Set a delegate to handle authorization.

import ZenKeySDK

class LoginViewController {
    let zenKeyButton = ZenKeyAuthorizationButton()

    override func viewDidLoad() {
        super.viewDidLoad()

        let scopes: [Scope] = [.openid, .email, .name, .phone, .postalCode]
        // Besides .openid, all scopes are optional. Please include only those scopes you truly need.

        zenKeyButton.scopes = scopes
        zenKeyButton.delegate = self

        view.addSubview(zenKeyButton)
    }
}

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:

    zenKeyButton.style = .light

The light button style looks like this:

Verify Button Light

6.1.3 Custom Button or View

Instead of the default ZenKeyAuthorizationButton, 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.mcc)\(authorizedResponse.mnc)"
            let redirectURI: authorizedResponse.redirectURI
            let codeVerifier = authorizedResponse.codeVerifier
            // 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]

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.

  • correlation_id - This is a tracking ID used for transaction logging. When you pass a correlation_id, it will be added to the carrier logs. To access log entries, you will need to use the ZenKey Service Provider Portal. As a rule of thumb, use the same correlation_id for code, token, and userinfo requests.

  • context - While context isn't 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 (i.e., an 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 SP Server. The SP app gets the codeVerifier string from the AuthorizedResponse and securely transmits it to their 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 all Service Providers must be able to:

  1. Receive the codeVerifier value in the AuthorizedResponse;

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

  3. Configure their 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, SPs 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]
        authService.authorize(
            scopes: scopes,
            fromViewController: self) { result in

            switch result {
            case .code(let authorizedResponse):
                let code = authorizedResponse.code
                let mcc = authorizedResponse.mcc
                let mnc = authorizedResponse.mnc
                let codeVerifier = authorizedResponse.codeVerifier
                // 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.
            }
        }
    }
}

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.


ZenKeyAppDelegate.shared.application(
            application,
            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

Before issuing a token request, your application and the ZenKey SDK will re-perform discovery and use the discovered token endpoint to request an access token from ZenKey with the processes already detailed:

  • Auth Code
  • MCC (Mobile Country Code)
  • MNC (Mobile Network Code)
  • Redirect URI

As a result of the discovery call made from the ZenKey SDK, your backend server will receive the MCC/MNC and use it to issue a token request. The token received should serve as the basis for accessing or creating a token within the domain of your application. 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.

Information on setting up your secure server can be found in the ZenKey Server and Web Integration Guide.

10.0 Account Migration

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. But to see how account migration works, let us first imagine a user with these traits:

  • phone=1234567890.

  • mccmnc=310010.

  • sub= 001001-{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. As for sub, this variable is a pairwise identifier that ties a particular user to a particular SP's client_id.

With this in mind, now imagine:

  1. The user above visits a Service Provider (SP) with client_id=ccid-SP0001.

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

10.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 the SP client_id=ccid-SP0001, that SP triggers a login prompt with the phone number.

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

  4. The SP SDK 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 CCID 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 SP 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 of the SPs, and then returns the user to the Service Provider application with an authentication code.

  12. The Service Provider issues an access_token and id_token. 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 the SP 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": "https://zenkey.oldmno.com",
        "sub": "mncmcc-002002-Z",
        "iat": 1516239022,
        "aud": client_id
    }
  1. The Service Provider updates the user's sub in its database with the new value. For example, mncmcc-001001-B would become mncmcc-002002-Z.

10.2 Migration Best Practices

When an Service Provider (SP) first authenticates a user, the first id_token should contain a single subject claim. The SP should store this as a reference rather than a phone number or email address, since these profile attributes are liable to change. If an SP receives a new id_token that contains an AKA claim and an unrecognized sub, the SP 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 the Service Provider has a 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, the Service Provider should host methods to update the CCID references.

Support

For technical questions, contact support.

License

Copyright © 2019 ZenKey, LLC.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Revision History

Date Version Description
1.8.2020 0.9.21 Removed Mobile Authentication Taskforce (1.0). Updated OpenID Connect content (1.1). Improved Section 4.0 instructions. Created new Info.plist image. Improved ZenKeyAuthorizationButton (6.1) section. Updated code block (6.1 and 6.2).
12.19.2019 0.9.20 Added section about WebView (6.1.3).
12.17.2019 0.9.19 Deleted ZenKeyButtonView (6.1.4) and all carrier endorsement text. Reverted to ZenKeyAuthorizationButton (6.0, 6.1, 6.2).
12.10.2019 0.9.18 Edited ACR Values in Section 6.3.2. Corrected Token Request details in Section 9.0.
12.04.2019 0.9.17 Deleted License Notice. Changed XCI JV, LLC to ZenKey, LLC.
12.04.2019 0.9.16 Removed header logo; updated CocoaPod instructions (Section 3); added Info.plist image (Section 4), ButtonView instructions, notes on .openid scope, Carrier Endorsement details (Section 6.1.4), and minor edits; switched order of Sections 9 and 10.
11.27.2019 0.9.15 Updated multiple sections.
10.4.2019 0.9.14 Added Migrating Accounts section
9.17.2019 0.9.13 Updated license with Apache 2.0 text.
9.9.2019 0.9.12 Added minor edits.
8.29.2019 0.9.11 Updating verbiage and instructions
8.27.2019 0.9.10 Updated high-level flows; Updated sample code.
8.20.2019 0.9.9 Added section numbers; Added revision history; Added additional info about Redirect URIs to section 4.0

Last Update: Document Version 0.9.21 January 9, 2020