WebAuthn enables hardware-based authentication using security keys, platform authenticators (like Touch ID or Windows Hello), and other FIDO2-compliant devices.
Overview
The WebAuthn strategy provides:
Second-factor authentication (2FA/MFA)
First-factor passwordless authentication
Support for security keys (YubiKey, etc.)
Platform authenticators (Touch ID, Face ID, Windows Hello)
Multiple credentials per identity
WebAuthn can function as both first-factor (passwordless) and second-factor (MFA) authentication. For modern passwordless-only flows, consider using Passkeys instead.
Configuration
Enable WebAuthn
Configure WebAuthn in your Kratos configuration:
selfservice :
methods :
webauthn :
enabled : true
config :
passwordless : true # Enable passwordless login
rp :
id : localhost
display_name : Your Application Name
# Note: use 'origin' for single origin or 'origins' for multiple
origin : http://localhost:4455 # For single origin
# OR use origins for multiple:
# origins:
# - https://example.com
# - https://www.example.com
Passwordless vs. Second-factor
Passwordless mode
Second-factor mode
Users can register and login with only WebAuthn, no password required: selfservice :
methods :
webauthn :
enabled : true
config :
passwordless : true
rp :
id : localhost
display_name : My App
origin : http://localhost:4455
When passwordless: true:
WebAuthn is a first-factor credential (AAL1)
Users can login without a password
Credentials include user handle for identification
WebAuthn used as second factor after password: selfservice :
methods :
password :
enabled : true
webauthn :
enabled : true
config :
passwordless : false
rp :
id : localhost
display_name : My App
origin : http://localhost:4455
When passwordless: false:
WebAuthn is a second-factor credential (AAL2)
Requires password login first
Provides multi-factor authentication
Relying Party configuration
The Relying Party (RP) configuration is critical:
selfservice :
methods :
webauthn :
config :
rp :
# Domain identifier (no protocol/port)
id : example.com
# Display name shown in authenticator UI
display_name : Example Application
# Single origin (legacy)
origin : https://example.com
# OR multiple origins (recommended)
origins :
- https://example.com
- https://www.example.com
- https://app.example.com
The rp.id must match the domain where WebAuthn is used. Set it to your top-level domain without protocol or port.
Identity schema configuration
For passwordless WebAuthn, define the identifier field:
{
"$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" : {
"webauthn" : {
"identifier" : true
}
},
"verification" : {
"via" : "email"
},
"recovery" : {
"via" : "email"
}
}
}
},
"required" : [ "email" ]
}
}
}
The field marked with "identifier": true is used for passwordless login lookups.
Combined with other methods
You can combine WebAuthn with other authentication methods:
"email" : {
"type" : "string" ,
"format" : "email" ,
"title" : "E-Mail" ,
"ory.sh/kratos" : {
"credentials" : {
"password" : {
"identifier" : true
},
"webauthn" : {
"identifier" : true
},
"passkey" : {
"display_name" : true
}
}
}
}
User flows
Registration with WebAuthn
Initialize registration
Create a registration flow: curl -X GET https://your-kratos-instance/self-service/registration/browser
Collect user information
If passwordless mode, collect the identifier (email): curl -X POST https://your-kratos-instance/self-service/registration?flow= < flow-i d > \
-H "Content-Type: application/json" \
-d '{
"method": "webauthn",
"traits": {
"email": "[email protected] "
}
}'
Create credential
Use WebAuthn JavaScript API: const publicKey = JSON . parse ( webauthnRegisterTrigger );
const credential = await navigator . credentials . create ({
publicKey: publicKey
});
Submit credential
Submit the encoded credential response to complete registration.
Passwordless login
Initialize login
Create a login flow with WebAuthn challenge.
Request assertion
const publicKey = JSON . parse ( webauthnLoginTrigger );
const assertion = await navigator . credentials . get ({
publicKey: publicKey
});
Submit assertion
Submit the assertion to authenticate the user.
Second-factor authentication
When passwordless: false, WebAuthn is used as a second factor:
Login with password
User first authenticates with password (or another first-factor method).
Initialize AAL2 flow
If AAL2 is required, Kratos requests second-factor authentication.
Complete WebAuthn challenge
User completes WebAuthn authentication to reach AAL2.
Managing WebAuthn credentials
Users can manage their credentials through settings:
# Add new credential
curl -X POST https://your-kratos-instance/self-service/settings?flow= < flow-i d > \
-H "Content-Type: application/json" \
-H "Cookie: ory_kratos_session=<session-token>" \
-d '{
"method": "webauthn",
"webauthn_register": "<credential-response>"
}'
# Remove credential
curl -X POST https://your-kratos-instance/self-service/settings?flow= < flow-i d > \
-H "Content-Type: application/json" \
-H "Cookie: ory_kratos_session=<session-token>" \
-d '{
"method": "webauthn",
"webauthn_remove": "<credential-id>"
}'
Security considerations
Authenticator types
WebAuthn supports multiple authenticator types:
Platform authenticators
Built into devices (Touch ID, Face ID, Windows Hello)
More convenient for users
Tied to specific device
Roaming authenticators
Security keys (YubiKey, etc.)
Portable across devices
Physical possession required
Attestation
WebAuthn supports attestation to verify authenticator provenance:
None - No attestation (default, most privacy-preserving)
Indirect - Anonymized attestation
Direct - Full attestation with manufacturer info
Kratos uses “none” by default for better privacy.
User verification
WebAuthn can require user verification (biometric or PIN):
Required - Always requires user verification
Preferred - Requests verification if available
Discouraged - Skips user verification
Kratos configures this based on the security requirements.
Credential counting logic
The strategy implements different credential counting based on passwordless mode (see selfservice/strategy/webauthn/strategy.go:80-110):
// First-factor counting (passwordless mode)
func ( s * Strategy ) CountActiveFirstFactorCredentials (
_ context . Context ,
cc map [ identity . CredentialsType ] identity . Credentials ,
) ( count int , err error ) {
return s . countCredentials ( cc , true ) // Only passwordless credentials
}
// Second-factor counting (MFA mode)
func ( s * Strategy ) CountActiveMultiFactorCredentials (
_ context . Context ,
cc map [ identity . CredentialsType ] identity . Credentials ,
) ( count int , err error ) {
return s . countCredentials ( cc , false ) // Only non-passwordless credentials
}
Passwordless credentials require an identifier to be set (see selfservice/strategy/webauthn/strategy.go:96-100).
WebAuthn is supported on:
Desktop : Chrome 67+, Edge 18+, Firefox 60+, Safari 13+
Mobile : iOS 14.5+, Android 7+
Security Keys : FIDO2/WebAuthn compliant keys (YubiKey 5, Titan, etc.)
API reference
Strategy implementation
Strategy ID : webauthn
Node Group : webauthn group in UI nodes
AAL Level :
AAL1 when passwordless: true (first factor)
AAL2 when passwordless: false (second factor)
See selfservice/strategy/webauthn/strategy.go:76-78 for the strategy structure.
Configuration options
Passwordless
Second-factor
Legacy (single origin)
selfservice :
methods :
webauthn :
enabled : true
config :
passwordless : true
rp :
id : example.com
display_name : My Application
origins :
- https://example.com
Migration from WebAuthn to Passkeys
If you’re using WebAuthn in passwordless mode, consider migrating to the Passkeys strategy:
Passkeys are purpose-built for passwordless flows
Better cross-device synchronization
Clearer separation from MFA use cases
Modern terminology users understand
Both strategies use WebAuthn protocol under the hood, but Passkeys is optimized for the passwordless experience.
Next steps