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:
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
Define which trait serves as the passkey display name:
{
"$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
Initialize registration
Create a registration flow:curl -X GET https://your-kratos-instance/self-service/registration/browser
Get registration challenge
The registration UI will include a passkey registration node with a WebAuthn challenge. Extract the webauthn_register_trigger value.
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)
))
}
};
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
Initialize login
Create a login flow:curl -X GET https://your-kratos-instance/self-service/login/browser
Get authentication challenge
The login UI includes a webauthn_login_trigger with the challenge.
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
}
};
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:
- Credentials are bound to the domain (
rp.id)
- Cannot be used on fraudulent sites
- Origin is cryptographically verified
- 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:
selfservice:
flows:
recovery:
enabled: true
use: code
methods:
code:
enabled: true
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