User Authentication with Face ID/Touch ID in SwiftUI

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

extension Course {
    static var sample: [Course] {
        [
            Course(name: "Mastering SwiftUI 3", duration: "1h 20m", category: "SwiftUI"),
            Course(name: "UIKit in Depth", duration: "2h 30m", category: "UIKit"),
            Course(name: "Machine Learning by Example", duration: "4h 20m", category: "iOS Machine Learning")
        ]
    }
}
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 {
            
        }
        
    }
}