Create a passkey for passwordless logins

Passkeys make user accounts safer, simpler, easier to use.

Eiji Kitamura
Eiji Kitamura

Using passkeys instead of passwords is a great way for websites to make their user accounts safer, simpler, easier to use and passwordless. With a passkey, a user can sign in to a website or an app just by using their fingerprint, face or device PIN.

A passkey has to be created, associated with a user account and have its public key be stored on your server before a user can sign in with it.

How it works

A user can be asked to create a passkey in one of the following situations:

  • When a user signs in using a password.
  • When a user signs in using a passkey from another device (that is, the authenticatorAttachment is cross-platform).
  • On a dedicated page where users can manage their passkeys.

To create a passkey, you use the WebAuthn API.

The four components of the passkey registration flow are:

  • Backend: Your backend server that holds the accounts database storing the public key and other metadata about the passkey.
  • Frontend: Your frontend which communicates with the browser and sends fetch requests to the backend.
  • Browser: The user's browser which is running your Javascript.
  • Authenticator: The user's authenticator which creates and stores the passkey. This may be on the same device as the browser (for example, when using Windows Hello) or on another device, like a phone.
Passkey registration diagram

The journey to add a new passkey to an existing user account is as follows:

  1. A user signs in to the website.
  2. Once the user is signed in, they request to create a passkey on the frontend, for example, by pressing a "Create a passkey" button.
  3. The frontend requests information from the backend to create a passkey, such as user information, a challenge, and the credential IDs to exclude.
  4. The frontend calls navigator.credentials.create() to create a passkey. This call returns a promise.
  5. A passkey is created after the user consents using the device's screen lock. The promise is resolved and a public key credential is returned to the frontend.
  6. The frontend sends the public key credential to the backend and stores the credential ID and the public key associated with the user account for future authentications.

Compatibilities

WebAuthn is supported by most browsers, but there are small gaps. Refer to Device Support - passkeys.dev to learn what combination of browsers and an operating systems support creating a passkey.

Create a new passkey

Here's how a frontend should operate upon a request to create a new passkey.

Feature detection

Before displaying a "Create a new passkey" button, check if:

  • The browser supports WebAuthn.
  • The device supports a platform authenticator (can create a passkey and authenticate with the passkey).
  • The browser supports WebAuthn conditional UI.
// Availability of `window.PublicKeyCredential` means WebAuthn is usable.  
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.  
// `​​isConditionalMediationAvailable` means the feature detection is usable.  
if (window.PublicKeyCredential &&  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&  
    PublicKeyCredential.​​isConditionalMediationAvailable) {  
  // Check if user verifying platform authenticator is available.  
  Promise.all([  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),  
    PublicKeyCredential.​​isConditionalMediationAvailable(),  
  ]).then(results => {  
    if (results.every(r => r === true)) {  
      // Display "Create a new passkey" button  
    }  
  });  
}  

Until all the conditions have been met, passkeys will not be supported on this browser. The "Create a new passkey" button shouldn't be displayed until then.

Fetch important information from the backend

When the user clicks the button, fetch important information to call navigator.credentials.create() from the backend:

  • challenge: A server-generated challenge in ArrayBuffer for this registration. This is required but unused during registration unless doing attestation—an advanced topic not covered here.
  • user.id: A user's unique ID. This value must be an ArrayBuffer which does not include personally identifying information, for example, e-mail addresses or usernames. A random, 16-byte value generated per account will work well.
  • user.name: This field should hold a unique identifier for the account that the user will recognise, like their email address or username. This will be displayed in the account selector. (If using a username, use the same value as in password authentication.)
  • user.displayName: This field is a required, more user-friendly name for the account. It need not be unique and could be the user's chosen name. If your site does not have a suitable value to include here, pass an empty string. This may be displayed on the account selector depending on the browser.
  • excludeCredentials: Prevents registering the same device by providing a list of already registered credential IDs. The transports member, if provided, should contain the result of calling getTransports() during the registration of each credential.

Call WebAuthn API to create a passkey

Call navigator.credentials.create() to create a new passkey. The API returns a promise, waiting for the user's interaction displaying a modal dialog.

const publicKeyCredentialCreationOptions = {
  challenge: *****,
  rp: {
    name: "Example",
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },
  pubKeyCredParams: [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
};

const credential = await navigator.credentials.create({
  publicKey: publicKeyCredentialCreationOptions
});

// Encode and send the credential to the server for verification.  

The parameters not explained above are:

  • rp.id: An RP ID is a domain and a website can specify either its domain or a registrable suffix. For example, if an RP's origin is https://login.example.com:1337, the RP ID can be either login.example.com or example.com. If the RP ID is specified as example.com, the user can authenticate on login.example.com or on any subdomains on example.com.

  • rp.name: The RP's name.

  • pubKeyCredParams: This field specifies the RP's supported public-key algorithms. We recommend setting it to [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]. This specifies support for ECDSA with P-256 and RSA PKCS#1 and supporting these gives complete coverage.

  • authenticatorSelection.authenticatorAttachment: Set this to "platform" if this passkey creation is an upgrade from a password e.g. in a promotion after a sign-in. "platform" indicates that the RP wants a platform authenticator (an authenticator embedded to the platform device) which will not prompt to insert e.g. a USB security key. The user has a simpler option to create a passkey.

  • authenticatorSelection.requireResidentKey: Set it to a boolean "true". A discoverable credential (resident key) stores user information to the passkey and lets users select the account upon authentication.

  • authenticatorSelection.userVerification: Indicates whether a user verification using the device screen lock is "required", "preferred" or "discouraged". The default is "preferred", which means the authenticator may skip user verification. Set this to "preferred" or omit the property.

Send the returned public key credential to the backend

After the user consents using the device's screen lock, a passkey is created and the promise is resolved returning a PublicKeyCredential object to the frontend.

The promise can be rejected for different reasons. You can handle these errors by checking the Error object's name property:

  • InvalidStateError: A passkey already exists on the device. No error dialog will be shown to the user and the site should not treat this as an error—the user wanted the local device registered and it is.
  • NotAllowedError: The user has canceled the operation.
  • Other exceptions: Something unexpected happened. The browser shows an error dialog to the user.

The public key credential object contains the following properties:

  • id: A Base64URL encoded ID of the created passkey. This ID helps the browser to determine whether a matching passkey is in the device upon authentication. This value needs to be stored in the database on the backend.
  • rawId: An ArrayBuffer version of the credential ID.
  • response.clientDataJSON: An ArrayBuffer encoded client data.
  • response.attestationObject: An ArrayBuffer encoded attestation object. This contains important information such as an RP ID, flags and a public key.
  • authenticatorAttachment: Returns "platform" when this credential is created on a passkey capable device.
  • type: This field is always set to "public-key".

If you use a library to handle the public key credential object on the backend, we recommend sending the entire object to the backend after encoding it partially with base64url.

Save the credential

Upon receiving the public key credential on the backend, pass it to the FIDO library to process the object.

You can then store the information retrieved from the credential to the database for future use. The following list includes some typical properties to save:

  • Credential ID (Primary key)
  • User ID
  • Public key

The public-key credential also includes the following information that you may want to save in the database:

To authenticate the user, read Sign in with a passkey through form autofill.

Resources