User Authentication with Face ID/Touch ID in SwiftUI
DevTechie Inc
DevTechie Inc
May 28, 2022
Authentication is important to keep your user’s information safe but asking for a username and password every time they land on your app can spoil the experience. Face ID or Touch ID (AKA. Biometric Authentication) provides a convenient way to authenticate users without the need to enter a username and password.
Your app users can rely on biometric authentication like Face ID or Touch ID to enable secure, effortless access to the app. As a fallback option, and for devices without biometry, passcode or password serves a similar purpose.
LocalAuthentication is the framework used to leverage these mechanisms in your app and to extend authentication procedures your app already implements. This framework wraps biometric authentication functionality and gives you access to an easy-to-use API that can be used for user authentication.
To maximize security, Apple never gives your app access to any of the underlying authentication data. You can’t access any fingerprint images or face recognition data. User’s biometric information is saved in a hardware-based security processor called “The Secure Enclave” which keeps this information isolated from the rest of the system and manages the data out of reach even of the operating system.
You can specify a particular policy and provide messaging that tells the user why you want them to authenticate. The framework then coordinates with the Secure Enclave to carry out the operation. Afterward, you receive only a Boolean result indicating authentication success or failure.
Let’s build out a sample page that will hide behind Face ID authentication.
Here is what we will build:
We will begin with simple course model, this model will conform to identifiable protocol.
struct Course: Identifiable {
let id = UUID().uuidString
var name: String
var duration: String
var category: String
}
We will also add some sample data in course model’s extension
Next, we will build out a view that will render all the sample courses in a list
struct DevTechieHomeView: View {
let courses = Course.sample
var body: some View {
NavigationView {
List(courses) { course in
VStack(alignment: .leading) {
Text(course.name)
.font(.title)
HStack {
Text(course.duration)
Spacer()
Text(course.category)
}.foregroundColor(.secondary)
}.padding()
.background(RoundedRectangle(cornerRadius: 10).fill(Color.secondary.opacity(0.3)))
.listRowSeparator(.hidden)
}
.listStyle(.plain)
.navigationTitle("DevTechie")
}
}
}
Our view will look like this
Authentication using Face ID
Now we will build UI which will be presented when the app is launched so our users can authenticate themselves before accessing the list of DevTechie courses.
struct DevTechieLoginView: View {
@State private var isUnlocked = false
@State private var failedAuth = ""
var body: some View {
if isUnlocked {
DevTechieHomeView()
} else {
VStack {
Text("Welcome to")
Text("DevTechie Courses")
.font(.largeTitle)
Button(action: /* call authentcation here */ ) {
Label("Login with FaceID", systemImage: "faceid")
.padding()
.background(RoundedRectangle(cornerRadius: 15).foregroundColor(.white))
}
Text(failedAuth)
.padding()
.opacity(failedAuth.isEmpty ? 0.0 : 1.0)
}
}
}
}
We will manage two State variables called isUnlocked and failedAuth. isUnlocked will be toggled based on Face ID. failedAuth will be used to store error messages if authentication fails.
Before we begin to implement our authenticate function, we need to include the NSFaceIDUsageDescription key in the app’s Info.plist file. Without this key, the system won’t allow our app to use Face ID. The value for this key is a string that the system presents to the user the first time the app attempts to use Face ID. The string should clearly explain why the app needs access to this authentication mechanism. The system doesn’t require a comparable usage description for Touch ID.
Let’s look at the authentication part. Add following below body property in DevTechieLoginView.
private func authenticate() {
var error: NSError?
let laContext = LAContext()
if laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
let reason = "Need access to authenticate"
laContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, error in
DispatchQueue.main.async {
if success {
isUnlocked = true
failedAuth = ""
} else {
print(error?.localizedDescription ?? "error")
failedAuth = error?.localizedDescription ?? "error"
}
}
}
} else {
}
}
We start with an instance object for LAContext, which will help us determine if user’s device supports biometric authentication or not. This instance acts as a broker's interaction between the app and the Secure Enclave.
Complete DevTechieLoginView’s code:
import SwiftUI
import LocalAuthenticationstruct DevTechieLoginView: View {
@State private var isUnlocked = false
@State private var failedAuth = ""
var body: some View {
if isUnlocked {
DevTechieHomeView()
} else {
VStack {
Text("Welcome to")
Text("DevTechie Courses")
.font(.largeTitle)
Button(action: authenticate) {
Label("Login with FaceID", systemImage: "faceid")
.padding()
.background(RoundedRectangle(cornerRadius: 15).foregroundColor(.white))
}
Text(failedAuth)
.padding()
.opacity(failedAuth.isEmpty ? 0.0 : 1.0)
}
}
}
private func authenticate() {
var error: NSError?
let laContext = LAContext()
if laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
let reason = "Need access to authenticate"
laContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, error in
DispatchQueue.main.async {
if success {
isUnlocked = true
failedAuth = ""
} else {
print(error?.localizedDescription ?? "error")
failedAuth = error?.localizedDescription ?? "error"
}
}
}
} else {
}
}
}