Biometric authentication has transformed mobile security, and Apple’s Face ID, introduced with the iPhone X in 2017, stands at the forefront of this revolution.
Utilizing advanced facial recognition technology, Face ID goes beyond traditional methods by leveraging depth-sensing cameras and machine learning algorithms.
Stored securely within the device’s Secure Enclave, Face ID creates a mathematical representation of facial features, ensuring unparalleled security. Moreover, its seamless integration into iOS apps has redefined user interactions, setting new standards for mobile authentication.
In this blog, we’ll delve into the process of integrating Face ID into iOS apps to enhance both security and user experience.
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
Face ID replaces the conventional Touch ID with a sophisticated facial recognition system, providing users with a secure and effortless means of unlocking their devices and validating actions.
Utilizing the TrueDepth camera system, Face ID captures and analyzes facial features in real-time, generating a precise 3D map. This map, created from thousands of infrared dots projected onto the face, forms a unique Faceprint and is securely stored within the device’s Secure Enclave.
During authentication, Face ID compares the current facial data with the stored Faceprint, utilizing machine learning to adapt to changes in appearance and ensure consistent performance.
In essence, Face ID represents a user-friendly authentication solution that integrates cutting-edge technology to redefine mobile security and user experience standards on iOS devices.
Face ID presents a significant leap forward in security and convenience compared to traditional password or PIN-based authentication methods.
By relying on facial recognition technology, it eliminates the need for manual input, offering users a seamless and hands-free authentication experience.
Furthermore, the inherent uniqueness of facial features makes Face ID exceptionally secure, greatly reducing the risk of phishing attacks.
Misconceptions surrounding Face ID’s security and privacy have led to concerns about data storage and privacy risks. However, it’s crucial to understand that Face ID securely stores facial data locally on the device, making it inaccessible to apps or cloud services.
Additionally, it continually improves its performance through machine learning algorithms, enhancing security measures while safeguarding user privacy.
Since Face ID operates solely on the device, it doesn’t transmit facial data externally, mitigating privacy concerns and ensuring user data remains protected.
To successfully integrate Face ID into iOS apps, developers need to meet specific hardware, software, and prerequisite criteria.
Ensure the device includes a TrueDepth camera system (a key component for Face ID functionality) available on iPhone X and later models.
Face ID integration requires a minimum iOS version compatible with the TrueDepth camera, typically iOS 11 or later.
Info.plist
file includes the necessary permissions and descriptions for accessing FaceID usage description. This is crucial for informing users about why the app requires Face ID access.To access the faceID into an iOS app involves several key steps:
Update Info.plist: Add ‘Privacy — Face ID Usage Description’ to the app’s Info.plist file. This string value informs users about the app’s use of Face ID and should be concise yet informative.Note: Keep the key as sort as possible which allow user to undestand the purpose of permission.
Create Authenticator: Create an authenticator within the app that uses Face ID APIs to authenticate users. Import the ‘LocalAuthentication’ module, which provides access to authentication-related APIs.Note: The app’s Info.plist must contain an NSFaceIDUsageDescription key with a string value explaining to the user how the app uses this data.
Here we need to check whether the device supports biometric authentication or not, to do that we have created an extension of LAContext
which provides the status of availability.
extension LAContext {
enum BiometricType: String {
case none
case touchID
case faceID
case opticID // Added a case for potential future biometric methods
}
// Returns the detected biometric type based on the capabilities of the device
var biometricType: BiometricType {
var error: NSError?
// Check if the device supports biometric authentication
guard self.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
return .none
}
if #available(iOS 11.0, *) {
// Determine the specific biometric type supported on iOS 11.0 and later
switch self.biometryType {
case .none:
return .none
case .touchID:
return .touchID
case .faceID:
return .faceID
default:
if #available(iOS 17.0, *) {
// Support for potential future biometric methods
if self.biometryType == .opticID {
return .opticID
} else {
return .none
}
}
}
}
// Fallback to checking for Touch ID support on iOS versions older than 11.0
return self.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) ? .touchID : .none
}
}
This extension introduces an enum BiometricType
, enabling developers to determine the supported biometric authentication method on iOS devices.
It checks for the device supports for the biometric authentication using canEvaluatePolicy(_:error:)
. If biometric authentication is not available, it returns .none
.
For devices running iOS 11.0 and later, it utilizes the biometryType
property of LAContext
to determine the supported biometric type.
Additionally, it includes a case for potential future biometric methods, such as optic scanning, and provides a fallback mechanism for devices running iOS versions older than 11.0.
For authentication, we have created an Authenticator class which manages all the authentication-related things.
In this class, we have the authenticate() method to authenticate whether the user is verified or not and it will act accordingly.
private func authenticate() {
// Check if biometric authentication is available and not locked
guard isBiometricAvailable() && !isBiometricLocked else { return }
isLoading = true
var error: NSError?
// Check if biometric authentication is possible
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
// Perform biometric authentication
let reason = "We need to unlock your data."
// It presents a localized reason for authentication to the user, explaining why authentication is necessary.
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { [weak self]
success, authenticationError in
guard let self else { return }
// Handle authentication completion
if success {
// proceeds to decrypt and verify the user's passcode
let passcode = self.decryptUserPasscode()
self.isAuthenticated = passcode != nil
} else {
// increments the failedAttempt count
self.failedAttempt += 1
// check for the maximum allowed failed limit
self.isBiometricLocked = self.failedAttempt >= self.maxFailedAttemptAllowed
}
self.isLoading = false
}
} else {
// Handle error if biometric authentication is not possible
if let error {
handleLaError(error: error)
} else {
// Handle other errors if any
}
}
}
This authenticate()
function is responsible for initiating the biometric authentication process.
canEvaluatePolicy
. If it's possible, it proceeds with it and handles success or failure accordingly.This function provides a structured approach to biometric authentication, handling success, failure, and error scenarios gracefully while ensuring a smooth user experience.
deviceOwnerAuthenticationWithBiometrics
policy then you will have to handle the fallback yourself.deviceOwnerAuthentication
only, then biometrics will be used if available and authorized, otherwise, it will automatically fall back to passcode if biometrics is unavailable, or give you the fallback option to enter passcode automatically if biometrics attempt fails.func setPasscodeWith(_ code: String) {
// Check if biometric authentication is available
guard isBiometricAvailable() else { return }
// Generate a unique encryption key
let key = UUID().uuidString // Instead of this random string you can use a key that can be recoverable
// Encrypt the passcode using AES encryption algorithm
let encryptedPasscode = AESEncryptionManager.encrypt(plainText: code, key: key)
// Store the encrypted passcode and encryption key securely
userDefault.setValue(encryptedPasscode, forKey: userDefaultPasscodeKey)
userDefault.setValue(key, forKey: userDefaultSecretKey)
}
This Swift function setPasscodeWith(_:)
is responsible for setting a passcode using biometric authentication, if available.
Here's a breakdown of its functionality:
UUID().uuidString
that will be used to encrypt the passcode securely.AESEncryptionManager.encrypt(plainText:key:)
function encrypts the passcode using the AES encryption algorithm.The encrypted passcode is stored with a key userDefaultPasscodeKey
and the encryption key is stored with a key userDefaultSecretKey
, This ensures that the passcode remains stored on the device.
If you are not aware about AES encryption then here is the good Article on it which explains AES encryption in detail, written by Amisha.
In this post, we have used deviceOwnerAuthenticationWithBiometrics
policy so we need to handle fallback if the failed attempt reaches its maximum limit.
In the example, we have managed it with failedAttempt
a variable that records the failed attempt.
We’ve kept the limit to 3 failed attempts, you can choose according to your convenience.
Once the limit reaches the max we hide the FaceID option and force the user to enter the passcode.
And then we match the entered passcode with the stored passcode and authenticate the user based on that.
// Decrypt stored passcode
func decryptUserPasscode() -> String? {
guard let encryptedPasscode = userDefault.value(forKey: userDefaultPasscodeKey) as? String else { return nil }
var passcode: String?
if let key = userDefault.value(forKey: userDefaultSecretKey) as? String {
passcode = AESEncryptionManager.decrypt(encryptedText: encryptedPasscode, key: key)
isAuthenticated = passcode != nil
}
return passcode
}
// Verify entered passcode
func verifyPin(pin: String) {
guard let passcode = decryptUserPasscode() else {
isAuthenticated = false
return
}
isAuthenticated = passcode == pin
resetFailCount()
}
Here is the breakdown of decryptUserPasscode()
,
Now verifyPin(pin: _)
does the rest of the work done,
decryptUserPasscode()
.pin
.isAuthenticated
.For testing, initiate the app and utilize the FaceID button located at the bottom. In a real application, you can directly call the authenticate()
method on viewAppear
or in view’s .task()
modifier callback to make authentication smoother and faster.
When the FaceID recognition pop-over appears, press Cmd+Opt+M
to trigger the matching face action. You can locate this option in the simulator’s Menu -> Features -> Face ID -> Matching Face for testing matching faces.
To test non-matching faces, press Cmd+Opt+N
or access the simulator’s Menu -> Features -> Face ID -> Non-matching Face option.
Make sure you have enabled
Enrolled
fromManu->Face ID->Enrolled
otherwise you will be not able to test FaceID in the simulator.
By following the above steps you can successfully test the FaceID behavior in the simulator.
That’s it for today.
The full source code is available in this GitHub Repo.
In conclusion, integrating biometric authentication, particularly Face ID, into iOS apps represents a substantial leap forward in both the security and user experience realms.
With its superior security features and seamless user experience, Face ID surpasses traditional authentication methods like passwords.
By embracing biometric authentication, developers play a pivotal role in cultivating a more secure and user-friendly digital landscape, providing iOS users with smooth and dependable authentication experiences.
Are you looking for a similar implementation on the Android side?
Here is the Article written by Megh.
Happy coding… 🤖
Let's Work Together
Not sure where to start? We also offer code and architecture reviews, strategic planning, and more.