So far we have an ap that can scan its config from a QR code and persist it in the keychain on the device.
Today we'll add support for using biometric unlock (Face ID or Touch ID depending on the device).
Simulator Data
However - first we'll make one super quick change to make testing in the simulator a little easier. We'll return a dummy config json in the config scanner.
In ScannerView - create a constant:
let simulatedData = """
{
"clientId": "clientId",
"clientSecret": "clientSecret",
"userId": "userId",
"accountNr": "12345678903"
}
"""
And then in the call to CodeScannerView pass the simulatedData variable instead of "-"
Touch ID/Face ID
These use Apple's LocalAuthentication module.
Create a swift file to hold some utility functions - Authentication.swift.
Import LocalAuthentication
just after Foundation
then create the following utility method and enum:
enum AuthStatus {
case OK
case Error
case Unavailable
}
func authenticateUser(_ callback: @escaping (_ status: AuthStatus) -> Void) {
let context = LAContext()
var error: NSError?
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Unlock") { success, authenticationError in
DispatchQueue.main.async {
if success {
print("Authentication OK")
callback(.OK)
} else {
print("Authentication Error")
callback(.Error)
}
}
}
} else {
print("Authentication Unavailable")
callback(.Unavailable)
}
}
Notes
error: &error
don't really feel so "swift" but they still work.Face ID
But wait - this will only work for Touch ID. We gave a (too brief) reason to the call to evaluatePolicy here - but that is only used for Touch ID. For Face ID we have to add it to the Info.plist instead.
Open Info.plist and add a new line:
"Privacy - Face ID Usage Description" - "Unlock"
Using the authenticate function in the view
Add the following state variable:
@State private var authenticated = false
A quick utility function to call and handle the response:
func askForAuth() {
authenticateUser() { status in
switch(status) {
case .OK:
self.authenticated = true
case .Error:
self.authenticated = false
case .Unavailable:
self.authenticated = true
}
}
}
Note that if authentication is unavailable we're just letting the user in. Here you would need fallback to a login or similar - but for this proof of concept app (and given that my son has a touch ID device) this will do here.
So - when do we call this?
I want to call this in two places:
App Load
Add the if clause after the call to loadConfig()
:
.onAppear {
self.config = Config.loadConfig()
if (self.config != nil && self.authenticated == false) {
self.askForAuth()
}
}
Config load
In newScanData - add the if clause after the line self.config = config
:
func newScanData(_ code: String) {
if let config = Config.decodeConfig(json: code) {
config.save()
self.config = config
if (self.authenticated == false) {
self.askForAuth()
}
}
}
View
In the view - we currently have a conditional display of the account number or the string "You need to scan in a configuation". Extend this to check the authenticated
state:
if (self.config != nil) {
if (self.authenticated == false) {
Text("Please authenticate")
} else {
Text(config!.accountNr)
}
} else {
Text("You need to scan in a configuation")
}
Simulator
You can test both Face ID and Touch ID in the simulator if you use the menu Hardware > Face/Touch ID
first to enrol the device and then also to tell the simulator that the face/fingerprint is good.
Summary
We've now added basic support for biometric authentication. In the next post we'll actually start calling the S'banken API and then in further posts we will start doing something with the view layout.