Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .vale/styles/config/vocabularies/vocab/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ Thunder
npm
pnpm
[Vv]ite
[Pp]asswordless
[Uu]sernameless
APIs
UIs
iCloud
hostname

6 changes: 6 additions & 0 deletions docs/content/guides/authentication/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"position": 5,
"label": "Authentication",
"collapsible": true
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"position": 1,
"label": "Passwordless Authentication",
"collapsible": true
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
---
title: Passkeys
description: Configure and use Passkeys for passwordless sign-in
---

# 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`):

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

```bash
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.

2) **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`.

3) **Finish registration** – send the attestation result with the session token.

```bash
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.

```bash
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.

2) **Run WebAuthn assertion in the browser** – call `navigator.credentials.get()` with the options.

3) **Finish authentication** – send the assertion result with the session token.

```bash
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.

```bash
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.

2) **Run WebAuthn in the browser** – call `navigator.credentials.get()` with the decoded `passkeyChallenge`.

3) **Continue the flow** – post the assertion back to the flow engine.

```bash
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.

```bash
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.

2) **Run WebAuthn in the browser** – call `navigator.credentials.create()` with the decoded `passkeyCreationOptions`.

3) **Finish registration in the flow** – post the attestation back to the flow engine.

```bash
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).
Loading