Skip to main content
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:
kratos.yml
selfservice:
  methods:
    lookup_secret:
      enabled: true
Typically used alongside other 2FA methods:
kratos.yml
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.
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": {
              "password": {
                "identifier": true
              }
            }
          }
        }
      },
      "required": ["email"]
    }
  }
}

User flows

Generating lookup secrets

1

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>"
2

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-id> \
  -H "Content-Type: application/json" \
  -H "Cookie: ory_kratos_session=<session-token>" \
  -d '{
    "method": "lookup_secret"
  }'
3

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"
  ]
}
4

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

1

First-factor authentication

User logs in with their primary credential:
curl -X POST https://your-kratos-instance/self-service/login?flow=<flow-id> \
  -H "Content-Type: application/json" \
  -d '{
    "method": "password",
    "identifier": "[email protected]",
    "password": "user-password"
  }'
2

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

Submit recovery code

User submits one of their saved recovery codes:
curl -X POST https://your-kratos-instance/self-service/login?flow=<flow-id> \
  -H "Content-Type: application/json" \
  -d '{
    "method": "lookup_secret",
    "lookup_secret": "a1b2c3d4"
  }'
4

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-id> \
  -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-id> \
  -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-id> \
  -H "Content-Type: application/json" \
  -H "Cookie: ory_kratos_session=<session-token>" \
  -d '{
    "method": "lookup_secret",
    "lookup_secret_disable": true
  }'

Implementation details

Code format

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:
  1. Display codes immediately after generation
  2. Require user confirmation that codes are saved
  3. Never email codes (email is not secure)
  4. Hash codes before database storage
  5. 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:
kratos.yml
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:
  1. User loses phone with TOTP app
  2. User logs in with password
  3. Uses recovery code for second factor
  4. Can then disable TOTP and set up new 2FA

Travel or device unavailability

Backup when primary 2FA is unavailable:
  1. User traveling without security key
  2. Uses recovery code as alternative 2FA
  3. Can resume normal 2FA when device is available

Emergency access

Provide emergency access method:
  1. User stores codes in secure location (safe, password manager)
  2. Codes available for emergency account access
  3. 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)

AAL2 form hydrator

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

selfservice:
  methods:
    password:
      enabled: true
    lookup_secret:
      enabled: true

Best practices

User education

Educate users about recovery codes:
  1. Explain they are one-time use
  2. Emphasize secure storage
  3. Recommend saving in password manager
  4. Suggest printing and storing physically
  5. 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

Build docs developers (and LLMs) love