Passkeys
Use passkeys to offer phishing-resistant, passwordless authentication. Thunder exposes WebAuthn-based APIs to register passkey credentials and to authenticate users with those credentials.
Overviewβ
Passkeys are a modern, secure alternative to passwords based on the WebAuthn standard. They use public-key cryptography to provide:
- Phishing-resistant authentication: Passkeys are bound to your domain and cannot be used on fake sites
- Passwordless experience: Users authenticate with biometrics, PINs, or security keys instead of remembering passwords
- Cross-device compatibility: Passkeys sync across devices via platform authenticators (e.g., iCloud Keychain, Google Password Manager)
Thunder supports passkeys through three approaches:
- Thunder Gate (Hosted UI): Use Thunder's hosted authentication pages (
thunder-gate) by configuring passkeys in your application settingsβno custom UI or API calls needed - Atomic API approach: Direct HTTP endpoints (
/register/passkey/*and/auth/passkey/*) for full control over the registration and authentication flow - Flow-based approach: Integrate passkeys into orchestrated authentication/registration flows via the
/flow/executeAPI, combining passkeys with other authentication methods
All approaches follow the WebAuthn standard ceremony:
- Registration: Generate a credential creation challenge, collect the attestation from the browser's
navigator.credentials.create(), and store the credential - Authentication: Generate an assertion challenge, collect the signed assertion from
navigator.credentials.get(), and verify it
Prerequisitesβ
- Serve the UI over HTTPS with a hostname that matches your WebAuthn Relying Party ID (RP ID).
- Add allowed origins for WebAuthn to your deployment configuration. Example (
repository/conf/deployment.yaml):
passkey:
allowed_origins:
- "https://localhost:8090"
- "https://localhost:3000"
- Use a WebAuthn-capable browser (recent Chrome, Edge, Safari, or Firefox); you can confirm support at https://passkeys.dev/device-support/ or by checking
window.PublicKeyCredentialin the browser console. - Ensure users already exist or are created beforehand in Thunder for passkey registration.
Use Thunder Gate (Hosted UI)β
The simplest way to enable passkeys is through Thunder Gate, Thunder's hosted authentication and registration UI. This approach uses OAuth2/OIDC authorization flow:
- Create an application in the Thunder Develop console (or via the Application API).
- Configure authentication flows for the application:
- Navigate to Applications β Select your application β Flows tab
- Select an Authentication Flow that includes passkey authentication (e.g., "Passkey Authentication" or "Basic + Passkey Authentication and Registration Flow")
- Optionally, select a Registration Flow that includes passkey registration (e.g., "Passkey Registration Flow")
- The flow builder UI lets you customize which executors run and configure relying party settings
- Integrate with your application:
- Redirect users to Thunder's OAuth2 authorize endpoint:
https://localhost:8090/oauth2/authorize?client_id=<your-client-id>&redirect_uri=<your-callback>&response_type=code&scope=openid - Thunder automatically redirects to Thunder Gate (e.g.,
https://localhost:5190/gate/signin) based on thegate_clientconfiguration indeployment.yaml - Thunder Gate renders the authentication UI based on your selected flow, handling passkey WebAuthn ceremonies in the browser
- After successful authentication, users are redirected back to your application with an authorization code (exchange it for tokens via
/oauth2/token)
- Redirect users to Thunder's OAuth2 authorize endpoint:
This approach requires no custom UI development or direct WebAuthn API calls from your application. Thunder Gate (thunder-gate) handles:
- Rendering sign-in/registration prompts based on the configured flow
- Passkey registration during user sign-up (if registration flow includes passkey executor)
- Passkey authentication during sign-in
- Fallback to other authentication methods (password, social login, etc.) as defined in the flow
- All WebAuthn ceremony handling (
navigator.credentials.create()and.get())
When to use this approach:
- You want a quick setup without building custom authentication UIs
- You're using Thunder as an OAuth2/OIDC provider for your applications
- You want Thunder to manage the full authentication experience with customizable flows
When to use API-based approaches:
- You need full control over the UI/UX beyond what flow configuration offers
- You're building a mobile application or SPA with custom authentication flows that don't fit OAuth2 redirect flow
- You want to embed authentication directly in your application without redirects
Use Passkey Atomic API in Your Applicationβ
The registration and authentication flows below describe the atomic API approach (direct
/register/passkey/*and/auth/passkey/*calls).
Registration Flowβ
- Start registration β create WebAuthn creation options and a session token.
curl -k -X POST https://localhost:8090/register/passkey/start \
-H "Content-Type: application/json" \
-d '{
"userId": "<user-id>",
"relyingPartyId": "localhost",
"relyingPartyName": "Thunder",
"authenticatorSelection": {
"userVerification": "preferred"
},
"attestation": "none"
}'
Response fields:
publicKeyCredentialCreationOptions: pass directly tonavigator.credentials.create()(after Base64URLβArrayBuffer conversion).sessionToken: required for the finish call.
-
Run WebAuthn ceremony in the browser β call
navigator.credentials.create()with the returned options. See the sample implementation insamples/apps/react-vanilla-sample/src/services/authService.ts. -
Finish registration β send the attestation result with the session token.
curl -k -X POST https://localhost:8090/register/passkey/finish \
-H "Content-Type: application/json" \
-d '{
"publicKeyCredential": {
"id": "<credential-id>",
"type": "public-key",
"response": {
"clientDataJSON": "<base64url>",
"attestationObject": "<base64url>"
}
},
"sessionToken": "<session-token>",
"credentialName": "My laptop key"
}'
On success, the API returns passkey registration metadata for the newly created credential (CredentialID, CredentialName, and CreatedAt).
Authentication Flowβ
- Start authentication β request assertion options.
curl -k -X POST https://localhost:8090/auth/passkey/start \
-H "Content-Type: application/json" \
-d '{
"userId": "<user-id-optional>",
"relyingPartyId": "localhost"
}'
userIdis optional for usernameless authentication.- Response fields:
publicKeyCredentialRequestOptions: pass tonavigator.credentials.get().sessionToken: required for finish.
-
Run WebAuthn assertion in the browser β call
navigator.credentials.get()with the options. -
Finish authentication β send the assertion result with the session token.
curl -k -X POST https://localhost:8090/auth/passkey/finish \
-H "Content-Type: application/json" \
-d '{
"publicKeyCredential": {
"id": "<credential-id>",
"type": "public-key",
"response": {
"clientDataJSON": "<base64url>",
"authenticatorData": "<base64url>",
"signature": "<base64url>",
"userHandle": "<base64url>"
}
},
"sessionToken": "<session-token>",
"skipAssertion": false
}'
On success, the API returns an authentication response compatible with other Thunder auth flows.
Use Passkeys With Flow/Executeβ
Passkeys also work through the flow engine (POST /flow/execute), which returns dynamic prompts and additional data.
Authentication With Flow/Executeβ
- Start the flow β send an initial flow request.
curl -k -X POST https://localhost:8090/flow/execute \
-H "Content-Type: application/json" \
-d '{
"applicationId": "<app-id>",
"flowType": "AUTHENTICATION"
}'
- The response includes a
flowIdand a stepactionfor passkeys.data.additionalData.passkeyChallengecontains the WebAuthn request options. The passkey session token is stored server-side in the flow context runtime data and is managed by the server; the client does not need to read or send it.
-
Run WebAuthn in the browser β call
navigator.credentials.get()with the decodedpasskeyChallenge. -
Continue the flow β post the assertion back to the flow engine.
curl -k -X POST https://localhost:8090/flow/execute \
-H "Content-Type: application/json" \
-d '{
"flowId": "<flow-id-from-step>",
"action": "<action-ref-from-step>",
"inputs": {
"credentialId": "<credential-id>",
"clientDataJSON": "<base64url>",
"authenticatorData": "<base64url>",
"signature": "<base64url>",
"userHandle": "<base64url-optional>"
}
}'
- On success, the next response either completes the flow (with an assertion) or advances to the next step.
Registration With Flow/Executeβ
Passkey registration in a flow must run after the user is created or identified. Ensure the flow provisions the user (e.g., collecting username/email and running provisioning) or resolves an existing user before the passkey register start node, as shown in the bundled flow definitions.
- Start a registration flow β send an initial flow request.
curl -k -X POST https://localhost:8090/flow/execute \
-H "Content-Type: application/json" \
-d '{
"applicationId": "<app-id>",
"flowType": "REGISTRATION"
}'
Reminder: Calling
/flow/executewith a flow that only runs the passkey registration executor is impractical; the flow must first provision or resolve the user so registration has a valid subject.
- The response includes a
flowId, a registration stepaction, anddata.additionalData.passkeyCreationOptionswith the WebAuthn creation options. The passkey session state is maintained server-side and associated with the flow, so clients only need to carryflowId(and the stepaction) between calls. Ensure the flow has already collected the user identifier before this step because registration requires a user ID.
-
Run WebAuthn in the browser β call
navigator.credentials.create()with the decodedpasskeyCreationOptions. -
Finish registration in the flow β post the attestation back to the flow engine.
curl -k -X POST https://localhost:8090/flow/execute \
-H "Content-Type: application/json" \
-d '{
"flowId": "<flow-id-from-step>",
"action": "<action-ref-from-step>",
"inputs": {
"credentialId": "<credential-id>",
"clientDataJSON": "<base64url>",
"attestationObject": "<base64url>",
"credentialName": "My laptop key"
}
}'
- On success, the response includes credential metadata (e.g.,
passkeyCredentialID) or advances to the next step of the flow.
Common Issuesβ
- Origin mismatch: The browser origin must be listed under
passkey.allowed_originsand must match the RP ID domain. - HTTP instead of HTTPS: WebAuthn requires HTTPS in production.
- Stale session token: Use the
sessionTokenfrom the most recent start call for each ceremony. - Unsupported platform authenticator: Adjust
authenticatorSelection(e.g.,authenticatorAttachment) in the start request to match the target device. - User not found: Ensure the user exists in Thunder before registering a passkey or start authentication with a valid user ID (unless using usernameless auth).