-
Notifications
You must be signed in to change notification settings - Fork 49
Implement: Front End Requirements
General comments:
For the user flows I used existing videos from Yubico's resources - Feel free to replace them with something more generic, as some of them go through security key flows
The implementation guidance below is web focused; I'd need others to contribute if we want guidance for Android/iOS
This page outlines the requirements for adding passkey support to a client application. This will help developers understand what components are required in order to ensure that users are able to successfully leverage passkeys from an application; regardless of platform or ecosystem. This section will then provide general implementation guidance, that will provide a starting point for developers who are ready to add passkey support to their client application.
This section will outline general requirements that need to be considered before attempting to add passkey support to your application.
Utilizing passkeys on an application requires that the user has a WebAuthn authenticator available for use. A WebAuthn authenticator does not need to come in the form of specialty hardware, and may already be included in most mainstream consumer mobile devices. Different modalities could include:
- Laptop or mobile phone utilizing the on-device security module
- Security key
- Software implementation, such as a password manager
All of these modalities should be communicating with your client application through the WebAuthn protocol; this will allow you to support different authenticators without requiring special configurations.
As a developer you should understand that there are differences when supporting passkeys across different application types. Different application types will require different implementations based on the platform being developed against.
Your first step should be to understand which platforms (operating systems) your application exists on, and ensure that it supports passkeys. Click here to view the passkey support matrix.
Support for web applications will be determined by the WebAuthn support on the browser being leveraged by the user. Passkey support can be determined by identifying if a browser supports WebAuthn with discoverable credentials. Click here to view the WebAuthn browser support matrix.
If your browser supports WebAuthn, then you should be able to leverage the navigator.credentials.create
and navigator.credentials.get
methods, as demonstrated in the web implementation guidance provided below. These methods will be standard across all browsers that support WebAuthn.
Both of the major consumer mobile operating systems support passkeys, as noted in the passkey support matrix. In order to support passkeys on your mobile application, you will need to leverage the platform’s specific APIs to access WebAuthn functionality. The experience will be the same for users leveraging passkeys from a web application, the major difference will be in the implementation.
A relying party is the backend application that supports your passkey features; specifically the registration, storage, and authentication operations.
Guidance for implementing a relying party can be found in this guide on this page (//TODO insert link to this page here).
This section will outline the primary flows that are required for supporting passkeys in your application.
The registration flow is what will allow a user to create a new passkey for their account. In our scenario, we will assume that the user has already authenticated into their account through some form of traditional authentication mechanism.
The user is in their account management screen, where they are shown an option to add passkeys to their account.
The video below will demonstrate a user registering a passkey using the authenticator on their personal device
reg-touchid.mp4
The authentication flow is what will allow a user to use a passkey to access their account. In our scenario, we will assume that the user has already registered a passkey to their account.
The user will load into your application, and be presented with a login screen.
It's important to note that there are a few different flows that a user can use in order to authenticate using a passkey. The videos below will demonstrate each flow, with implementation guidance being offered in the next section.
We will start with a method that is familiar to users, autofill. In the current paradigm, autofill is used to insert both a username, and password, so that a user does not need to manually input or remember their credentials. So how does this apply to passkeys?
For passkey autofill, the user is presented with a list of possible credentials tied to their account for a webpage, similar to what has been done traditionally for passwords. The user selects their credential (for passkeys on their phone/laptop), or they can attempt to use a passkey on an external authenticator.
The video below demonstrates how to use a passkey on your phone/laptop autofill.
auth-touchid.mp4
There is another authentication flow that can be leveraged for passkeys. You can take advantage of the traditional "modal" experience that has existed in browsers/platforms for some time. There is almost no difference between this flow, and autofill, other than how they are invoked. The user will still be prompted for their credential information in similar ways.
The first video demonstrates how to use a passkey on a security key, without providing a username to the service.
auth-modal-input.mp4
The next video demonstrates how this same flow can occur, even without the presence of an input field for a username
auth-modal-sec-key.mp4
MODAL VS AUTOFILL
It should be noted that autofill for passkeys was created as a way to encourage the use of passkeys by providing a mental model that is familiar to non-technical users. It is our recommendation to leverage autofill for your initial implementation to increase the adoption of passkeys among your users.
With that said, there should be some eventual transition to the use of the "button-only" flow as a username input field will no longer be needed in a world where everyone is leveraging a passkey. The presence of a username field also helps users who have authenticators that don't support the creation of discoverable credentials, so also ensure that you understand the authenticators being used by your users.
Credential management will allow a user to manage the passkeys associated with their account. The following features should be supported:
- Deletion of a passkey
- Editing user defined metadata of a passkey
Deletion of a passkey will allow a user to remove a passkey from their account, meaning that the specific passkey can no longer be utilized to access an application. This can help in scenarios where a user's cloud account is compromised, or if a user’s device is stolen.
Note, there should be mechanisms to ensure that a user does not delete a passkey if it is the only one associated with their device; if the user deletes their only passkey, then they will be unable to access their account. It should also be noted that deleting a passkey from an application will not remove the passkey from the device; the implication being that the passkey that still exists on the device will no longer be able to authenticate into the user’s account.
“Editing” of a passkey is very different from the current paradigm of editing a password. Editing of a passkey does not mean that the user is changing the credential itself. In fact, once a passkey is added to your relying party, the credential itself should never be changed. What could be allowed is changing metadata that may help users identify passkeys. For instance a user may be allowed to change the nickname given to a passkey, or “last used” field may be updated as a user uses a specific passkey. These will help with the user experience, and may help users further understand how passkeys are being used in their account.
This section will outline high level implementation guidance on how to invoke passkey flows from a web application. This guidance is meant to be general, and not an extensive guide on optimizing the user experience.
The method below will be used to facilitate the passkey registration ceremony. Note that this method will require a PublicKeyCredentialCreationOptions
item from your relying party.
const addNewPasskey = async (e) => {
try {
e.preventDefault();
/**
* Assume the username was set by a global variable
* when the user authenticated into their account
* getAttestationOptions is a helper method used to get
* a PublicKeyCredentialCreationOptions From your relying
* party. The username should correlate to the user currently authenticated
*/
const attestationOptions = await getAttestationOptions(username);
const makeCredentialResult = await navigator.credentials.create(attestationOptions);
/** sendAttestationResult is a helper method
* used to send the new passkey to the relying
* party for use by this user’s account
*/
await sendAttestationResult(makeCredentialResult);
} catch (e) {
throw new Error("Error with creating the passkey");
}
};
The user interface items for passkey registration is fairly simple; it's just a single button!
The user will click this button to begin the registration ceremony. The button will trigger an 'onClick' event that will handle the registration ceremony.
Below is an example of the button that can be used to trigger the registration ceremony.
<button onClick="{addNewPasskey}">Add a new passkey</button>
This first section will highlight how to implement a usernameless flow for passkeys.
We will start by declaring a method that will be used to authenticate using passkeys.
const authenticateUser = async () => {
try {
/**
* No username is required to get the assertion options
*/
const assertionOptions = await getAssertionOptions();
/**
* Attempt to get the assertion using the auth
* options
*/
const assertionResult = await navigator.credentials.get(assertionOptions);
/**
* Send the assertion to the RP
* Assume the result of sendAssertionResult
* is an object with a property status,
* which will return ok if successful.
* Assume a status value that is not “ok” is a failed authentication
*/
const authenticationResult = await sendAssertionResult(
assertionResult
);
/**
* Validate that auth was successful
* Otherwise display a message
*/
if (authenticationResult.status === "ok") {
console.info("Authentication successful");
} else {
throw new Error("Auth failed");
}
} catch (e) {
console.error(e.getMessage());
}
};
The user interface items for usernameless passkey authentication is fairly simple; it's just a single button!
The user will click this button to begin the authentication ceremony. The button will trigger an 'onClick' event that will handle the authentication ceremony.
<Button onClick="{authenticateUser}">Add a new passkey</Button>
The use of autofill will require a few new mechanisms to the code introduced above, as there are some new properties and requirements that are non-standard to the traditional modal flows
In an autofill flow, the first thing that your client should do is check if autofill is available on your platform, more specifically your browser.
As autofill for passkeys is a relatively new feature, it may not yet be implemented in your platform of choice.
You can use the method below to verify if your browser has autofill. Note the use of the name conditional mediation, which is another term used to describe autofill in a passkey context.
/**
* This method call is a promise, be sure to utilize
* async in Javascript to get the response
*/
window.PublicKeyCredential.isConditionalMediationAvailable();
In an ideal world, this method could work across any browser, but as with autofill, this method may not be available. We can extend this method call further to check if this method is available, and the result of the method call if it is. We can assume that if this method is not present, then neither is autofill.
const mediationAvailable = async () => {
const pubKeyCred = window.PublicKeyCredential;
if (
typeof pubKeyCred.isConditionalMediationAvailable === "function" &&
(await pubKeyCred.isConditionalMediationAvailable())
) {
return true;
} else {
return false;
}
};
When using autofill there are properties in the get
method parameters that deviate from the standard modal experience. We need to:
- Invoke the WebAuthn get ceremony as soon as the user enters the webpage
- Append additional information to the object that is passed into the get method.
- Create an abort controller object that can terminate the autofill request (more on this below)
- Add the autofill property to the username input field
The code sample below will demonstrate methods that can be used to handle the autofill request.
const [authAbortController, setAuthAbortController] = useState(
new AbortController()
);
const passkeySignIn = async () => {
try {
/**
* Call to RP to initiate a
* discoverable credential flow
*/
const assertionOptions = await getAssertionOptions();
/**
* Ensure that you set
* mediation to conditional
* signal to your abort controller
*/
const assertionResult = await navigator.credentials.get({
publicKey: assertionOptions.publicKey,
mediation: "conditional",
signal: authAbortController.signal,
});
/**
* Send the assertion to the RP
* Assume the result of sendAssertionResult
* is an object with a property status,
* which will return ok if successful.
* Assume a status value that is not “ok” is a failed authentication
*/
const authenticationResult = await sendAssertionResult(assertionResult);
/**
* Validate that auth was successful
* Otherwise display a message
*/
if (authenticationResult.status === "ok") {
console.info("Authentication successful");
} else {
throw new Error("Auth failed");
}
} catch (e) {
console.error(e);
/**
* If the WebAuthn get ceremony is canceled
* then create a new abort controller
*/
setAuthAbortController(new AbortController());
}
};
/**
* This method should be called as soon
* as the user enters the page
* In React, this will commonly be the
* useEffect method
*/
const onPageLoad = async () => {
if (
(await mediationAvailable()) &&
authAbortController.signal.aborted === false
) {
await passkeySignIn(authAbortController);
}
};
/**
* Ensure that the input field has the
* autoComplete property with "username webauth"
* as the value
*/
<Form>
<Form.Group>
<Form.Label>Username</Form.Label>
<Form.Control
value={username}
onChange={onUsernameChange}
autoComplete="username webauthn"
/>
</Form.Group>
</Form>;
Note the use of an abort controller in the method above. The abort controller acts as a mechanism to terminate the active autofill request.
This is important as most of the mainstream browsers will only allow one active WebAuthn request at a time. So while autofill is active, your user may be unable to invoke other authentication flows, or registrations of new passkeys
Cancelling the autofill request OR calling the abort controller will terminate the passkeySignIn method.
You can reinvoke the passkeySignIn method, just be sure to instantiate a new abort controller, otherwise the get method will fail immediately if you attempt to use an abort controller that has been used.