Migrating to Passwordless

Migrating to Passwordless

On this page

In the past, internet users have often had to create a new username and password for every online application they use. This is problematic from both a security and user experience viewpoint, since passwords are both reused and forgotten. These days there are better options for authenticating users, and many users will expect a passwordless experience. Using WebAuthn is one way to enable this, with both strong security and also great usability. A first attempt to support both password and WebAuthn logins might involve simply presenting an authentication selection screen:

Authentication Selection

By default though, this would create a duplicate user account for any existing user who upgraded to use WebAuthn logins. Instead you need a smarter design that ensures data integrity, and therefore business continuity, for all users. This tutorial shows how to implement the correct behavior, using the Curity Identity Server.

User Classification

This tutorial will cover an internet application scenario, though the techniques used could be applied to many other use cases. First, users will be classified into one of the following categories. Next, all users will be given a choice of how they authenticate.

User CategoryDescription
Existing + PasswordUsers who want to continue to use password logins
Existing + PasswordlessUsers who want to upgrade their login to use WebAuthn
New + PasswordFuture users who want to use password logins after registration
New + PasswordlessFuture users who want to use WebAuthn logins after registration

Authentication Workflow

The authentication workflow is implemented using Authenticators and Authentication Actions. It starts by presenting a Username Authenticator, which identifies users before authentication, by collecting the user's email. This is then remembered in an HTTP-only cookie, and auto-filled for subsequent logins:

Username Authenticator

If the user exists and has any WebAuthn devices registered, they are prompted to provide a WebAuthn credential. This involves a simple action such as inserting a Yubikey and tapping it. Proof of ownership of a private key is then sent to the authorization server. Login therefore requires only a couple of clicks, and is both user friendly and secure.

Password Logins

If the user does not exist, or currently authenticates using a password, a Selector Action is shown. This category of user can either continue to use passwords, or can upgrade their primary authentication method to passwordless:

Selector Action

Users who select the password option will be prompted with the HTML Form authenticator. Existing users can sign in with their credentials, and new users can register via the create account option. In the example flow, registration also requires email verification, so that the user also proves ownership of their email address by clicking an activation link.

Password Login

WebAuthn Onboarding

Users who select the WebAuthn option will be prompted to register a device:

WebAuthn Register Device

A user who does not have an account yet will be prompted to fill in a registration form targeted at WebAuthn users. This could be designed to capture any identity information you wish to store against the user account. When the form is submitted, the example flow again uses email verification to prove that the user owns the email address. The new user account is then created.

WebAuthn Registration

Next, the user is prompted to present a device, in order to register a public key at the authorization server:

WebAuthn Present Device

On success the following message is presented and the user is then automatically signed in:

WebAuthn Registered Device

Further Customization

Once you understand how to compose actions together to enable the above behavior, it is straightforward to extend the authentication workflow. This might include using Opt-in MFA to add extra authentication factors, or adopting techniques from the Actions Toolbox to inject further custom data or custom logic.

Example Deployment

You can use the GitHub repository linked at the top of this page to run a working setup and then understand the account data. By default the repo assumes that OAuth Tools is being used as a test client and that it is connecting to a local Docker based instance of the Curity Identity Server using ngrok. If you prefer, you can instead configure the same settings in your own deployed instance.


After cloning the repo, copy a license file for the Curity Identity Server into its root folder. Also ensure that a Docker engine is installed on your local computer. You will also need to have a WebAuthn device such as a Yubikey, or to use the built-in features of your operating system, as explained in these short Webauth videos.

Deploy the System

Next run the following commands to deploy the Curity Identity Server, along with a PostgreSQL database that will store account data:

./apply-use-case.sh ./config/4-configure-migrating-to-passwordless.xml

Create a Test Setup

The script will then output an OpenID Connect metadata URL similar to this:

The OpenID Connect Metadata URL is at: https://4099-2-26-218-80.eu.ngrok.io/oauth/v2/oauth-anonymous/.well-known/openid-configuration

In OAuth Tools, create a new environment and paste this URL into the Metadata URL field:

OAuth Tools Environment

You will then need to run a code flow in OAuth Tools with the following settings. User accounts are cleared on every deployment, so that you can test onboarding and subsequent logins, for both password and WebAuthn flows:

Client ID: demo-web-client
Client Secret: Password1
Scope: openid

Action Configuration

A number of authentication actions are configured in the Curity Identity Server, to control the account data. A partial flow, excluding registration of new users, is illustrated below, to show how authenticators and authentication actions can be composed together to enable your desired behavior.

Actions Workflow

Account Data Storage

The account data can be queried by first getting a shell to the PostgreSQL container:

CONTAINER_ID=$(docker ps | grep postgres | awk '{print $1}')
docker exec -it $CONTAINER_ID bash

Next, connect to the SQL database:

export PGPASSWORD=Password1 && psql -p 5432 -d idsvr -U postgres

Finally, select account related data with these simple queries:

select * from accounts;
select * from linked_accounts;
select * from devices;

You will then see a single account record similar to this:

0cee591a-461b-11ed-8779-0242c0a89002john.doe@company.com077334455given_name: John, family_name: Doe

Since both WebAuthn and HTML Form are used as primary authentication factors, they are configured with a local domain. This is a way of indicating that they are responsible for creating records in the accounts table.

Local Domain

Use of the WebAuthn device results in a public key being stored in the devices table, and a simplified version of the data is shown below. When the WebAuthn device is used, it therefore identifies a particular user:

0cee591a-461b-11ed-8779-0242c0a8900203b57d8e-d069-4cda-894a-aebfcaf66a68webauthnpublic_key: pQECAyYgA ...

In future, it is also possible to link additional primary authentication methods to this main account. To do so you would assign each such authentication method a foreign domain. After the correct configuration is done, this will result in a linked_accounts record being created. For a walkthrough on implementing this data outcome, see the Account Linking with Facebook tutorial.


Access Tokens

Access tokens issued to clients will contain a subject claim as the user identifier. This can be provided in multiple ways, including using the username stored in the database. The example deployment is instead configured to use a Pairwise Pseudonymous Identifier (PPID) for the subject claim. Its value must remain the same regardless of the user's primary authenticator factor, so that a consistent identity is provided to your APIs:

  "jti": "78aefc27-bb45-455b-8cc8-16f4b825f0a9",
  "delegationId": "ec57e21d-b0e9-4dee-a253-bafb8f3f1b63",
  "exp": 1665661983,
  "nbf": 1665661683,
  "scope": "openid",
  "iss": "https://c81d-2-26-218-24.eu.ngrok.io/oauth/v2/oauth-anonymous",
  "sub": "35996a48baa64ac46614349b134e867276f199db5b392e42900142134a723e51",
  "aud": "demo-web-client",
  "iat": 1665661683,
  "purpose": "access_token"

In this example, mixed authentication was used, and it is also possible to issue claims in access tokens based on authentication attributes, as described in Claims from Authentication. This could be used to enable behavior such as excluding high privilege claims from tokens unless a Preregistered WebAuthn Device was used to sign in.


In real world authentication workflows you will need to manage migrations of the primary authentication factor. An essential outcome is that you maintain a single account record per user, and also deliver a consistent identity to your APIs in access tokens. A technique of first identifying users, then authenticating them should be used. This ensures business continuity for existing users, while also allowing new users to onboard.