Skip to main content
Passkeys provide modern, phishing-resistant passwordless authentication using WebAuthn/FIDO2 standards. They offer better security and user experience compared to traditional passwords.

Overview

The passkey strategy enables:
  • Passwordless registration and login
  • Device-bound cryptographic credentials
  • Biometric authentication support
  • Cross-device passkey sync (via platform providers)
  • Multiple passkeys per identity
Passkeys are different from WebAuthn MFA. Passkeys are first-factor credentials that replace passwords, while WebAuthn can be used as second-factor authentication.

Configuration

Enable passkeys

Configure the passkey method in your Kratos configuration:
kratos.yml
selfservice:
  methods:
    passkey:
      enabled: true
      config:
        rp:
          id: localhost  # Your domain (without protocol or port)
          display_name: Your Application Name
          origins:
            - https://your-app.com
            - https://your-app.com:3000

Relying Party (RP) configuration

The Relying Party configuration is critical for passkey functionality:
selfservice:
  methods:
    passkey:
      enabled: true
      config:
        rp:
          id: localhost
          display_name: My App (Dev)
          origins:
            - http://localhost:4455
            - http://localhost:3000
The rp.id must match the domain where passkeys are used. Credentials registered for one domain cannot be used on another.
Configuration parameters:
  • rp.id - The relying party identifier (typically your top-level domain)
  • rp.display_name - Human-readable application name shown during registration
  • rp.origins - Array of allowed origins for WebAuthn ceremonies

Identity schema configuration

Configure passkey identifier

Define which trait serves as the passkey display name:
identity.schema.json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "traits": {
      "type": "object",
      "properties": {
        "email": {
          "type": "string",
          "format": "email",
          "title": "E-Mail",
          "ory.sh/kratos": {
            "credentials": {
              "passkey": {
                "display_name": true
              }
            },
            "verification": {
              "via": "email"
            },
            "recovery": {
              "via": "email"
            }
          }
        },
        "name": {
          "type": "object",
          "properties": {
            "first": {
              "type": "string",
              "title": "First Name"
            },
            "last": {
              "type": "string",
              "title": "Last Name"
            }
          }
        }
      },
      "required": ["email"]
    }
  }
}
The field marked with "display_name": true is used to identify the passkey in the user’s authenticator (typically shown as the account name). See selfservice/strategy/passkey/passkey_schema_extension.go:29-35 for the schema extension implementation.

User flows

Registration with passkey

1

Initialize registration

Create a registration flow:
curl -X GET https://your-kratos-instance/self-service/registration/browser
2

Get registration challenge

The registration UI will include a passkey registration node with a WebAuthn challenge. Extract the webauthn_register_trigger value.
3

Create credential on device

Use the WebAuthn JavaScript API to create a credential:
// Parse the publicKey options from the flow
const publicKey = JSON.parse(webauthnRegisterTrigger);

// Create credential
const credential = await navigator.credentials.create({
  publicKey: publicKey
});

// Encode the response
const response = {
  id: credential.id,
  rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),
  type: credential.type,
  response: {
    attestationObject: btoa(String.fromCharCode(
      ...new Uint8Array(credential.response.attestationObject)
    )),
    clientDataJSON: btoa(String.fromCharCode(
      ...new Uint8Array(credential.response.clientDataJSON)
    ))
  }
};
4

Submit registration

Submit the credential to complete registration:
curl -X POST https://your-kratos-instance/self-service/registration?flow=<flow-id> \
  -H "Content-Type: application/json" \
  -d '{
    "method": "passkey",
    "traits": {
      "email": "user@example.com"
    },
    "passkey_register": "<encoded-credential-response>"
  }'

Login with passkey

1

Initialize login

Create a login flow:
curl -X GET https://your-kratos-instance/self-service/login/browser
2

Get authentication challenge

The login UI includes a webauthn_login_trigger with the challenge.
3

Authenticate with device

Use the WebAuthn API to get the assertion:
const publicKey = JSON.parse(webauthnLoginTrigger);

const assertion = await navigator.credentials.get({
  publicKey: publicKey
});

const response = {
  id: assertion.id,
  rawId: btoa(String.fromCharCode(...new Uint8Array(assertion.rawId))),
  type: assertion.type,
  response: {
    authenticatorData: btoa(String.fromCharCode(
      ...new Uint8Array(assertion.response.authenticatorData)
    )),
    clientDataJSON: btoa(String.fromCharCode(
      ...new Uint8Array(assertion.response.clientDataJSON)
    )),
    signature: btoa(String.fromCharCode(
      ...new Uint8Array(assertion.response.signature)
    )),
    userHandle: assertion.response.userHandle ? btoa(String.fromCharCode(
      ...new Uint8Array(assertion.response.userHandle)
    )) : undefined
  }
};
4

Submit authentication

curl -X POST https://your-kratos-instance/self-service/login?flow=<flow-id> \
  -H "Content-Type: application/json" \
  -d '{
    "method": "passkey",
    "passkey_login": "<encoded-assertion-response>"
  }'

Managing passkeys

Users can add or remove passkeys through the settings flow:
# Add a new passkey
curl -X POST https://your-kratos-instance/self-service/settings?flow=<flow-id> \
  -H "Content-Type: application/json" \
  -H "Cookie: ory_kratos_session=<session-token>" \
  -d '{
    "method": "passkey",
    "passkey_register": "<credential-response>"
  }'

# Remove a passkey
curl -X POST https://your-kratos-instance/self-service/settings?flow=<flow-id> \
  -H "Content-Type: application/json" \
  -H "Cookie: ory_kratos_session=<session-token>" \
  -d '{
    "method": "passkey",
    "passkey_remove": "<credential-id>"
  }'

Security considerations

Phishing resistance

Passkeys provide strong phishing protection:
  1. Credentials are bound to the domain (rp.id)
  2. Cannot be used on fraudulent sites
  3. Origin is cryptographically verified
  4. User cannot be tricked into using credentials on wrong site

Device security

Passkeys leverage device security features:
  • Private keys never leave the device
  • Biometric authentication or device PIN required
  • Hardware-backed key storage on supported devices
  • Automatic credential sync (platform-dependent)

Backup and recovery

Passkeys can be synced across devices by platform providers (Apple iCloud Keychain, Google Password Manager), but you should still implement account recovery flows.
Configure recovery methods:
kratos.yml
selfservice:
  flows:
    recovery:
      enabled: true
      use: code
  methods:
    code:
      enabled: true

Browser and platform support

Passkeys are supported on:
  • Desktop: Chrome 93+, Edge 93+, Safari 16+, Firefox 119+
  • Mobile: iOS 16+, Android 9+
  • Authenticators:
    • Platform authenticators (Face ID, Touch ID, Windows Hello)
    • Security keys (YubiKey, etc.)
    • Synced passkeys (iCloud Keychain, Google Password Manager)

API reference

Strategy implementation

The passkey strategy is implemented as:
  • Strategy ID: passkey (see selfservice/strategy/passkey/passkey_strategy.go:80-82)
  • Node Group: passkey group in UI nodes
  • AAL Level: AAL1 (first factor authentication)
  • Authentication Method: Returns identity.CredentialsTypePasskey with AAL1 (see selfservice/strategy/passkey/passkey_strategy.go:88-92)

Credential counting

The strategy counts only first-factor credentials (see selfservice/strategy/passkey/passkey_strategy.go:95-100):
func (s *Strategy) CountActiveMultiFactorCredentials(
  _ context.Context, 
  _ map[identity.CredentialsType]identity.Credentials,
) (count int, err error) {
  return 0, nil  // Passkeys are not MFA
}

Display name resolution

Display names are extracted from identity traits using the schema extension (see selfservice/strategy/passkey/passkey_schema_extension.go:45-51):
func (s *Strategy) PasskeyDisplayNameFromIdentity(
  ctx context.Context, 
  id *identity.Identity,
) string

Configuration reference

selfservice:
  methods:
    passkey:
      enabled: true
      config:
        rp:
          id: example.com
          display_name: My App
          origins:
            - https://example.com

Next steps

Build docs developers (and LLMs) love