Streaming is available in most browsers,
and in the Developer app.
-
Streamline local authorization flows
Discover how you can use the latest authorization-focused APIs in LocalAuthentication to protect the privacy and security of people's data. We'll show you how LocalAuthentication can authorize access to secrets, keys, and other sensitive resources in your app, all while reducing complexity and relying on the security and usability of common local authentication methods such as Touch ID and Face ID.
Resources
-
Download
♪ ♪ Felix Acero: Hi, my name is Felix Acero, and I am a Software Engineer with the Security Engineering and Architecture team. In this video, I am going to show you how you can use the LocalAuthentication framework to improve the authentication and authorization flows of your app. We will start by taking a look at the generic concepts of authentication and authorization and how they apply to your application. Then we will review how the existing LocalAuthentication API, and in particular the LAContext, can help you implement a wide range of authorization schemes. And finally we will see how the new APIs we are adding to LocalAuthentication this year can help you further streamline your authorization code.
So let's start by talking about authentication and authorization. Authentication and authorization are distinct yet closely related security concepts. On the one hand, authentication is the act of verifying the identity of the user. On the other hand, authorization is the act of verifying whether a given user is allowed to perform a specific operation on a concrete resource. Put together, we can see that, since we first need to verify that the user is who they claim to be before we can evaluate what resources and operations are available to them, we can say that authentication in fact enables authorization. To help illustrate these concepts, let's look at a concrete example involving a common security resource managed by your applications, such as Secure Enclave keys.
Secure Enclave keys are special types of asymmetric keys that are bound to a specific device and which are protected by a hardware-based key manager that is isolated from the main processor. What makes these keys special is that when you store a private key in the Secure Enclave, you never actually handle the key but instead instruct the Secure Enclave to perform operations with it. Secure Enclave keys can be associated with access control lists or ACLs for short. An access control list specifies the requirements that need to be satisfied in order to perform specific operations such as signing or decrypting a blob.
They can specify when a given item is available, for example after device unlock, as well as the authentication requirements needed to allow the execution of certain operations. For this example, let's say that your app wants to protect the sign and decrypt operations of its key with biometric authentication, while also ensuring that the key is only available after the device has been unlocked.
Now let's see how an authorization flow would look like for a sign operation involving this key.
First, your application issues a request to sign a blob using the private key.
Then, after verifying that your application can access the key, the system proceeds to identify the authorization requirements for the sign operation. In this case, the sign operation requires a successful biometric authentication from any of the currently enrolled users. The system will then walk the user through the biometric authentication process via the standard UI. Upon a successful authentication, the system verifies that all the remaining authorization requirements have been satisfied before finally performing the sign operation and returning a signed blob to your app.
Let's break down the main components involved in this flow to see how they fit into our initial definitions. First, we have a resource: the Secure Enclave key. Second, we have an operation that can be performed with the key. And third, we have a set of requirements that, among other things, specify who is allowed to perform the operation as well as the means of authentication that should be used to verify their identity. Plugging the parameters of this example into our definitions, we can see that for authentication, the question of whether this is the right user is answered by means of a biometric authentication; while for authorization, the question of whether the user is allowed to perform a signature operation using the private key is answered by verifying the requirements specified in the access control list. Now that we have seen how this works at a high level, we can take a look at the way a flow like this can be implemented using the current API of LocalAuthentication. Let's start by quickly reviewing the features offered by the LAContext, which is one of the core components of the framework. An LAContext can be used to evaluate the user's identity. It handles user interaction when biometric or passcode authentication is required. And it also interfaces with the Secure Enclave to enable the secure management of biometric data. From this perspective, the LAContext can be used to support your authentication use cases. The LAContext can also be used in association with other frameworks to support authorization flows. For instance, you could use it to evaluate access control lists like the one we saw in our previous example. Let's take a closer look. The first thing we need to do is to get access the ACL associated with our private key. We can do this with the help of the SecItemCopyMatching API offered by the Security framework, making sure that we provide the return-attributes key inside of our query.
Once we obtain access to the access control list, we can evaluate it directly using the LAContext and the evaluateAccessControl API. The biggest advantage that this approach gives you is that it lets you decide the right moment and the right place in your application to prompt the user for this authorization. In this case, since the access control list requires biometric authentication for the signature operation, the LAContext will present the familiar Face ID or Touch ID UI.
Once the ACL has been authorized inside our LAContext, we will be able to use it as part of our query for obtaining a reference to our key. We do this by appending the LAContext to our SecItem query under the use-authentication-context key.
By binding the LAContext to our private key reference, we ensure that executing the signature operation will not trigger another authentication, while allowing the operation to continue without unnecessary prompts. These binding also means that no additional user interactions will be required for future signatures until the LAContext is invalidated.
The LAContext offers a great deal of flexibility and it lets you control each of the steps and parameters involved in your authorization flows. It can be used in combination with other frameworks such as the Security framework, which in turn unlocks a wide range of use cases. This versatility, however, comes at the cost of higher code complexity, requiring you to carefully orchestrate the APIs offered by several frameworks. Depending on your use case, the LAContext might be the right tool for you, especially if the main value proposition of your app requires low-level access to keys, secrets, contexts, and access control lists. However, if all you need for your app is a way of authorizing access to content or a sensitive resource, then you may want to trade off some of this flexibility for a simpler API. This brings us to our last topic, streamline your app. New to iOS 16 and macOS 13, LocalAuthentication is introducing a higher level, authorization focused API. The new API builds on top of existing concepts in LocalAuthentication such as the LAContext and is geared towards simplifying the implementation of common authorization flows, allowing you to focus all your energy in the core value proposition of your apps. The most important abstraction introduced by the new API is the LARight.
The simplest use case you can give a LARight is to help you authorize operations on application defined resources. For instance, you could use a right to help you gate access to the user profile section of your application by first requiring a successful biometric authentication from your user.
By default, rights are protected by a set of authentication requirements that allow your users to authenticate using Touch ID, Face ID, Apple Watch, or their device passcode depending on the device they are using.
You can also choose to associate your rights with more granular requirements, which allow you to further constrain the means of authentication. Let's have a look at how we can use LARights in code.
The first thing that we need to do is to instantiate our right. We do this by specifying its requirements. In this case, our login right will require users to authenticate using biometry or providing the device passcode. We then proceed to verify that the current user can obtain the login right. We use this information to determine whether we can continue with the login operation or if instead we need to redirect the user to the public section of our app. Finally, we can proceed with the actual authorize operation providing a localized reason that will be visible to the user in the authorization UI.
When authorizing a right in this way, a brand-new, system-driven UI is presented. The UI is rendered inside your application window and provides users with relevant information to help them understand the origin and purpose of the operation. We believe that the new look will allow you to craft authorization flows that integrate more seamlessly with your application and that provide more context and information for your users.
Now that we have seen how to create and authorize a right, let's take a closer look at its lifecycle. Rights start their lifecycle in an unknown state. As soon as your application issues the authorize request, the state of the right changes to authorizing. It is at this point that the user will be presented with the authorization UI that we saw in the previous slide.
Depending on the success or failure of the operation, the right may transition to authorized or notAuthorized state. This is the most important state transition for your application. Finally, the right can also move from the authorized to the notAuthorized state. This occurs when your application explicitly issues a deauthorize request on the right, or when the right instance is deallocated.
Be sure to keep a strong reference to your right in order to preserve its authorized state.
After a right has been deauthorized, your application can continue to issue authorization requests to restart the cycle. All the previous state transitions can be queried and observed. If you have access to the LARight instance, you can query its state property directly. You can also observe all state transitions using KVO or Combine. Additionally, if your application handles several rights, you can observe the state of all them from a single place by listening to the didBecomeAuthorized and the didBecomeUnauthorized notifications, which are published to the default NotificationCenter after a change in the authorization state is detected.
Before we move on, let's jump back to our example and add a logout operation to deauthorize our login right. By doing this, we guarantee that a new authorization will be required the next time the user wants to log in.
So far, we have seen how to use right instances to authorize operations on application-defined resources. We have also seen how the lifecycle and state of these rights is ultimately tied to the runtime, which means that on every session of your application, you need to instantiate and configure these rights correctly. So let's take a look at how rights can be persisted and what sort of possibilities this enables for your app.
LARights can be persisted with the help of the right store.
When persisted, rights are backed by a unique Secure Enclave key that is protected with an access control list or ACL that matches the authorization requirements of the right. This approach helps us ensure that the authorization requirements will remain immutable after the right has been persisted.
You can also access the private key that backs your right and use it to perform protected cryptographic operations such as decryption, signature, and key exchanges.
The corresponding public key is also accessible and can be used to perform operations such as encryption and signature verification. Because this is a public key, you can also export the bytes associated with it.
Private key operations are only allowed after the right has been successfully authorized. In contrast, public key operations are always allowed.
When persisting your right, you also have a chance of storing a single, immutable secret along with it. The secret is also associated with an access control list that matches the authorization requirements of your right and it only becomes accessible after the right has been authorized.
To summarize, LAPersistedRights are created with the help of the right store. They are configured only once and their authorization requirements are immutable. Because they are stored, they can be used across different sessions of your application. Internally, they are bound to a specific device and are backed by a unique Secure Enclave key which can be used to perform different cryptographic operations, depending on the authorization status of the right. Finally, they can be used to protect a single, immutable secret that only becomes available after the right has been authorized.
Now that we understand some of the features offered by persisted rights, let's see how they can help us implement the scenario we discussed at the beginning of the presentation, where we wanted to authorize a signature operation.
We start by instantiating a regular right specifying its authorization requirements. In this case, we want to ensure that the right would only be granted to users that have biometric enrollments in the device at the moment of the creation of our right. Therefore, we use the biometryCurrentSet requirement.
We can then persist the right with the help of the right store, providing a unique identifier. This identifier will be useful the next time we need to fetch the right in future sessions of our application.
Once the right is persisted, we get immediate access to its public key and can start performing unprotected operations with it, without the need for an explicit authorization. In this example, we are simply exporting its public bytes.
Later on, when it's time to perform a signature operation, we can retrieve our right from the store using the unique identifier we provided during creation. We can then proceed to authorize the current user through the authorize operation on our right. At this point, the system will walk the user through the authentication process and will verify that all the authorization requirements are satisfied.
After the right has been authorized, we can use its private key to perform protected cryptographic operations. In this case, we are using the private key to sign a challenge issued by the backend server of our application.
To wrap up, we talked about the relationship that exists between the generic concepts of authentication and authorization, and particularly how authentication enables authorization. We went over some of the features offered by the LAContext and how it can be combined with frameworks such as Security to unlock very powerful and extensible authorization flows. And finally, we looked into how the newly added LARight could help you streamline the code to implement certain authorization use cases. We invite you take a look at existing usages of LocalAuthentication in your app and consider whether some of the features we discussed today can help you simplify your code while still protecting the privacy and security of your users. Thanks.
-
-
4:58 - LAContext (authorize a signature operation 1)
let query: [String: Any] = [ kSecClass as String: kSecClassKey, kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, kSecAttrApplicationTag as String: "com.example.app.key", kSecReturnAttributes as String: true, ] var item: CFTypeRef? = nil guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, let attrs = item as? NSDictionary, let accessControl = attrs[kSecAttrAccessControl] else { throw .aclNotFound }
-
5:15 - LAContext (authorize a signature operation 2)
let context = LAContext() try await context.evaluateAccessControl(accessControl as! SecAccessControl, operation: .useKeySign, localizedReason: "Authentication is required to proceed")
-
5:44 - LAContext (authorize a signature operation 3)
let query: [String: Any] = [ kSecClass as String: kSecClassKey, kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, kSecAttrApplicationTag as String: "com.example.app.key", kSecReturnRef as String: true, kSecUseAuthenticationContext as String: context ] var item: CFTypeRef? = nil guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, item != nil else { throw .keyNotFound }
-
6:00 - LAContext (authorize a signature operation 4)
let privateKey = item as! SecKey var error: Unmanaged<CFError>? guard let sgt = SecKeyCreateSignature(privateKey, self.algorithm, blob, &error) as Data? else { throw .signatureFailure }
-
8:28 - LA Right (basic usage)
// LARight: Basic usage func login() async { self.loginRight = LARight(requirement: .biometry(fallback: .devicePasscode)) do { try await loginRight.checkCanAuthorize() } catch { navigateTo(section: .public) return } do { try await self.loginRight.authorize(localizedReason: self.localizedReason) navigateTo(section: .protected) } catch { showError(.authenticationRequired) } }
-
11:01 - LARight (logout and deauthorization)
// LARight: Basic usage func login() async { self.loginRight = LARight(requirement: .biometry(fallback: .devicePasscode)) // ... do { try await self.loginRight.authorize(localizedReason: self.localizedReason) navigateTo(section: .protected) } catch { showError(.authenticationRequired) } } func logout() async { await self.loginRight.deauthorize() }
-
13:44 - LAPersistedRight
// LAPersistedRight: Retrieval and private key usage func generateClientKeys() async throws -> Data { let login2FA = LARight(requirement: .biometryCurrentSet) let persisted2FA = try await LARightStore.shared.saveRight(login2FA, identifier: "2fa") return try await persisted2FA.key.publicKey.bytes } func signChallenge(_ challenge: Data, algorithm: SecKeyAlgorithm) async throws -> Data { let persisted2FA = try await LARightStore.shared.right(forIdentifier: "2fa") let localizedReason = "Biometric authentication is required to proceed" try await persisted2FA.authorize(localizedReason: localizedReason) return try await persisted2FA.key.sign(challenge, algorithm: algorithm) }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.