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:

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 Category | Description |
---|---|
Existing + Password | Users who want to continue to use password logins |
Existing + Passwordless | Users who want to upgrade their login to use WebAuthn |
New + Password | Future users who want to use password logins after registration |
New + Passwordless | Future 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:

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:

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.

WebAuthn Onboarding
Users who select the WebAuthn option will be prompted to register a 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.

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

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

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 authentication actions example 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.
Prerequisites
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:
export USE_NGROK=true./deploy.sh./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:

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-clientClient Secret: Password1Scope: 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.
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;
Initially the data will be empty. To create an account record, run a login, select the HTML form option, then use the create account
link on the login page to register a new user. The account data will then look similar to this:
account_id | username | phone | attributes |
---|---|---|---|
0cee591a-461b-11ed-8779-0242c0a89002 | john.doe@company.com | 077334455 | given_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.

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:
account_id | device_id | device_type | attributes |
---|---|---|---|
0cee591a-461b-11ed-8779-0242c0a89002 | 03b57d8e-d069-4cda-894a-aebfcaf66a68 | webauthn | public_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 Social Identity Providers tutorial.
account_id | linked_account_id | linked_account_domain_name | linking_account_manager |
---|---|---|---|
0cee591a-461b-11ed-8779-0242c0a89002 | 102269322839797161925 | google-domain | default-account-manager |
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.
Conclusion
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.
Join our Newsletter
Get the latest on identity management, API Security and authentication straight to your inbox.
Start Free Trial
Try the Curity Identity Server for Free. Get up and running in 10 minutes.
Start Free Trial