Skip to main content

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:

  1. 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
  2. Atomic API approach: Direct HTTP endpoints (/register/passkey/* and /auth/passkey/*) for full control over the registration and authentication flow
  3. Flow-based approach: Integrate passkeys into orchestrated authentication/registration flows via the /flow/execute API, 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.PublicKeyCredential in 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:

  1. Create an application in the Thunder Develop console (or via the Application API).
  2. 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
  3. 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 the gate_client configuration in deployment.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)

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​

  1. 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 to navigator.credentials.create() (after Base64URLβ†’ArrayBuffer conversion).
  • sessionToken: required for the finish call.
  1. Run WebAuthn ceremony in the browser – call navigator.credentials.create() with the returned options. See the sample implementation in samples/apps/react-vanilla-sample/src/services/authService.ts.

  2. 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​

  1. 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"
}'
  • userId is optional for usernameless authentication.
  • Response fields:
    • publicKeyCredentialRequestOptions: pass to navigator.credentials.get().
    • sessionToken: required for finish.
  1. Run WebAuthn assertion in the browser – call navigator.credentials.get() with the options.

  2. 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​

  1. 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 flowId and a step action for passkeys. data.additionalData.passkeyChallenge contains 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.
  1. Run WebAuthn in the browser – call navigator.credentials.get() with the decoded passkeyChallenge.

  2. 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.

  1. 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/execute with 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 step action, and data.additionalData.passkeyCreationOptions with the WebAuthn creation options. The passkey session state is maintained server-side and associated with the flow, so clients only need to carry flowId (and the step action) between calls. Ensure the flow has already collected the user identifier before this step because registration requires a user ID.
  1. Run WebAuthn in the browser – call navigator.credentials.create() with the decoded passkeyCreationOptions.

  2. 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_origins and must match the RP ID domain.
  • HTTP instead of HTTPS: WebAuthn requires HTTPS in production.
  • Stale session token: Use the sessionToken from 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).
Β© 2026 Thunder. All rights reserved.
Terms & ConditionsPrivacy Policy