Installation

The iOS SDK is written in Swift. It is a lightweight XCFramework with a download size of 2.5 MB and an install size of 1.5 MB for the minimum required components. It only relies on iOS native APIs; no external dependencies are included.

Prove manages a repository with the libraries to enable integration.

The following needs to be followed to remotely import CocoaPod from the Prove pod repository:

# Run this command to install the cocoapods-art plugin (authored by Artifactory)
gem install cocoapods-art

# Run this command to add the Prove pod repository
pod repo-art add prove.jfrog.io https://prove.jfrog.io/artifactory/api/pods/libs-public-cocoapods

# In your Podfile, paste in the Prove pod repository as a source
plugin 'cocoapods-art', :sources => [  
    'prove.jfrog.io'  
]

# In your Podfile, paste in the SDK pods
pod 'ProveAuth', '6.1.0'
pod 'ProveBase', '3.1.3'
pod 'ProveDeviceAuth', '3.1.2'
pod 'ProveMobileAuth', '2.9.2'

# Run this command to install the SDK pods
pod install

Permissions

No additional permissions are required to use the SDK.

Send the Type of Flow: Mobile

Unlike the Web SDK, when using the iOS SDK, the mobile flow must be used. You should pass mobile to the Start() function on the server. In a mobile flow, Mobile Auth is attempted first and if that fails, it will perform OTP validation on the mobile phone.

In the mobile flow, once either Mobile Auth or the OTP validation is complete, the AuthFinishStep function will be called.

Authenticate()

The SDK requires an authToken as a parameter for the Authenticate() function. This token is returned from the Start() call of the server SDK. The token is session specific so it can only be used for a single flow so it cannot be reused. It also expires after 15 minutes.

Retrieve authToken

To start the flow, you'll need to send a request to your backend server with the phone number, flow type, and an optional challenge of either the date of birth (format: YYYY-MM-DD) or social security number (format: last 4 digits).

// The below example uses native iOS URLSession, but any other  
// alternative networking approaches should also work  
func initialize(phoneNumber: String, ssn: String, flowType: String, completion: @escaping (Result\<String, Error>) -> Void) {  
    guard let url = URL(string: "\(backendUrl)/initialize") else {  
        completion(.failure(URLError(.badURL)))  
        return  
    }  
    var request = URLRequest(url: url)  
    request.httpMethod = "POST"  
    request.addValue("application/json", forHTTPHeaderField: "Accept")  
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")  
    // Set up the request body  
    let body: [String: Any] = [  
        "phoneNumber": phoneNumber,  
        "flowType": flowType,  
        "ssn": ssn  
    ]  
    do {  
        request.httpBody = try JSONSerialization.data(withJSONObject: body, options: \[])  
    } catch {  
        completion(.failure(error))  
        return  
    }  
    // Perform the request  
    let task = URLSession.shared.dataTask(with: request) { \_, response, error in  
        // Handle network or connection errors  
        if let error = error {  
            completion(.failure(error))  
            return  
        }  
        do {  
            if let json = try JSONSerialization.jsonObject(with: data, options: \[]) as? [String: Any],  
               let authToken = json["authToken"] as? String {  
                completion(.success(authToken))  
            } else {  
                let parsingError = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Failed to parse JSON or authToken is missing"])  
                completion(.failure(parsingError))  
            }  
        } catch {  
            completion(.failure(error))  
        }  
    }  
    // Start the network call  
    task.resume()  
}

Setup Authenticator

Once you have the authToken, build the authenticator for the mobile flow.

// Object implementing ProveAuthFinishStep protocols  
let finishStep = FinishAuthStep()

// Objects implementing OtpStartStep/OtpFinishStep protocols  
let otpStartStep = MobileOtpStartStep()  
let otpFinishStep = MobileOtpFinishStep()

let proveAuthSdk: ProveAuth  
proveAuthSdk = ProveAuth.builder(authFinish: finishStep)  
  .withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep)  
  .build()

Cellular data connection might not always be available during testing. The Builder class offers a method, which permits simulated successful session results while connected to Wi-Fi network only (without a cellular data connection available). This feature is particularly useful for testing on the Emulator. This can only be used in Sandbox mode. It will allow Mobile Auth℠ to be simulated successfully and won’t fallback to OTP.

proveAuthSdk = ProveAuth.builder(authFinish: finishStep)  
  .withMobileAuthTestMode() // Test mode flag  
  .build()

Performing the Authentication

The ProveAuth object is thread-safe and can be used as a singleton. Most Prove Auth methods are blocking and therefore cannot be performed in the main application thread. The application is required to manage threads, for instance, with an executor service, which provides at least two threads, since the SDK may have more then one simultaneous blocking requests.

// authToken retrieved from your server via StartAuthRequest  
proveAuthSdk.authenticate(authToken) { error in  
  DispatchQueue.main.async {  
    self.messages.finalResultMessage = "ProveAuth.authenticate returned error: \(error.localizedDescription)"  
    print(self.messages.finalResultMessage)  
  }  
}

Validate the Mobile Phone

In the AuthFinishStep, you'll specify a function to call once the possession checks are complete on the mobile phone. In the code below, we are referencing an endpoint called /verify and this is not a Prove endpoint, this should be a new endpoint on your backend server that should call the Validate() function to check if the phone number was validated. If it was successful, the server should then call the Challenge() function and then return the results that will include user information. If it was not successful, the server won't return user information. We've included a few example fields that should be returned and then pre-filled on a form for the user to verify.

// Send a verify request to get return user information.  
// The below example uses native iOS URLSession, but any other  
// alternative networking approaches should also work  
func verify(String: authId, completion: @escaping (Error?) -> Void) {  
    guard let url = URL(string: "\(backendUrl)/verify") else {  
        completion(URLError(.badURL))  
        return  
    }  
    // Create the request  
    var request = URLRequest(url: url)  
    request.httpMethod = "POST"  
    request.addValue("application/json", forHTTPHeaderField: "Accept")  
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")  
    // Set up the request body (empty JSON object) - authId is not needed so you can ignore.
    let body: [String: Any] = ["authId": authId]  
    do {  
        request.httpBody = try JSONSerialization.data(withJSONObject: body, options: \[])  
    } catch {  
        completion(error)  
        return  
    }  
    // Perform the request  
    let task = URLSession.shared.dataTask(with: request) { \_, response, error in  
        if let error = error {  
            completion(error)  
            return  
        }  
        // Parse the response  
        do {  
            if let json = try JSONSerialization.jsonObject(with: data, options: \[]) as? [String: Any],  
               let firstName = json["firstName"] as? String,  
               let lastName = json["lastName"] as? String {  
                // Update the UI on the main thread  
                DispatchQueue.main.async {  
                    // Update your UI with the pre-fill user information  
                    // For example, assuming you have IBOutlet references for your text fields  
                    self.firstNameInput.text = firstName  
                    self.lastNameInput.text = lastName  
                }  
                completion(nil)  
            } else {  
                let parsingError = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to parse JSON])  
                completion(parsingError)  
            }  
        } catch {  
            completion(error)  
        }  
    }  
    // Start the network call  
    task.resume()  
}

OTP Configuration

There are two functions to implement for the OTP handling - a start and a finish step.

In order to set the OTP handlers, OtpStartStep and OtpFinishStep interfaces should be implemented. The Swift snippet has an example.

OtpStartStep example:

import Foundation
import ProveAuth
import SwiftUI

// Implementation of OtpStartStep protocol
class MobileOtpStartStep: OtpStartStep {
    @ObservedObject var sheetObservable: SheetObservable
    var callback: OtpStartStepCallback?

    init(sheetObservable: SheetObservable){
        self.sheetObservable = sheetObservable
    }

    // ProveAuth SDK calls this method when it is time to collect an end user's phone number for SMS OTP and/or obtain
    // the user's confirmation to initiate the SMS message to the phone number provided
    // 'phoneNumberNeeded' argument indicates whether mobile app needs to collect the phone number
    func execute(phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback) {
        // here - signal to ContentView to display OtpStartView
        DispatchQueue.main.async {
            // SDK returns phoneValidationError if the phone number provided is invalid or blocked
            if case .phoneNumberValidationError = phoneValidationError {
                self.sheetObservable.isPhoneValidationError = true
            } else {
                self.sheetObservable.isPhoneValidationError = false
            }
            self.sheetObservable.isOtpStartActive = true
            self.sheetObservable.isPhoneNumberNeeded = phoneNumberNeeded
            self.callback = callback
        }
    }

    // Here - return collected phone number string to SDK
    func handlePhoneNumber(phoneNumber: String? = nil) {
        guard let callback = self.callback else {
            print("Error: OtpStartStepCallback is not set ")
            return
        }
        if let phoneNumber = phoneNumber {
            let otpStartInput = OtpStartInput(phoneNumber: phoneNumber)
            // This is how you pass collected phone number to SDK
            callback.onSuccess(input: otpStartInput)
            return
        }
        // If phoneNumberNeeded = false (in the OtpStartStep.execute) and phone number was already provided in the initial server-to-server call to /start endpoint
        // Then return nil back to SDK, Prove Auth server already got the phone number
        // calling onSuccess here indicates that the user's confirmation to send SMS was obtained
        callback.onSuccess(input: nil)
    }

    // Here - communicate to the SDK any issues while trying to obtain the phone number.
    // Error should be reported if the user explicitly cancels the SMS OTP transaction or presses the back button to exit out the SMS OTP start step screen.
    func handleOtpStartError() {
        guard let callback = self.callback else {
            print("Error: OtpStartStepCallback is not set ")
            return
        }
        callback.onError()
    }
}

OtpFinishStep example:

// Copyright 2022 Prove. All rights reserved.
// Unauthorized copying or excerpting via any medium is strictly prohibited.
// Proprietary and confidential.

import Foundation
import SwiftUI
import ProveAuth

// Implementation of OtpFinishStep protocol
class MobileOtpFinishStep: OtpFinishStep {
    @ObservedObject var sheetObservable: SheetObservable
    var callback: OtpFinishStepCallback?

    init(sheetObservable: SheetObservable){
        self.sheetObservable = sheetObservable
    }
    // SDK calls this method when it is time to collect OTP value delivered in the SMS message
    // otpError will be returned if OTP value was incorrect and the user allowed to re-enter the corrected OTP
    func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) {
        self.callback = callback
        // here - signal to ContentView to display OtpFinishView
        DispatchQueue.main.async {
            if case .otpValidationError = otpError {
                self.sheetObservable.isOtpValidationError = true
            }
            self.sheetObservable.isOtpFinishActive = true
        }
    }

    // Here - return the OTP value to the SDK
    func handleOtp(_ otp: String) {
        guard let callback = self.callback else {
            print("Error: OtpFinishStepCallback is not set ")
            return
        }
        let otpFinishInput = OtpFinishInput(otp: otp)
        callback.onSuccess(input: otpFinishInput)
    }

    // Here - communicate to the SDK any issues while trying to obtain the OTP value.
    // Error should be reported if the user explicitly cancels the SMS OTP transaction or presses the back button to exit out the SMS OTP finish step screen.
    func handleOtpFinishError() {
        guard let callback = self.callback else {
            print("Error: OtpFinishStepCallback is not set ")
            return
        }
        callback.onError()
    }

    // Here - request a new SMS OTP message. The Prove Auth SDK then will initiate a new OtpStartStep.execute call
    // to allow the mobile app to restart the phone number collection logic.
    // Prove Auth allows for up to 3 OTPs to be sent during the same authentication session.
    func handleOtpResend() {
        guard let callback = self.callback else {
            print("Error: OtpFinishStepCallback is not set ")
            return
        }
        callback.onOtpResend()
    }
}

Verify the User Information

Once the user has made any edits to their pre-fill information, you should submit that information to the backend server so the Complete() call can then verify the user information.

// Send request to the backend to verify user information.  
// The below example uses native iOS URLSession, but any other  
// alternative networking approaches should also work  
func sendInfo(firstName: String, lastName: String, completion: @escaping (Result\<[String:Any], Error>) -> Void) {  
    guard let url = URL(string: "\(backendUrl)/finish") else {  
        completion(.failure(URLError(.badURL)))  
        return  
    }  
    var request = URLRequest(url: url)  
    request.httpMethod = "POST"  
    request.addValue("application/json", forHTTPHeaderField: "Accept")  
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")  
    // Set up the request body  
    let body: [String: Any] = [  
        "firstName": firstName,  
        "lastName": lastName  
    ]  
    do {  
        request.httpBody = try JSONSerialization.data(withJSONObject: body, options: \[])  
    } catch {  
        completion(.failure(error))  
        return  
    }  
    let task = URLSession.shared.dataTask(with: request) { \_, response, error in  
        // Handle network or connection errors  
        if let error = error {  
            completion(.failure(error))  
            return  
        }  
        do {  
            if let results = try JSONSerialization.jsonObject(with: data, options: \[]) as? [String: Any] {  
                completion(.success(results))  
            } else {  
                let parsingError = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Failed to parse JSON or authToken is missing"])  
                completion(.failure(parsingError))  
            }  
        } catch {  
            completion(.failure(error))  
        }  
    }  
    // Start the network call  
    task.resume()  
}