Lookup secrets, also known as recovery codes or backup codes, provide a fallback authentication method when primary two-factor methods are unavailable.
Overview
The lookup secrets strategy provides:
One-time use recovery codes
Backup authentication when TOTP/WebAuthn is unavailable
Multiple codes per identity (typically 12-16)
Secure second-factor alternative
Code regeneration capability
Lookup secrets are exclusively a second-factor method (AAL2). They provide backup access when users lose their primary 2FA device.
Configuration
Enable lookup secrets
Configure lookup secrets in your Kratos configuration:
selfservice :
methods :
lookup_secret :
enabled : true
Typically used alongside other 2FA methods:
selfservice :
methods :
password :
enabled : true
totp :
enabled : true
config :
issuer : My App
lookup_secret :
enabled : true
session :
whoami :
required_aal : highest_available
Identity schema configuration
Lookup secrets don’t require special identity schema configuration. They work with any identity schema that has a first-factor authentication method configured.
{
"$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" : {
"password" : {
"identifier" : true
}
}
}
}
},
"required" : [ "email" ]
}
}
}
User flows
Generating lookup secrets
Initialize settings flow
User must be authenticated: curl -X GET https://your-kratos-instance/self-service/settings/browser \
-H "Cookie: ory_kratos_session=<session-token>"
Request lookup secret generation
The settings flow includes lookup secret management nodes. To generate new codes: 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": "lookup_secret"
}'
Receive recovery codes
The response includes a list of recovery codes: {
"lookup_secret_codes" : [
"a1b2c3d4" ,
"e5f6g7h8" ,
"i9j0k1l2" ,
"m3n4o5p6" ,
"q7r8s9t0" ,
"u1v2w3x4" ,
"y5z6a7b8" ,
"c9d0e1f2" ,
"g3h4i5j6" ,
"k7l8m9n0" ,
"o1p2q3r4" ,
"s5t6u7v8"
]
}
Save codes securely
User must save these codes in a secure location. Each code can only be used once. Display a prominent warning to users to save these codes. They won’t be shown again.
Using a lookup secret for login
First-factor authentication
User logs in with their primary credential: curl -X POST https://your-kratos-instance/self-service/login?flow= < flow-i d > \
-H "Content-Type: application/json" \
-d '{
"method": "password",
"identifier": "[email protected] ",
"password": "user-password"
}'
Second-factor challenge
If AAL2 is required, user is prompted for second-factor authentication. They can choose to use a lookup secret instead of TOTP.
Submit recovery code
User submits one of their saved recovery codes: curl -X POST https://your-kratos-instance/self-service/login?flow= < flow-i d > \
-H "Content-Type: application/json" \
-d '{
"method": "lookup_secret",
"lookup_secret": "a1b2c3d4"
}'
Code consumed
On success:
Session is elevated to AAL2
The used code is invalidated
User should generate new codes when running low
Regenerating codes
Users can regenerate their recovery codes at any time:
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": "lookup_secret",
"lookup_secret_regenerate": true
}'
Regenerating codes invalidates all previous codes. Users must save the new codes.
Confirming code generation
After viewing the codes, users should confirm they’ve saved them:
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": "lookup_secret",
"lookup_secret_confirm": true
}'
Disabling lookup secrets
Users can remove lookup secrets:
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": "lookup_secret",
"lookup_secret_disable": true
}'
Implementation details
Lookup secrets are:
Randomly generated strings
Typically 8-12 characters
Hashed before storage (like passwords)
Each code is single-use only
Credential structure
The credentials are stored as (see selfservice/strategy/lookup/strategy.go:86-99):
type identity . CredentialsLookupConfig struct {
RecoveryCodes [] RecoveryCode
}
type RecoveryCode struct {
Code string
UsedAt * time . Time
CreatedAt time . Time
}
Used codes are marked with UsedAt timestamp rather than deleted.
Credential counting
Lookup secrets are only counted as second-factor credentials (see selfservice/strategy/lookup/strategy.go:82-100):
func ( s * Strategy ) CountActiveFirstFactorCredentials (
_ context . Context ,
_ map [ identity . CredentialsType ] identity . Credentials ,
) ( count int , err error ) {
return 0 , nil // Never a first factor
}
func ( s * Strategy ) CountActiveMultiFactorCredentials (
_ context . Context ,
cc map [ identity . CredentialsType ] identity . Credentials ,
) ( count int , err error ) {
for _ , c := range cc {
if c . Type == s . ID () && len ( c . Config ) > 0 {
var conf identity . CredentialsLookupConfig
if err := json . Unmarshal ( c . Config , & conf ); err != nil {
return 0 , errors . WithStack ( err )
}
if len ( conf . RecoveryCodes ) > 0 {
count ++
}
}
}
return
}
Security considerations
Storage and display
Recovery codes should be displayed only once after generation. Do not email them or store them in plain text.
Best practices:
Display codes immediately after generation
Require user confirmation that codes are saved
Never email codes (email is not secure)
Hash codes before database storage
Allow regeneration if codes are lost
Code security
Protect recovery codes:
Codes are hashed using the same algorithm as passwords
Each code can only be used once
Used codes remain in the database (marked as used)
Regular rotation is recommended
Number of codes
Typical configurations provide:
12-16 codes - Balance between security and usability
Too few codes - User might run out
Too many codes - Increased attack surface
Rate limiting
Implement rate limiting on lookup secret authentication:
selfservice :
flows :
login :
ui_url : http://localhost:4455/login
lifespan : 10m
Consider implementing additional rate limiting at the proxy/load balancer level.
Use cases
Lost authenticator device
Primary use case for recovery codes:
User loses phone with TOTP app
User logs in with password
Uses recovery code for second factor
Can then disable TOTP and set up new 2FA
Travel or device unavailability
Backup when primary 2FA is unavailable:
User traveling without security key
Uses recovery code as alternative 2FA
Can resume normal 2FA when device is available
Emergency access
Provide emergency access method:
User stores codes in secure location (safe, password manager)
Codes available for emergency account access
Can be used to regain access and reconfigure security
API reference
Strategy implementation
The lookup secret strategy is implemented as (see selfservice/strategy/lookup/strategy.go:78-80):
type Strategy struct { d dependencies }
func NewStrategy ( d dependencies ) * Strategy {
return & Strategy { d : d }
}
Strategy ID : lookup_secret (as identity.CredentialsTypeLookup)
Node Group : lookup_secret group in UI nodes
AAL Level : AAL2 (second factor only)
Authentication Method : Returns AAL2 (see selfservice/strategy/lookup/strategy.go:110-114)
The strategy implements login.AAL2FormHydrator (see selfservice/strategy/lookup/strategy.go:34):
var (
_ settings . Strategy = ( * Strategy )( nil )
_ login . AAL2FormHydrator = ( * Strategy )( nil )
_ identity . ActiveCredentialsCounter = ( * Strategy )( nil )
)
This allows lookup secrets to be presented as an option during AAL2 login flows.
Configuration reference
Minimal
With TOTP
Complete 2FA setup
selfservice :
methods :
password :
enabled : true
lookup_secret :
enabled : true
Best practices
User education
Educate users about recovery codes:
Explain they are one-time use
Emphasize secure storage
Recommend saving in password manager
Suggest printing and storing physically
Remind to regenerate when running low
UI recommendations
When displaying recovery codes:
< div class = "recovery-codes" >
< h2 > Save Your Recovery Codes </ h2 >
< p class = "warning" >
Store these codes in a secure location.
Each code can only be used once.
</ p >
< div class = "codes" >
< code > a1b2c3d4 </ code >
< code > e5f6g7h8 </ code >
<!-- ... more codes ... -->
</ div >
< button > Download as text file </ button >
< button > Print codes </ button >
< label >
< input type = "checkbox" required />
I have saved these codes in a secure location
</ label >
</ div >
Monitoring
Monitor lookup secret usage:
Track when codes are used
Alert on suspicious patterns (multiple failed attempts)
Monitor code regeneration frequency
Track remaining code count per user
Next steps