Skip to content

Sign In with WebAuthn

Learn about WebAuthn and Security Keys

WebAuthn security key passkey FIDO2 biometric fingerprint face ID YubiKey passwordless PKCE

Follow this guide to sign in users with security keys and the WebAuthn API.

Examples of security keys:

You can read more about this feature in our blog post

Enable the Security Key sign-in method in the Nhost Dashboard under Settings -> Sign-In Methods -> Security Keys.

You need to make sure you also set a valid client URL under Settings -> Authentication -> Client URL.

Users must use an email address to sign up with a security key. When email verification is enabled, pass a codeChallenge for PKCE:

import { generatePKCEPair } from '@nhost/nhost-js/auth';
import { startRegistration } from '@simplewebauthn/browser';
const { verifier, challenge } = await generatePKCEPair();
localStorage.setItem('nhost_pkce_verifier', verifier);
// Step 1: Request registration challenge
const response = await nhost.auth.signUpWebauthn({
email: 'joe@example.com',
options: {
redirectTo: `${window.location.origin}/verify`,
},
});
// Step 2: Create credential with authenticator
const credential = await startRegistration({ optionsJSON: response.body });
// Step 3: Verify credential with PKCE
await nhost.auth.verifySignUpWebauthn({
credential,
options: {
redirectTo: `${window.location.origin}/verify`,
},
nickname: 'My Security Key',
codeChallenge: challenge,
});

If email verification is required, the user will receive a verification email. After clicking the link, the authorization code is exchanged for a session.

sequenceDiagram
autonumber
actor U as User
participant C as Client
participant A as Nhost Auth
participant G as Authenticator
participant E as SMTP Server
C->>C: Generate PKCE pair (verifier + challenge)
C->>C: Store verifier in localStorage
U->>+A: POST /signup/webauthn
A->>A: Create tentative user
A->>-U: Challenge
U->>+G: Sign challenge
G->>-U: Signed challenge
U->>+A: POST /signup/webauthn/verify
Note right of U: credential, codeChallenge
A->>A: Verify signed challenge
A->>A: Add security key
alt No email verification required
A->>-U: Session (access + refresh tokens)
else Email verification required
A->>A: Generate ticket (stores codeChallenge)
A-)E: Send verification email
A->>U: OK (no session)
E-)U: Receive email
U->>+A: GET /verify (follow email link)
A->>A: Verify email
A->>A: Generate authorization code
A->>-C: Redirect with ?code=...
C->>C: Consume verifier from localStorage
C->>+A: POST /token/exchange
Note right of C: code + codeVerifier
A->>A: Validate S256(codeVerifier) == codeChallenge
A->>-C: Session (access + refresh tokens)
end

Once a user signed up with a security key and verified their email if needed, they can use it to sign in.

await nhost.auth.signIn({
email: 'joe@example.com',
securityKey: true,
});
sequenceDiagram
autonumber
actor U as User
participant A as Nhost Auth
participant G as Authenticator
U->>+A: POST /signin/webauthn
alt Email not verified or user disabled
A->>U: ERROR
else Email verified and user enabled
A->>-U: Challenge
end
U->>+G: Sign challenge
G->>-U: Signed challenge
U->>+A: POST /signin/webauthn/verify
A->>A: Verify signed challenge
alt Email not verified or user disabled
A->>U: ERROR
else Valid
A->>-U: Session (access + refresh tokens)
end

Any signed-in user with a verified email can add a security key to their user account. It’s possible to add multiple security keys.

const { key, error } = await nhost.auth.addSecurityKey('My iPhone');
sequenceDiagram
autonumber
actor U as User
participant A as Nhost Auth
participant G as Authenticator
U-->A: Authenticated
U->>+A: POST /user/webauthn/add
A->>-U: Challenge
U->>+G: Sign challenge
G->>-U: Signed challenge
U->>+A: POST /user/webauthn/verify
A->>A: Verify signed challenge
A->>A: Add security key
A->>-U: OK

To list and remove security keys, use GraphQL and set permissions on the auth.security_keys table:

query securityKeys($userId: uuid!) {
authUserSecurityKeys(where: { userId: { _eq: $userId } }) {
id
nickname
}
}

To remove a security key:

mutation removeSecurityKey($id: uuid!) {
deleteAuthUserSecurityKey(id: $id) {
id
}
}