Biometric Authentication With Keychain in iOS
Properly setting up biometric authentication in iOS with Keychain.
Users love Touch ID and Face ID because these authentication mechanisms let them access their devices securely, with minimal effort. Today most Apps provide a biometric authentication system for their users to have secure and easy access to their Apps.
Let's discuss how can we properly set up biometrics in our App.
Set the Face ID Usage Description
In any project that uses biometrics, include the NSFaceIDUsageDescription key in your app’s Info.plist
file. Without this key, the system won’t allow your app to use Face ID. The value for this key is a string that the system presents to the user the first time your app attempts to use Face ID. The string should clearly explain why your app needs access to this authentication mechanism. The system doesn’t require a comparable usage description for Touch ID.
Let's create a Biometrics Manager class.
class BiometricsManager {static var shared: BiometricsManager = BiometricsManager() private init() {}}
Create and Configure a Context
You perform biometric authentication in your app using an LAContext
instance, which brokers interaction between your app and the Secure Enclave. Begin by creating a context inside your Biometrics Manager class with a device Biometrics type to identify if your device supports biometrics and if it does what kind of biometrics does it supports(Touch Id or FaceId)
var context = LAContext()var error: NSError?lazy var biometricType: LABiometryType? = {func checkForBiometricsPermission(completion: @escaping((Bool) -> Void)) {context.localizedCancelTitle = "Enter Username/Password"var error: NSError?let permission = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error)permission ? completion(true) : completion(false)}
You can always return a localized text in the evaluatePolicy parameter based on your requirement and design type like this. We will use this method below when we ask the user for his biometric permission.
var authenticationTypeReason: String {switch biometricType {case .faceID:return "Log in with Face ID"case .touchID:return "Log in with Touch ID"case .none:return ""@unknown default:return ""}}
Choose a value from the LAPolicy
enumeration for which to test. The policy controls how the authentication behaves. For example, the LAPolicy.deviceOwnerAuthentication
policy used in this sample indicates that reverting to a passcode is allowed when biometrics fails or is unavailable. Alternatively, you can indicate the LAPolicy.deviceOwnerAuthenticationWithBiometrics
policy, which doesn’t allow reverting to the device passcode. We will use deviceOwnerAuthentication so we can have a fallback to passcode if biometric fails.
Now we can check if the device has biometrics and if yes we can ask user for the biometrics permission.
// .deviceOwnerAuthentication allows biometric or passcode authenticationfunc authenticateWithBiometrics(completion: @escaping((Bool, String?) -> Void)) {checkForBiometricsPermission { permission inif permission {context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: authenticationTypeReason) { success, error inif success, error == nil {// Handle successful authenticationcompletion(true, nil)} else {// Handle LAError errorif let err = error {completion(false, self.getErrorDetails(error: err as NSError))}else {completion(false, nil)}}}}else {completion(false, nil)}}}
Apple returns LAError for the error code we provided based on our Biometrics authentication failure. In this method I am returning the comments written for each of the error codes by Apple as string, you can do whatever you need based on your requirement.
private func getErrorDetails(error: NSError) -> String {// If error is an instance of LAErrorif let code = LAError.Code(rawValue: error.code) {switch code {case .appCancel:return "Authentication was canceled by application."case .authenticationFailed:return "Authentication was not successful because user failed to provide valid credentials."case .invalidContext:return "The LAContext was invalid"case .notInteractive:return "Authentication failed because it would require showing UI which has been forbidden"case .passcodeNotSet:return "Authentication could not start because passcode is not set on the device"case LAError.Code.systemCancel:return "Authentication was canceled by system (e.g. another application went to foreground)"case LAError.Code.userCancel:return "Authentication was canceled by user (e.g. tapped Cancel button)."case LAError.Code.userFallback:return "Authentication was canceled because the user tapped the fallback button (Enter Password)."case LAError.Code.biometryLockout:return "Authentication was not successful because there were too many failed Touch ID attempts and Touch ID is now locked. Passcode is required to unlock Touch ID"case LAError.Code.biometryNotAvailable:return "Authentication could not start because Touch ID is not available on the device."case LAError.Code.biometryNotEnrolled:return "Authentication could not start because Touch ID has no enrolled fingers."case .touchIDNotAvailable:return "Authentication could not start because Touch ID is not available on the device."case .touchIDNotEnrolled:return "Authentication could not start because Touch ID has no enrolled fingers."case .touchIDLockout:return "Authentication was not successful because there were too many failed Touch ID attempts and Touch ID is now locked. Passcode is required to unlock Touch ID"@unknown default:return "Unknown Error Occurred"}}return "Unknown Error Occurred"}
Now let's set up Keychain.
We will be using KeychainWrapper dependency so that we don't have to code for keychain setup all from scratch. KeychainWrapper is an excellent pod that provides flexibility to use Keychain as similar to UserDefaults, also been recommended by Paul Hudson.
Below is the link to the Pod. You can either install it using cocoapods or SMP.
https://github.com/jrendel/SwiftKeychainWrapper
Let's create our Keychain Storage class so we can store user credentials in the Keychain by converting them into Data(), once we receive authentication information from the user the first time. Next time he can log in with biometrics, based on if the credentials are already stored in the keychain and his biometrics matches the device biometrics.
First, we will create a model class to hold the credentials, and then we can save and return them from our KeychainStroage class.
struct Credentials: Codable {var email: String?var password: String?func encode() -> String {let encoder = JSONEncoder()if let credentials = try? encoder.encode(self) {return String(data: credentials, encoding: .utf8) ?? ""}return ""}static func decode(credentials: String) -> Credentials? {let decoder = JSONDecoder()guard let jsonData = credentials.data(using: .utf8) else { return nil }guard let credentials = try? decoder.decode(Credentials.self, from: jsonData) else { return nil }return credentials}}
Now let's create two methods to set and get our credentials in our keychain.
struct KeyChainStorage {static var key = "credentials"static func getCredentials() -> Credentials? {if let credentials = KeychainWrapper.standard.string(forKey: key) {return Credentials.decode(credentials: credentials)}return nil}static func saveCredentials(credentials: Credentials) {KeychainWrapper.standard.set(credentials.encode(), forKey: key)}}
Our work is done, now we can do the implementation.
First of all, we will need to save the credentials in the keychain whenever a user logs in.
I will not implement a full API call but let's assume we have a login API call that takes username and password. So in the success of that API call we can save our credentials in the keychain like this.
let creds = Credentials.init(email: userEmail, password: userPassword)KeyChainStorage.saveCredentials(credentials: creds)
Now when calling to authenticate a user through biometrics always remember to do it in the main thread if you are updating any UI component. Like this.
DispatchQueue.main.async {BiometricsManager.shared.authenticateWithBiometrics {[weak self] result, message inif result, message == nil {let creds = KeyChainStorage.getCredentials()self?.callLogin(userEmail: creds?.email ?? "", userPassword: creds?.password ?? "")}else {self?.setupMessageDialog(message: message ?? "")}}}
After the login API success completion is called from the credentials we passed from our keychain on successful biometrics, route the user into the App.
You can check the full code here:
So that’s basically it!!
Please leave a comment if you like the article or have any questions in any regard related to the article.
Until next time.
Thank you.
Sheeraz Ahmed