Touch ID for Wealthfront App

At Wealthfront, our clients count on us to provide them with delightful financial services built with leading technology. They have chosen to trust us with some of their most important financial needs and keeping their money and data secure is of the utmost importance to us. When we released the Wealthfront iOS App in February we required our clients to login to the app if it had been inactive for more than 15 minutes, causing many of them to enter their full password multiple times each day. We soon pushed out our PIN unlock feature to allow them to view their data in the app with a four digit PIN. When a client needs “privileged” access, for example scheduling a deposit, the app still requires their password. This way we can ensure security around sensitive events while providing greater convenience for everyday use.

As you can see from the graph below, within a week after our PIN unlock feature went live, more than 75% of clients were actively using it. To this day it remains a highly utilized feature.

 

One of the exciting new features Apple announced at this year’s WWDC allows developers to use biometric-based authentication, or Touch ID, right within their apps. Touch ID is very secure because fingerprints are saved on the device in a security co-processor called the Secure Enclave. It handles all Touch ID operations and, in addition, it handles cryptographic operations used for keychain access. The Secure Enclave guarantees data integrity even if the kernel is compromised. For this reason, all device secrets and passcode secrets are stored in the Secure Enclave.

Touch ID makes authenticating with an application even easier than our PIN feature while providing additional layers of security. From the day it was announced we’ve wanted to use Touch ID to allow our clients to authenticate with the Wealthfront app. Apple provides two mechanisms for us to integrate Touch ID:

  1. Use Touch ID to access credentials stored in the keychain
  2. Use Touch ID to authenticate with the app directly (called Local Authentication)

We carefully built test apps to compare each of these two approaches and today we’ll examine our thought process and how we chose which mechanism best suited our needs.

Decide which Touch ID mechanism to use

The following is a diagram adapted from WWDC 2014 Session 711 to compare the two authentication mechanisms:

The biggest differences between keychain access and local authentication are:

  • Keychain access
    • The Keychain is protected with the user’s passcode, it is also protected with a unique secret built into each device only known to that device. If the keychain is removed from the device, it is not readable
    • The Keychain can be used to store a user’s full credentials (e.g. email and password) on device encrypted by the Secure Enclave that can be unlocked based on authentication policy evaluation:
      • If a device does not have a passcode set, the Secure Enclave is locked and there is no way to access any information stored in it
      • If a device has a passcode, the Secure Enclave can be unlocked by the passcode only
      • If a device has Touch ID as well, the preferred method is to authenticate with Touch ID and passcode is the backup mechanism
      • No other fallback mechanism is permitted and Apple does not allow customization of the fallback user interface
  • LocalAuthentication
    • Any application can directly call LocalAuthentication for Touch ID verification
    • No permission is granted to store secrets into or retrieve secrets from the Secure Enclave
    • Contrary to the keychain access case, Apple does not allow device passcode authentication as a backup
    • Every application needs to provide its own fallback to handle failed Touch ID case with custom UI

We had one major concern about storing sensitive information in the keychain, the only fallback for failing to authenticate with Touch ID is the device passcode. iOS users usually configure a four digit passcode, which we feel is less secure than their account password. Apple, for example, uses your iCloud account password as the fallback mechanism if you are trying to make a purchase on the iTunes store and fail to successfully authenticate with Touch ID. If we authenticate with Touch ID via LocalAuthentication, we can use our PIN unlock feature or the client’s password as the fallback mechanism. We still don’t store the password on the device, failure to authenticate with Touch ID requires full authentication with Wealthfront’s servers if the device does not have a Wealthfront PIN configured. Furthermore, any “privileged” access still requires a password. We feel this represents the best compromise between security and convenience. Now lets take a closer look at how we implemented our integration with Touch ID.

Integrating Touch ID Through Local Authentication

Integrating Touch ID into an application is a two step process:

  1. We ask if the device supports Touch ID by calling -canEvaluatePolicy:error:
  2. We call -evaluatePolicy:localizedReason:reply: to display the Touch ID alert view, it will call our reply block

Let’s take a closer look at how we use these methods in our Wealthfront application in Touch ID authentication.

Check if Touch ID is available

The follow code fragment is a simplified version from our production code.

  • Lines 5-7: To set things up, we create an instance of LAContext that will be used for Touch ID authentication.
  • Lines 9-26: We use the -canEvaluatePolicy:error: API to see if the device can use Touch ID. If we get a YES back, we know the device is capable of evaluating the LAPolicyDeviceOwnerAuthenticationWithBiometrics policy. We will invoke the second API method (below) to request a fingerprint match. If the return value is NO, we will check the error code and generate a new localError to send back to the caller. Instead of using the LAError domain, we generate our own WFTouchIDErrorDomain and error code (see below for reasons) to propagate error message back to the caller.
  • Line 28: Here we call a method in another class to check if the user has opted out of using Touch ID.
  • Lines 29-34: Again we use our own WFTouchIDErrorDomain and error code so the caller method can parse it to get the error message.

 

Authenticate with Touch ID

If the above check result is YES, we can now proceed to the second step by calling -evaluatePolicy:localizedReason:reply:.

  • Lines 4-6: Here we first confirm the fallbackButtonTitleString, successBlock, and fallbackBlock are not nil.
  • Lines 7-9: We create a new LAContext object if it is nil.
  • Line 11: We pass a fallbackButtonTitleString as the fallback button title. This one cannot be nil, passing nil causes an exception which could crash the app.
  • Line 12: The reasonString is also required because Touch ID operations require this string to tell the user why we are requesting their fingerprint.
  • Lines 13-26: We pass the reasonString, successBlock, and fallbackBlock to -evaluatePolicy:localizedReason:reply:. The replyBlock will be passed a BOOL indicating whether or not the authentication attempt was successful. If the reply is a YES we can now proceed with the successBlock. Otherwise, we pass the error to fallbackBlock so it can check the error code to find out the reason for failure and act accordingly.

We can only call -evaluatePolicy:localizedReason:reply: when the app is in foreground. As soon as we make the call, we will see the Touch ID alert view to prompt the user to scan their registered finger.

It is very important to use dispatch_async to dispatch the successBlock and fallbackBlock back to the main queue for UI update. Otherwise the app will freeze for a long time since the -evaluatePolicy:localizedReason:reply: appears to be using a XPC service (no document from Apple to say so, but we saw evidence of this in Instruments). The UI would be updated only after the XPC service gives back control to the main queue.

Customize LAError error code for iOS 7 devices

When we were working with the iOS8 beta, we also needed to maintain compatibility with iOS 7. The problem is that LAContext, LAError are not available in iOS7 since the LocalAuthentication framework is new in iOS8. We also wanted to give the users the option to opt out of Touch ID operations. Moreover, we rely heavily on automatic testing on both devices and simulators. LAContext gives an undocumented -1000 error code if we try to call the methods on a simulator. In order to cope with every possibility listed above, we made a custom NS_ENUM called WFTouchIDError and use the userInfo dictionary to describe any error.

For example, if the user opts out of Touch ID, we use WFTouchIDOptOut so that the caller can behave accordingly. In our unit tests, we can also use the WFTouchIDNotAvailable and userInfo to tell the simulator not to fail on a test since Touch ID is not supported.

Testing

At Wealthfront, testing is in our blood; no code is shipped without test coverage. The following code snippet test -authenticateByTouchIDWithFallbackButtonTitle:success:fallback:

This test follows a similar testing paradigm as I described in more detail in a previous blog. Briefly, we use the OCMock framework to decompose code into testable components. By isolating the code from its outside dependencies, we are able to make sure the code behaves as we expect from the bottom up. In this test, we use OCMock‘s andDo: block to make sure only one of the successBlock and fallbackBlock blocks is called.

  • Lines 2-6: Here we mock out LAContext and set the mocked object mockLAContext as _authManager‘s localAuthContext. Then we call out LAContext‘s expected -setLocalizedFallbackTitle: method with the expected calling parameter:@"Wealthfront PIN" as it will be used to set the fallback button title.
  • Lines 8-15: We spell out another anticipated LAContext call with expected parameters. We make sure reply: needs to be valid by setting the expectation as [OCMArg isNotNil] so we can execute the void(^reply)(BOOL success, NSError *error) block inside the andDo: block.
  • Lines 17-25: We call -authenticateByTouchIDWithFallbackButtonTitle:success:fallback: with a simple successBlock in which the BOOL variable value is changed and we make sure the fallbackBlock is not called by inserting a XCTFail inside the fallbackBlock.
  • Lines 27-30: We make sure that there is an exception thrown if we try to set the fallbackButtonTitle to be nil.

 

Final Thoughts

At Wealthfront, our mission is to bring the best and latest technology to our clients and thereby improve their experience. In iOS 8 Apple has finally provided a public API for us to leverage Touch ID in our applications. This allows us to deliver greatly enhanced convenience to clients without sacrificing security. We carefully considered the implications of adopting the Touch ID mechanism. Direct interaction with LocalAuthentication gives our clients the best experience and greatest security. It was very important to support Touch ID as soon as possible to give our users a delightful experience. We leveraged our continuous integration infrastructure to both validate our integration as well as verify that Apple had fixed bugs we discovered during the beta. This allowed us to be ready to put a build in the App Store within a few hours of Apple releasing the iOS 8 GM build.