Skip to content

Margels/ProveDemoApp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 

Repository files navigation

Implement Passive Authentication with Prove Unify on your iOS Project using Swift

Introduction

Passive authentication refers to a method of signing in users in a seamless way, without requiring them to actively provide information to register their identity on your server. These authentication tasks are performed by the app in the background in a quick way, boosting user engagement and decreasing chances of appa abandonment - which is often an issue with apps that involve a complicated, multi-step signing up and login process.

The most obvious advantage of this approach is the improvement of user experience with your app and the time saved off password filling or performing extra steps just to log in. This type of identity verification often involves techniques that require little to no input from the user, who are no longer required to actively create and type their credentials to sign up.

While the most immediate advantage of this method of user verification is how much less time consuming it can be on the user side, it is also worth noting that it is more secure than using mere email-password credentials, which can be predictable, phished or leaked. Instead, passive authentication ensures the users' seecurity all along by performing risk evaluations and using advanced methods such as device intelligence or network signals.

It also provides an incomparable level of accuracy. While textual credentials can contain typos or even be forgotten over time, most methods of passive authentication verify the user without them having to prove who they are. The sysetem uses hard-to-fake signals, such as SIM swap detection or IP address reputation, and collects them during the app's natural use to analyze them over time and ddetect the user's identity.

In summary, as opposed to traditional authentication methods, passive authentication is a valuable tool for any mobile app in terms of speed and user experience, reliability, and also security.

In this article, you will learn how to implement Prove Unify in your iOS Project using Swift language.

Passive authentication with Prove Unify

Prove Unify is a tool that provides both active and passive authentication methods available to be integrated directly in your mobile app. Using its built-in SDK, Prove will first try to perform passive authentication, and, when not successful, it will fall back to verification through methods such as OTP. Once the user is verified, Prove will securely create a cryptographic key and store it on the user's device to ensure seamless logins in the future.

From a developer perspective, this type of approach has a number of benefits, for example:

  • it minimises login and signup effort for users with low risk
  • it automatically steps up to a higher security level type of authentication when the risk is higher
  • devices are binded so future logins will happen faster and smoothly

While Prove offers advanced techniques of passive authentication, such ad device intelligence and behavioural biometrics, it is still best used only as a first layer in the context of a defense-in-depth strategy in your project. This means that, while it grants enough security for low risk operations, you may want to involve more than just one authentication step within the course of your app lifecycle if there are higher risk scenarios involved. For example, to perform operations such as bank transfers or updating personal, sensitive information, stepping up to active measures of authentication before performing such tasks would add an extra layer of security for your users. This will require them to step in and actively perform authentication only in the context of high risk operations being involved, giving them a sense of security while still maintaining user engagement and making authentication low-effort.

Implementing Prove Unify on iOS

Prove has made it easy to implement their SDK on any project. While it is recommended to implement it both on your server side and frontend side, for the scope of this tutorial, you will walk through all the steps required to integrate Prove SDK in your frontend project, specifically, on iOS.

Prerequisites

Before getting to the code, you will need to sign up on Prove and fill in every field required. You should be redirected to your developer portal.

It should look like this:

developer portal on prove

On the left panel, click on "Projects" and create a new project named ProveAuthDemo and select Prove Unify as Prove solution. Click on Create Project and open the newly created project to see the its details. You will find the following information:

  • client ID,
  • client secret,
  • project ID.

You will these pieces of information to interact with Prove API. You can find them both in the Quick start tab or in the Credentials tab in your Developer portal.

credentials to access prove api

These credentials are on Sandbox environment, which is the automatic environment for Prove SDK to test its features without using real users, devices or phone numbers. You will use them throughout this tutorial for testing purposes.

Run the following cURL code in your terminal:

      curl --location 'https://platform.uat.proveapis.com/token' \
      --header 'Content-Type: application/x-www-form-urlencoded' \
      --data-urlencode 'client_id=<YOUR_CLIENT_ID>' \
      --data-urlencode 'client_secret=<YOUR_CLIENT_SECRET>' \
      --data-urlencode 'grant_type=client_credentials'

Replace the placeholders with your credentials from the Prove Developer Portal and hit Enter.

Your result should be similar to this:

curl code to test credentials

With the Prove Developer Portal running and working, your XCode and a physical device to run your tests, you're ready to start coding.

Adding Prove to your project

Following Prove's implementation guide for the iOS SDK, you will need to use the plugin cocoapods-art to access Prove's repository on Artifactory.

In order to do that, open your terminal and first run this command to install cocoapods-art:

gem install cocoapods-art

Once that's done, add the path to the directory of your project using command cd .../ProveAuthDemo and use the following command to add the repository to your project:

pod repo-art add prove.jfrog.io https://prove.jfrog.io/artifactory/api/pods/libs-public-cocoapods

Once that's done, open your Podfile (or create one using the command pod init) and make sure to add the plugin and the pod:

plugin 'cocoapods-art', :sources => ['prove.jfrog.io']

platform :ios, '13.0'
use_frameworks!

target 'ProveAppDemo' do
  pod 'ProveAuth', '~> 6.5.0'
end

In this Podfile, you added:

  • the reference to the source of Prove on Jfrog: plugin 'cocoapods-art', :sources => ['prove.jfrog.io']
  • the pod to install along with the correct, updated version: pod 'ProveAuth', '~> 6.5.0'

Now run pod install and, if you did everything correctly, you should be able to import ProveAuth in your ViewController.

Integrating Prove Unify

The Prove Unify flow goes through three main phases, which can be summarised as follows:

  • Backend: Calls /v3/unify with phone number and possession type
  • Client: Receives authToken and uses SDK to authenticate
  • Backend: Calls /v3/unify-status using correlationId to get the final result

Step 1: Getting the bearer token

In order to access Prove API, you will need the token you previously generated from the terminal. Add this model to your project to decode the response and save the bearer token:

struct OAuthTokenResponse: Codable {
    let access_token: String
    let token_type: String
    let expires_in: Int
}

Then create two variables named clientId and clientSecret where you store your Sandbox credentials as Strings for testing purposes.

You can then create the following function, using the variables and the model you just created, to retrieve the bearer token using Prove's /token endpoint:

    // MARK: - Get OAuth2 Bearer Token
    func getBearerToken(
        completion: @escaping (Result<String, Error>) -> Void
    ) {
        
        // Validate URL and return error upon failure
        guard let url = URL(string: "https://platform.uat.proveapis.com/token") else {
            completion(.failure(NSError(domain: "InvalidURL", code: 0)))
            return
        }
        
        // Set up request
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        
        // Set up body params
        let bodyParams = "grant_type=client_credentials&client_id=\(clientId)&client_secret=\(clientSecret)"
        request.httpBody = bodyParams.data(using: .utf8)
        
        // Start data task
        URLSession.shared.dataTask(with: request) { data, response, error in
            
            // If data task fails, return failure
            if let error = error {
                completion(.failure(error))
                return
            }
            
            // If data is absent, return failure
            guard let data = data else {
                completion(.failure(NSError(domain: "NoData", code: 0)))
                return
            }
            
            // Parse response
            do {
                let tokenResponse = try JSONDecoder().decode(OAuthTokenResponse.self, from: data)
                completion(.success(tokenResponse.access_token))
            } catch {
                completion(.failure(error))
            }
            
        }.resume()
    }

You will now be able to collect the bearer authorization token and use it to access Prove's API.

Step 2: Unify

In order to perform user authentication, you will need a phone number. Prove provides Sandbox test users so you can freely test Prove SDK without having to use real users.

Use this function to perform a POST request to the /v3/unify endpoint:

    // MARK: - Call /v3/unify
    func startUnifyFlow(
        bearerToken: String,
        phoneNumber: String,
        completion: @escaping (Result<(authToken: String, correlationId: String), Error>) -> Void
    ) {
        
        // Validate URL and return error upon failure
        guard let url = URL(string: "https://platform.uat.proveapis.com/v3/unify") else {
            completion(.failure(NSError(domain: "InvalidURL", code: 0)))
            return
        }
        
        // Create request
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(bearerToken)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        // Set up body params
        let bodyDict: [String: Any] = [
            "phoneNumber": phoneNumber,
            "flowType": "mobile",
            "possessionType": "mobile",
            "clientRequestId": UUID().uuidString
        ]

        // Transform body params to JSON
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: bodyDict)
        } catch {
            completion(.failure(error))
            return
        }

        // Start data task
        URLSession.shared.dataTask(with: request) { data, response, error in
            
            // If data task fails, return failure
            if let error = error {
                completion(.failure(error))
                return
            }
            
            // If data is absent, return failure
            guard let data = data else {
                completion(.failure(NSError(domain: "NoData", code: 0)))
                return
            }

            // Parse response
            do {
                if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
                   let authToken = json["authToken"] as? String,
                   let correlationId = json["correlationId"] as? String {
                    completion(.success((authToken, correlationId)))
                } else {
                    completion(.failure(NSError(domain: "MissingFields", code: 0)))
                }
            } catch {
                completion(.failure(error))
            }
        }.resume()
    }

With this mehod, you will return a correlationId which you will use to verify the status of the authentication request.

Create a method that incorporates the bearer token function and the unify call so it's easier to perform anywhere else in your project:

    // MARK: - Unify chain calls
    func unify(phoneNumber: String, completion: @escaping (Result<(authToken: String, correlationId: String), Error>) -> Void) {
        getBearerToken { result in
            switch result {
            case .success(let bearerToken):
                self.startUnifyFlow(bearerToken: bearerToken, phoneNumber: phoneNumber, completion: completion)
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }

You are now ready to start the Unify flow.

Step 3: Initialising the SDK

Once you have your authorization token, you are ready to start the SDK.

Head over to your ViewController and import ProveAuth to access all of Prove SDK's public methods.

Create the following variable to save an instance of ProveAuth to initialize:

    private var proveAuthInstance: ProveAuth?

You will need some interactive elements on your screen to be able to test the authentication process. Create a simple UI that involves:

  • a text field where the user can enter their phone number,
  • a button where the user can click to start authentication,
  • a label that updates the user on the status of the process.

Here is an example using the storyboard:

    @IBOutlet var phoneNumberTextField: UITextField!
    @IBOutlet var authButton: UIButton!
    @IBOutlet var resultLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        authButton.setTitle("Start Authentication", for: .normal)
        self.updateResultLabel(with: "")
    }
    
    @IBAction func authButtonTapped(_ sender: Any) {
	    // ...
    }

    private func updateResultLabel(with string: String) {
        DispatchQueue.main.async { self.resultLabel.text = string }
    }
    
    private lazy var onResult: (String) -> Void = { [weak self] message in
        guard let self = self else { return }
        self.updateResultLabel(with: message)
    }

With this code above, you created a simple UI for an authentication process and updated the text of the text field and the label to inform the user of eeither a successful or failure outcome.

Create a method called startUnify() which you will use in authButtonTapped to begin the Unify process. You will nee a phone number, so the first thing to do is make sure the user added it to the text field before pressing the button:

        guard let phoneNumber = phoneNumberTextField.text,
              !phoneNumber.isEmpty,
              phoneNumber.count > 9
        else {
            self.updateResultLabel(with: "Please enter a valid phone number.")
            return
        }

Using this guard statement, you are stopping the user from inserting a phone number too short, or no text at all. If that happens, the label will display the informative text "Please enter a valid phone number".

If the user passes this initial check, the authentication process can begin. You can update the label's text to "Authenticating...".

You are now ready to perform the unify API calls. In this tutorial, the backend functions are stored in a class named DataService and accessible usign a shared singleton, so the function can be used like this:

        DataService.shared.unify(
            phoneNumber: phoneNumber
        ) { result in
            
            switch result {
                
            case .success(let result):
                self.updateResultLabel(with: "Starting Prove Unify...")
                // TODO 1
                // TODO 2
            
            case .failure(let error):
                self.updateResultLabel(with: "Error: \(error.localizedDescription)")
            }
 
            
        }

This way, you are updating the result label to:

  • "Error:" + a localized description of the error, if the call fails;
  • "Starting Prove Unify..." if the call succeeds.

You are now ready to initialize the SDK. In order to do that, you will need:

  1. An Authentication Finish Handler, to manage the end of the process;
  2. An OTP Start and Finish Handler, to manage the beginning and the end of an OTP flow (as a fallback in casse of a passive authentication failure).

Start with the Authentication Finish Handler. Create a class named AuthFinishHandler that looks like this:

class AuthFinishHandler: ProveAuthFinishStep {

    func execute(authId: String) {
		// ...
    }
}

Inside that execute method, you will perform the final task of your authentication process.

Now you have to create the OTP handlers. In this tutorial, you will use the objects OtpStartStep and OtpFinishStep to create OTP handlers that create two UIAlertControllers that will allow the user to:

  1. Confirm that they wish to receive the OTP or specify their phone number, and
  2. Insert the OTP they just received for validation.

You can do all this by copy-pasting the following code:

class OtpStartHandler: OtpStartStep {
    
		// Set callback for when user submits or cancels
        var callback: OtpStartStepCallback?
        
        // View controller to present UI from
        private weak var presentingVC: UIViewController?
        
		// Initializer
        init(presentingViewController: UIViewController) {
            self.presentingVC = presentingViewController
        }
        
		// Execute function
        func execute(phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback) {
            self.callback = callback
            
            DispatchQueue.main.async {
                if phoneNumberNeeded {
                    // Requires phone number input
                    self.askForPhoneNumber(validationError: phoneValidationError)
                } else {
                    // Phone number is not needed, confirm that SMS can be sent
                    self.confirmSendSMS()
                }
            }
        }
        
		// Ask Phone Number
        private func askForPhoneNumber(validationError: ProveAuthError?) {
            guard let vc = presentingVC else {
                callback?.onError()
                return
            }
            
            let alert = UIAlertController(title: "Enter Phone Number", message: validationError != nil ? "Invalid phone number, please try again." : nil, preferredStyle: .alert)
            alert.addTextField { textField in
                textField.placeholder = "+1XXXXXXXXXX"
                textField.keyboardType = .phonePad
            }
            alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
                self.callback?.onError()
            })
            alert.addAction(UIAlertAction(title: "Send OTP", style: .default) { _ in
                let phoneNumber = alert.textFields?.first?.text
                if let phoneNumber = phoneNumber, !phoneNumber.isEmpty {
                    let otpStartInput = OtpStartInput(phoneNumber: phoneNumber)
                    self.callback?.onSuccess(input: otpStartInput)
                } else {
                    // No phone number entered, consider as error or retry
                    self.callback?.onError()
                }
            })
            
            vc.present(alert, animated: true)
        }
        
		// Confirm Send SMS
        private func confirmSendSMS() {
            guard let vc = presentingVC else {
                callback?.onError()
                return
            }
            let alert = UIAlertController(title: "Confirm", message: "Send SMS OTP to your phone?", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
                self.callback?.onError()
            })
            alert.addAction(UIAlertAction(title: "Yes", style: .default) { _ in
                // No phone number to input, just confirm send
                self.callback?.onSuccess(input: nil)
            })
            vc.present(alert, animated: true)
        }
}

class OtpFinishHandler: OtpFinishStep {

		// Set callback for when user submits or cancels
    	private var callback: OtpFinishStepCallback?

        // View controller to present UI from
        private weak var presentingVC: UIViewController?
        
		// Initializer
        init(presentingViewController: UIViewController) {
            self.presentingVC = presentingViewController
        }
        
		// Execute function
        func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) {
            self.callback = callback
            
            DispatchQueue.main.async {
                self.askForOtp(validationError: otpError)
            }
        }
        
		// Ask user to insert OTP
        private func askForOtp(validationError: ProveAuthError?) {
            guard let vc = presentingVC else {
                callback?.onError()
                return
            }
            
            let alert = UIAlertController(title: "Enter OTP", message: validationError != nil ? "Invalid OTP, please try again." : "Enter the OTP sent to your phone", preferredStyle: .alert)
            alert.addTextField { textField in
                textField.placeholder = "OTP"
                textField.keyboardType = .numberPad
            }
            alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
                self.callback?.onError()
            })
            alert.addAction(UIAlertAction(title: "Submit", style: .default) { _ in
                if let otp = alert.textFields?.first?.text, !otp.isEmpty {
                    let otpFinishInput = OtpFinishInput(otp: otp)
                    self.callback?.onSuccess(input: otpFinishInput)
                } else {
                    self.callback?.onError()
                }
            })
            
            vc.present(alert, animated: true)
        }
}

Great! With these two classes, if passive authentication fails, the SDK will automatically call the execute functions which will trigger the UIAlertControllers for the OTP insert process.

Now replace // TODO 1 with an instance of the authentication finish handler and the two otp handlers:

                let authFinishStep = AuthFinishHandler()
                let otpStartStep = OtpStartHandler(presentingViewController: self)
                let otpFinishStep = OtpFinishHandler(presentingViewController: self)

Then, replace // TODO 2 with an initalization of ProveAuth, using the instances of the handlers you just created.

You can do that using this block of code:

                self.proveAuthInstance = ProveAuth.builder(authFinish: authFinishStep)
                    .withMobileAuthTestMode()
                    .withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep)
                    .build()
                self.proveAuthInstance?.authenticate(authToken: result.authToken) { [weak self] error in
                    guard let self = self else { return }
                    DispatchQueue.main.async {
                        if let failureReason = error.failureReason {
                            self.updateResultLabel(with: "Error: \(failureReason)")
                        }
                    }
                }

Here's wha's happening:

  • you added .withMobileAuthTestMode() to support test mode also on simulators;
  • you added .withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep) to tell Prove that, if authentication fails, it should fall back to an OTP type of authentication;
  • you are authenticating the user using the authToken you received from the /v3/unify call;
  • you are updating the result label to display an error messaege if the call fails.

You can now build your app and start the unify flow. You can use user Lorant Nerger from Prove's documentation to test the OTP, but the flow is still incomplete until you implement the /v3/unify-status call.

Step 4: Check Unify status

Similarly to the v3/unify API call, you will need to create a new POST request, this time to endpoint v3/unify-status, to retrieve the success status and make sure the authentication process was successful.

Copy-paste this code in your DataService class:

    // MARK: - Call /v3/unify-status
    func checkUnifyStatus(
        bearerToken: String,
        correlationId: String,
        completion: @escaping (Result<Bool, Error>) -> Void
    ) {
        
        // Validate URL and return error upon failure
        guard let url = URL(string: "https://platform.uat.proveapis.com/v3/unify-status") else {
            completion(.failure(NSError(domain: "InvalidURL", code: 0)))
            return
        }
        
        // Create request
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(bearerToken)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        // Set up body params
        let bodyDict = ["correlationId": correlationId]

        // Transform body params to JSON
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: bodyDict)
        } catch {
            completion(.failure(error))
            return
        }

        // Start data task
        URLSession.shared.dataTask(with: request) { data, response, error in
            
            // If data task fails, return failure
            if let error = error {
                completion(.failure(error))
                return
            }
            
            // If data is absent, return failure
            guard let data = data else {
                completion(.failure(NSError(domain: "NoData", code: 0)))
                return
            }

            // Parse response
            do {
                if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
                   let success = json["success"] as? NSString {
                    completion(.success(success.boolValue))
                } else {
                    completion(.failure(NSError(domain: "InvalidResponse", code: 0)))
                }
            } catch {
                completion(.failure(error))
            }
        }.resume()
    }

Since this API also needs a bearer token, create another function that incorporates the two, like you did for the /v3/unify API:

    // MARK: - Unify status chain calls
    func checkUnifyStatus(correlationId: String, completion: @escaping (Result<Bool, Error>) -> Void) {
        getBearerToken { result in
            switch result {
            case .success(let bearerToken):
                self.checkUnifyStatus(bearerToken: bearerToken, correlationId: correlationId, completion: completion)
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }

This function will return a boolean result that will tell you whether authentication was successful. In order to perform it, you have to add it to the execute method of AuthFinishHandler, but you will need the correlationId and a onResult completion to handle both case of success and failure.

Update your AuthFinishHandler to store these variables and perform checkUnifyStatus:

class AuthFinishHandler: ProveAuthFinishStep {

    let correlationId: String?
    let onResult: (String) -> Void

    init(correlationId: String? = nil, onResult: @escaping (String) -> Void) {
        self.correlationId = correlationId
        self.onResult = onResult
    }

    func execute(authId: String) {
        guard let correlationId = correlationId else { self.onResult("Authentication failed ❌"); return }
        DataService.shared.checkUnifyStatus(correlationId: correlationId) { result in
            switch result {
            case .success(let success):
                self.onResult(success ? "Authentication succeeded ✅" : "Authentication failed ❌")
            case .failure(let error):
                self.onResult("Authentication failed ❌ \(error.localizedDescription)")
            }
        }
    }
}

In your view controller, update the authFinishStep instance to this:

                let authFinishStep = AuthFinishHandler(
                    correlationId: result.correlationId,
                    onResult: self.onResult)

This way, you will perform the entire flow and give the handler all the necessary information to perform the last API call.

Step 5: Test!

Now you're ready to test! Use the Sandbox number 2001004014 to simulate an authentication flow that fails with OTP fallback.

These should be the results:

sandbox number for testing fall back otp 1 fall back otp 2

And that's it! You can test your flow with both success and failure cases. The correct OTP to unlock this Sandbox user is 1234. Once you insert the correct OTP, the system won't ask for it again.

Conclusion

Passive authentication is a powerful, modern tool for any app. It significantly improves security and users experience by making their login and signup process smoth and seamless. With Prove Unify, you can easily implement passive authentication thanks to a developer-friendly SDK available for most platforms, including iOS. In this guide, youhave learned:

  • What passive authentication is,
  • Why it’s effective,
  • How to set up an iOS app with Prove Unify,
  • Demonstrated a full authentication flow using /v3/unify and /v3/unify-status. If you’re building a mobile app and are considering implementing passive authentication, Prove Unify is a great tool to explore further.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published