Protecting an app with a login screen is a great way to secure user data – you can use the Keychain, which is built right in to iOS, to ensure their data stays secure. Apple also offers yet another layer of protection with Face ID and Touch ID.
Available since the iPhone 5S, biometric data is stored in a secure enclave in the A7 and newer chips. All of this means you can comfortably hand over the responsibility of handling login information to the Keychain and either Face ID or Touch ID.
In this tutorial you’ll start out with static authentication. Next you’ll be using the Keychain to store and verify login information. Finally, you’ll explore using Touch ID or Face ID in your app.
Getting Started
Download the starter project for this tutorial here.
This is a basic note taking app that uses Core Data to store user notes; the storyboard has a login view where users can enter a username and password, and the rest of the app’s views are already connected to each other and ready to use.
Build and run to see what your app looks like in its current state:
Note
type missing. It will be autogenerated by Core DataAt this point, tapping the Login button simply dismisses the view and displays a list of notes – you can also create new notes from this screen. Tapping Logout takes you back to the login view. If the app is pushed to the background it will immediately return to the login view; this protects data from being viewed without being logged in.
Before you do anything else, you should change the Bundle Identifier, and assign an appropriate Team.
Select TouchMeIn in the Project navigator, and then select the TouchMeIn target. In the General tab change Bundle Identifier to use your own domain name, in reverse-domain-notation – for example com.raywenderich.TouchMeIn.
Then, from the Team menu, select the team associated with your developer account like so:
With all of the housekeeping done, it’s time to code! :]
Logging? No. Log In.
To get the ball rolling, you’re going to add the ability to check the user-provided credentials against hard-coded values.
Open LoginViewController.swift and add the following constants just below managedObjectContext
:
let usernameKey = "Batman"
let passwordKey = "Hello Bruce!"
These are simply the hard-coded username and password you’ll check the user-provided credentials against.
Next, add the following method below loginAction(_:)
:
func checkLogin(username: String, password: String) -> Bool {
return username == usernameKey && password == passwordKey
}
Here you check the user-provided credentials against the constants previously defined.
Next, replace the contents of loginAction(_:)
with the following:
if checkLogin(username: usernameTextField.text!, password: passwordTextField.text!) {
performSegue(withIdentifier: "dismissLogin", sender: self)
}
Here you call checkLogin(username:password:)
, which dismisses the login view only if the credentials are correct.
Build and run. Enter the username Batman and the password Hello Bruce!, and tap the Login button. The login screen should dismiss as expected.
While this simple approach to authentication seems to work, it’s not terribly secure, as credentials stored as strings can easily be compromised by curious hackers with the right tools and training. As a best practice, passwords should NEVER be stored directly in the app. To that end, you’ll employ the Keychain to store the password.
Check out Chris Lowe’s Basic Security in iOS 5 – Part 1 tutorial for the lowdown on how the Keychain works.
Rapper? No. Wrapper.
The next step is to add a Keychain wrapper to your app.
Along with the starter project, you downloaded a folder with useful resources. Locate and open the Resources folder in Finder. You’ll see the file KeychainPasswordItem.swift; this class comes from Apple’s sample code GenericKeychain.
Drag the KeychainPasswordItem.swift into the project, like so:
When prompted, make sure Copy items if needed and TouchMeIn target are both checked:
Build and run to make sure you have no errors. All good? Great — now you can leverage the Keychain from within your app.
Keychain, Meet Password. Password, Meet Keychain
To use the Keychain, you first store a username and password in it. Next, you’ll check the user-provided credentials against the Keychain to see if they match.
You’ll track whether the user has already created some credentials so you can change the text on the Login button from “Create” to “Login”. You’ll also store the username in the user defaults so you can perform this check without hitting the Keychain each time.
The Keychain requires some configuration to properly store your app’s information. You’ll provide that configuration in the form of a serviceName
and an optional accessGroup
. You’ll use a struct to store these values.
Open LoginViewController.swift. Add the following just below the import
statements:
// Keychain Configuration
struct KeychainConfiguration {
static let serviceName = "TouchMeIn"
static let accessGroup: String? = nil
}
Next, add the following below managedObjectContext
:
var passwordItems: [KeychainPasswordItem] = []
let createLoginButtonTag = 0
let loginButtonTag = 1
@IBOutlet weak var loginButton: UIButton!
passwordItems
is an empty array of KeychainPasswordItem
types you’ll pass into the keychain. You’ll use the next two constants to determine if the Login button is being used to create some credentials, or to log in; you’ll use the loginButton
outlet to update the title of the button depending on its state.
Next, you’ll handle the two cases for when the button is tapped: if the user hasn’t yet created their credentials, the button text will show “Create”, otherwise the button will show “Login”.
First you’ll need a way to tell the user if the login fails. Add the following after checkLogin(username:password:)
:
private func showLoginFailedAlert() {
let alertView = UIAlertController(title: "Login Problem",
message: "Wrong username or password.",
preferredStyle:. alert)
let okAction = UIAlertAction(title: "Foiled Again!", style: .default)
alertView.addAction(okAction)
present(alertView, animated: true)
}
Now, replace loginAction(sender:)
with the following:
@IBAction func loginAction(sender: UIButton) {
// 1
// Check that text has been entered into both the username and password fields.
guard let newAccountName = usernameTextField.text,
let newPassword = passwordTextField.text,
!newAccountName.isEmpty,
!newPassword.isEmpty else {
showLoginFailedAlert()
return
}
// 2
usernameTextField.resignFirstResponder()
passwordTextField.resignFirstResponder()
// 3
if sender.tag == createLoginButtonTag {
// 4
let hasLoginKey = UserDefaults.standard.bool(forKey: "hasLoginKey")
if !hasLoginKey && usernameTextField.hasText {
UserDefaults.standard.setValue(usernameTextField.text, forKey: "username")
}
// 5
do {
// This is a new account, create a new keychain item with the account name.
let passwordItem = KeychainPasswordItem(service: KeychainConfiguration.serviceName,
account: newAccountName,
accessGroup: KeychainConfiguration.accessGroup)
// Save the password for the new item.
try passwordItem.savePassword(newPassword)
} catch {
fatalError("Error updating keychain - \(error)")
}
// 6
UserDefaults.standard.set(true, forKey: "hasLoginKey")
loginButton.tag = loginButtonTag
performSegue(withIdentifier: "dismissLogin", sender: self)
} else if sender.tag == loginButtonTag {
// 7
if checkLogin(username: newAccountName, password: newPassword) {
performSegue(withIdentifier: "dismissLogin", sender: self)
} else {
// 8
showLoginFailedAlert()
}
}
}
Here’s what’s happening in the code:
- If either the username or password is empty, you present an alert to the user and return from the method.
- Dismiss the keyboard if it’s visible.
- If the login button’s tag is
createLoginButtonTag
, then proceed to create a new login. - Next, you read
hasLoginKey
fromUserDefaults
which you use to indicate whether a password has been saved to the Keychain. IfhasLoginKey
is false and theusername
field has any text, then you save that text asusername
toUserDefaults
. - You create a
KeychainPasswordItem
with theserviceName
,newAccountName
(username) andaccessGroup
. Using Swift’s error handling, you try to save the password. Thecatch
is there if something goes wrong. - You then set
hasLoginKey
inUserDefaults
totrue
to indicate a password has been saved to the keychain. You set the login button’s tag tologinButtonTag
to change the button’s text, so it will prompt the user to log in the next time they run your app, rather than prompting the user to create a login. Finally, you dismissloginView
. - If the user is logging in (as indicated by
loginButtonTag
), you callcheckLogin
to verify the user-provided credentials; if they match then you dismiss the login view. - If the login authentication fails, then present an alert message to the user.
UserDefaults
? That would be a bad idea because values stored in UserDefaults
are persisted using a plist file. This is essentially an XML file that resides in the app’s Library folder, and is therefore readable by anyone with physical access to the device. The Keychain, on the other hand, uses the Triple Digital Encryption Standard (3DES) to encrypt its data. Even if somebody gets the data, they won’t be able to read it.Next, replace checkLogin(username:password:)
with the following updated implementation:
func checkLogin(username: String, password: String) -> Bool {
guard username == UserDefaults.standard.value(forKey: "username") as? String else {
return false
}
do {
let passwordItem = KeychainPasswordItem(service: KeychainConfiguration.serviceName,
account: username,
accessGroup: KeychainConfiguration.accessGroup)
let keychainPassword = try passwordItem.readPassword()
return password == keychainPassword
} catch {
fatalError("Error reading password from keychain - \(error)")
}
}
Here you check that the username entered matches the one stored in UserDefaults
and that the password matches the one stored in the Keychain.
Next, delete the following lines:
let usernameKey = "Batman"
let passwordKey = "Hello Bruce!"
Now it’s time to set the button title and tags appropriately depending on the state of hasLoginKey
.
Add the following code to viewDidLoad()
, just below the call to super
:
// 1
let hasLogin = UserDefaults.standard.bool(forKey: "hasLoginKey")
// 2
if hasLogin {
loginButton.setTitle("Login", for: .normal)
loginButton.tag = loginButtonTag
createInfoLabel.isHidden = true
} else {
loginButton.setTitle("Create", for: .normal)
loginButton.tag = createLoginButtonTag
createInfoLabel.isHidden = false
}
// 3
if let storedUsername = UserDefaults.standard.value(forKey: "username") as? String {
usernameTextField.text = storedUsername
}
Taking each numbered comment in turn:
- You first check
hasLoginKey
to see if you’ve already stored a login for this user. - If so, change the button’s title to Login, update its tag to
loginButtonTag
, and hidecreateInfoLabel
, which contains the informative text “Start by creating a username and password“. In case you don’t have a stored login for this user, you set the button label to Create and displaycreateInfoLabel
to the user. - Finally, you set the username field to what is saved in
UserDefaults
to make logging in a little more convenient for the user.
Finally, you need to connect your outlet to the Login button. Open Main.storyboard and select the Login View Controller Scene. Ctrl-drag from the Login View Controller to the Login button, as shown below:
From the resulting popup, choose loginButton:
Build and run. Enter a username and password of your own choosing, then tap Create.
loginButton
IBOutlet then you might see the error Fatal error: unexpectedly found nil while unwrapping an Optional value
. If you do, connect the outlet as described in the relevant step above.Now tap Logout and attempt to login with the same username and password – you should see the list of notes appear.
Tap Logout and try to log in again; this time, use a different password and then tap Login. You should see the following alert:
Congratulations – you’ve now added authentication use the Keychain. Next up, Touch ID.
Touching You, Touching Me
In this section, you’ll add biometric ID to your project in addition to using the Keychain. While Keychain isn’t necessary for Face ID/Touch ID to work, it’s always a good idea to implement a backup authentication method for instances where biometric ID fails, or for users that don’t have at least a Touch ID compatible device.
Open Assets.xcassets.
Next, open the Resources folder from the starter project you downloaded earlier in Finder. Locate FaceIcon and Touch-icon-lg.png images (all three sizes), select all six and drag them into Images.xcassets so that Xcode knows they’re the same image, only with different resolutions:
Open Main.storyboard and drag a Button from the Object Library onto the Login View Controller Scene, just below the Create Info Label, inside the Stack View. You can open the Document Outline, swing open the disclosure triangles and make sure that the Button is inside the Stack View. It should look like this:
If you need to review stack views, take a look at UIStackView Tutorial: Introducing Stack Views.
In the Attributes inspector, adjust the button’s attributes as follows:
- Set Type to Custom.
- Leave the Title empty.
- Set Image to Touch-icon-lg.
When you’re done, the button’s attributes should look like this:
Ensure your new button is selected, then click the Add New Constraints button in the layout bar at the foot of the storyboard canvas and set the constraints as below:
- Width should be 66
- Height should be 67
Click Add 2 Constrains. Your view should now look like the following:
Still in Main.storyboard, open the Assistant Editor and make sure LoginViewController.swift is showing.
Next, Control-drag from the button you just added to LoginViewController.swift, just below the other IBOutlet
s, like so:
In the popup enter touchIDButton
as the Name and click Connect:
This creates an IBOutlet
you’ll use to hide the button on devices that don’t have biometric ID available.
Next, you need to add an action for the button.
Control-drag from the same button to LoginViewController.swift to just above checkLogin(username:password:)
:
In the popup, change Connection to Action, set Name to touchIDLoginAction, set Arguments to none for now. Then click Connect.
Build and run to check for any errors. You can still build for the Simulator at this point since you haven’t yet added support for biometric ID. You’ll take care of that now.
Adding Local Authentication
Implementing biometric ID is as simple as importing the Local Authentication framework and calling a couple of simple yet powerful methods.
Here’s what the Local Authentication documentation has to say:
“The Local Authentication framework provides facilities for requesting authentication from users with specified security policies.”
The specified security policy in this case will be your user’s biometrics — A.K.A their face or fingerprint! :]
New in iOS 11 is support for Face ID. LocalAuthentication
adds a couple of new things: a required FaceIDUsageDescription and a LABiometryType
to determine whether the device supports Face ID or Touch ID.
In Xcode’s Project navigator select the project and click the Info tab. Hover over the right edge of one of the Keys and click the +. Start typing “Privacy” and from the pop up list that appears select “Privacy – Face ID Usage Description” as the key.
The type should be a String. In the value field enter We use Face ID to unlock the notes.
In the Project navigator right-click the TouchMeIn group folder and select New File…. Choose iOS\Swift File. Click Next. Save the file as TouchIDAuthentication.swift with the TouchMeIn target checked. Click Create.
Open TouchIDAuthentication.swift and add the following import just below import Foundation
:
import LocalAuthentication
Next, add the following to create a new class:
class BiometricIDAuth {
}
Now you’ll need a reference to the LAContext
class.
Inside the class add the following code between the curly braces:
let context = LAContext()
The context
references an authentication context, which is the main player in Local Authentication. You’ll need a function to see if biometric ID is available to the user’s device or in the Simulator.
Add the following method to return a Bool
if biometric ID is supported inside BiometricIDAuth
:
func canEvaluatePolicy() -> Bool {
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
}
Open LoginViewController.swift and add the following property to create a reference to BiometricIDAuth
.
let touchMe = BiometricIDAuth()
At the bottom of viewDidLoad()
add the following:
touchIDButton.isHidden = !touchMe.canEvaluatePolicy()
Here you use canEvaluatePolicy(_:)
to check whether the device can implement biometric authentication. If so, show the Touch ID button; if not, leave it hidden.
Build and run on the Simulator; you’ll see the Touch ID logo is hidden. Now build and run on your physical Face ID/Touch ID-capable device; you’ll see the Touch ID button is displayed. In the Simulator you can choose Touch ID > Enrolled from the Hardware menu and test the button.
Face ID or Touch ID
If you’re running on an iPhone X or later Face ID equipped device you may notice a problem. You’ve taken care of the Face ID Usage Description, and now the Touch ID icon seems out of place. You’ll use the biometryType enum to fix this.
Open, TouchIDAuthentication.swift and add a BiometricType
enum above the class.
enum BiometricType {
case none
case touchID
case faceID
}
Next, add the following function to return which biometric type is supported using the canEvaluatePolicy
.
func biometricType() -> BiometricType {
let _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
switch context.biometryType {
case .none:
return .none
case .touchID:
return .touchID
case .faceID:
return .faceID
}
}
Open, LoginViewController and add the following to the bottom of viewDidLoad()
to fix the button’s icon.
switch touchMe.biometricType() {
case .faceID:
touchIDButton.setImage(UIImage(named: "FaceIcon"), for: .normal)
default:
touchIDButton.setImage(UIImage(named: "Touch-icon-lg"), for: .normal)
}
Build and run on the Simulator with Touch ID Enrolled to see the Touch ID icon; you’ll see the correct icon is shown on the iPhone X – the Face ID icon.
Putting Touch ID to Work
Open, TouchIDAuthentication.swift and add the following variable below context
:
var loginReason = "Logging in with Touch ID"
The above provides the reason the application is requesting authentication. It will display to the user on the presented dialog.
Next, add the following method to the bottom of BiometricIDAuth
to authenticate the user:
func authenticateUser(completion: @escaping () -> Void) { // 1
// 2
guard canEvaluatePolicy() else {
return
}
// 3
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
localizedReason: loginReason) { (success, evaluateError) in
// 4
if success {
DispatchQueue.main.async {
// User authenticated successfully, take appropriate action
completion()
}
} else {
// TODO: deal with LAError cases
}
}
}
Here’s what’s going on in the code above:
authenticateUser(completion:)
takes a completion handler in the form of a closure.- You’re using
canEvaluatePolicy()
to check whether the device is capable of biometric authentication. - If the device does support biometric ID, you then use
evaluatePolicy(_:localizedReason:reply:)
to begin the policy evaluation — that is, prompt the user for biometric ID authentication.evaluatePolicy(_:localizedReason:reply:)
takes a reply block that is executed after the evaluation completes. - Inside the reply block, you are handling the success case first. By default, the policy evaluation happens on a private thread, so your code jumps back to the main thread so it can update the UI. If the authentication was successful, you will call the segue that dismisses the login view.
You’ll come back and deal with errors in little while.
Open, LoginViewController.swift, locate touchIDLoginAction(_:)
and replace it with the following:
@IBAction func touchIDLoginAction() {
touchMe.authenticateUser() { [weak self] in
self?.performSegue(withIdentifier: "dismissLogin", sender: self)
}
}
If the user is authenticated, you can dismiss the Login view.
Go ahead and build and run to see if all’s well.
Dealing with Errors
Wait! What if you haven’t set up biometric ID on your device? What if you are using the wrong finger or are wearing a disguise? Let’s deal with that.
An important part of Local Authentication is responding to errors, so the framework includes an LAError
type. There also is the possibility of getting an error from the second use of canEvaluatePolicy
.
You’ll present an alert to show the user what has gone wrong. You will need to pass a message from the TouchIDAuth
class to the LoginViewController
. Fortunately you have the completion handler that you can use it to pass the optional message.
Open, TouchIDAuthentication.swift and update the authenticateUser
method.
Change the signature to include an optional message you’ll pass when you get an error.
func authenticateUser(completion: @escaping (String?) -> Void) {
Next, find the // TODO:
and replace it with the following:
// 1
let message: String
// 2
switch evaluateError {
// 3
case LAError.authenticationFailed?:
message = "There was a problem verifying your identity."
case LAError.userCancel?:
message = "You pressed cancel."
case LAError.userFallback?:
message = "You pressed password."
case LAError.biometryNotAvailable?:
message = "Face ID/Touch ID is not available."
case LAError.biometryNotEnrolled?:
message = "Face ID/Touch ID is not set up."
case LAError.biometryLockout?:
message = "Face ID/Touch ID is locked."
default:
message = "Face ID/Touch ID may not be configured"
}
// 4
completion(message)
Here’s what’s happening
- Declare a string to hold the message.
- Now for the “failure” cases. You use a
switch
statement to set appropriate error messages for each error case, then present the user with an alert view. - If the authentication failed, you display a generic alert. In practice, you should really evaluate and address the specific error code returned, which could include any of the following:
LAError.biometryNotAvailable
: the device isn’t Face ID/Touch ID-compatible.LAError.passcodeNotSet
: there is no passcode enabled as required for Touch IDLAError.biometryNotEnrolled
: there are no face or fingerprints stored.LAError.biometryLockout
: there were too many failed attempts.
- Pass the message in the
completion
closure.
iOS responds to LAError.passcodeNotSet
and LAError.biometryNotEnrolled
on its own with relevant alerts.
There’s one more error case to deal with. Add the following inside the else
block of the guard
statement, just above return
.
completion("Touch ID not available")
The last thing to update is the success case. That completion should contain nil
, indicating that you didn’t get any errors. Inside the first success block add the nil
.
completion(nil)
Once you’ve completed these changes your finished method should look like this:
func authenticateUser(completion: @escaping (String?) -> Void) {
guard canEvaluatePolicy() else {
completion("Touch ID not available")
return
}
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
localizedReason: loginReason) { (success, evaluateError) in
if success {
DispatchQueue.main.async {
completion(nil)
}
} else {
let message: String
switch evaluateError {
case LAError.authenticationFailed?:
message = "There was a problem verifying your identity."
case LAError.userCancel?:
message = "You pressed cancel."
case LAError.userFallback?:
message = "You pressed password."
case LAError.biometryNotAvailable?:
message = "Face ID/Touch ID is not available."
case LAError.biometryNotEnrolled?:
message = "Face ID/Touch ID is not set up."
case LAError.biometryLockout?:
message = "Face ID/Touch ID is locked."
default:
message = "Face ID/Touch ID may not be configured"
}
completion(message)
}
}
}
Open LoginViewController.swift and update the touchIDLoginAction(_:)
to look like this:
@IBAction func touchIDLoginAction() {
// 1
touchMe.authenticateUser() { [weak self] message in
// 2
if let message = message {
// if the completion is not nil show an alert
let alertView = UIAlertController(title: "Error",
message: message,
preferredStyle: .alert)
let okAction = UIAlertAction(title: "Darn!", style: .default)
alertView.addAction(okAction)
self?.present(alertView, animated: true)
} else {
// 3
self?.performSegue(withIdentifier: "dismissLogin", sender: self)
}
}
}
Here’s what you’re doing in this code snippet:
- You’ve updated the trailing closure to accept an optional message. If biometric ID works, there is no message.
- You use
if let
to unwrap the message and display it with an alert. - No change here, but if you have no message, you can dismiss the Login view.
Build and run on a physical device and test logging in with Touch ID.
Since LAContext
handles most of the heavy lifting, it turned out to be relatively straight forward to implement biometric ID. As a bonus, you were able to have Keychain and biometric ID authentication in the same app, to handle the event that your user doesn’t have a Touch ID-enabled device.
Look Mom! No Hands.
One of the coolest things about the iPhone X is using Face ID without touching the screen. You added a button which you can use to trigger the Face ID, but you can trigger Face ID automagically as well.
Open LoginViewController.swift and add the following code right below viewDidLoad()
:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let touchBool = touchMe.canEvaluatePolicy()
if touchBool {
touchIDLoginAction()
}
}
The above will verify if biometric ID is supported and if so try and authenticate the user.
Build and run on an iPhone X or Face ID equipped device and test logging in hands free!
Where to Go from Here?
You can download the completed sample application from this tutorial here.
The LoginViewController
you’ve created in this tutorial provides a jumping-off point for any app that needs to manage user credentials.
You can also add a new view controller, or modify the existing LoginViewController
, to allow the user to change the password from time to time. This isn’t necessary with biometric ID, since the user’s biometrics probably won’t change much in their lifetime! :] However, you could create a way to update the Keychain; you’d want to prompt the user for their current password before accepting their modification.
Apple also recommends hiding the username and password fields and login button when using Face ID. I’ll leave that for you as a simple challenge.
You can read more about securing your iOS apps in Apple’s official iOS Security Guide.
As always, if you have any questions or comments on this tutorial, feel free to join the discussion below!
The post How To Secure iOS User Data: The Keychain and Biometrics – Face ID or Touch ID appeared first on Ray Wenderlich.