Skip to main content
TOTP (Time-based One-Time Password) provides time-based two-factor authentication using authenticator apps like Google Authenticator, Authy, or 1Password.

Overview

The TOTP strategy enables:
  • Second-factor authentication (2FA/MFA)
  • QR code registration for authenticator apps
  • 6-digit time-based codes
  • Backup via recovery codes
  • Multiple TOTP credentials per identity
TOTP is exclusively a second-factor method (AAL2). Users must have a first-factor method (like password or passkey) configured.

Configuration

Enable TOTP

Configure TOTP in your Kratos configuration:
kratos.yml
selfservice:
  methods:
    totp:
      enabled: true
      config:
        issuer: Your Application Name
The issuer field appears in the authenticator app next to the account name.

Complete configuration example

kratos.yml
selfservice:
  methods:
    password:
      enabled: true
    totp:
      enabled: true
      config:
        issuer: Example App

session:
  whoami:
    required_aal: highest_available
Setting required_aal: highest_available ensures that if a user has TOTP configured, they must use it to access protected resources.

Identity schema configuration

Configure TOTP account name

Define which trait serves as the TOTP account 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": {
              "password": {
                "identifier": true
              },
              "totp": {
                "account_name": true
              }
            }
          }
        }
      },
      "required": ["email"]
    }
  }
}
The field marked with "account_name": true is displayed in the authenticator app alongside the issuer. See selfservice/strategy/totp/schema_extension.go:26-28 for the schema extension implementation.

User flows

Setting up TOTP

1

Initialize settings flow

User must be authenticated with a first-factor method:
curl -X GET https://your-kratos-instance/self-service/settings/browser \
  -H "Cookie: ory_kratos_session=<session-token>"
2

Request TOTP setup

The settings flow includes TOTP setup nodes with a QR code and secret.The response contains:
  • totp_qr - Base64-encoded QR code image
  • totp_secret_key - Secret key for manual entry
  • totp_url - Full TOTP URI
3

Scan QR code

User scans the QR code with their authenticator app.The QR code contains a URI like:
otpauth://totp/Example%20App:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Example%20App
4

Verify TOTP code

User submits the 6-digit code from their authenticator:
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": "totp",
    "totp_code": "123456"
  }'
5

TOTP activated

Once verified, TOTP is activated for the user’s account.

Login with TOTP

1

First-factor authentication

User logs in with their primary credential (password, passkey, etc.):
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

TOTP challenge

If the user has TOTP configured and AAL2 is required, Kratos presents a TOTP challenge.
3

Submit TOTP code

User submits the current 6-digit code:
curl -X POST https://your-kratos-instance/self-service/login?flow=<flow-id> \
  -H "Content-Type: application/json" \
  -d '{
    "method": "totp",
    "totp_code": "123456"
  }'
4

AAL2 session created

On success, the session is elevated to AAL2.

Disabling TOTP

Users can remove TOTP through settings:
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": "totp",
    "totp_unlink": true
  }'

TOTP implementation details

Secret generation

TOTP secrets are generated with (see selfservice/strategy/totp/generator.go:27-39):
  • Secret size: 160 bits (20 bytes) as recommended by RFC 4226
  • Digits: 6-digit codes
  • Period: 30 seconds
  • Algorithm: SHA1 (HMAC-SHA1)
The secret is generated using crypto/rand for cryptographic security.

QR code generation

QR codes are generated as PNG images and base64-encoded (see selfservice/strategy/totp/generator.go:47-59):
func KeyToHTMLImage(key *otp.Key) (string, error) {
    var buf bytes.Buffer
    img, err := key.Image(256, 256)
    if err != nil {
        return "", errors.WithStack(err)
    }
    
    if err := png.Encode(&buf, img); err != nil {
        return "", errors.WithStack(err)
    }
    
    return "data:image/png;base64," + 
           base64.StdEncoding.EncodeToString(buf.Bytes()), nil
}
The result is a data URI that can be directly embedded in HTML.

Account name resolution

The account name is extracted from identity traits (see selfservice/strategy/totp/schema_extension.go:19-34):
type SchemaExtension struct {
    AccountName string
    l           sync.Mutex
}

func (r *SchemaExtension) Run(
  _ jsonschema.ValidationContext, 
  s schema.ExtensionConfig, 
  value interface{},
) error {
    r.l.Lock()
    defer r.l.Unlock()
    if s.Credentials.TOTP.AccountName {
        r.AccountName = fmt.Sprintf("%s", value)
    }
    return nil
}

Security considerations

Time synchronization

TOTP requires accurate time synchronization between server and client devices. Ensure your server’s clock is synchronized via NTP.
  • TOTP codes are valid for 30 seconds
  • Clock drift can cause authentication failures
  • Most implementations allow ±1 time window for tolerance

Secret security

Protect TOTP secrets:
  1. Secrets are stored hashed in the database
  2. QR codes should only be displayed over HTTPS
  3. Users should not share QR codes or secret keys
  4. Consider time-limiting QR code display

Backup codes

Always configure backup authentication methods:
kratos.yml
selfservice:
  methods:
    totp:
      enabled: true
      config:
        issuer: My App
    lookup_secret:
      enabled: true  # Enable backup/recovery codes
See Lookup secrets for recovery code setup.

AAL2 enforcement

To require TOTP when configured:
kratos.yml
session:
  whoami:
    required_aal: highest_available
Options:
  • aal1 - Only first-factor required
  • aal2 - Always require second factor
  • highest_available - Require AAL2 if user has it configured

Authenticator app compatibility

TOTP works with any RFC 6238-compliant authenticator:
  • Google Authenticator
  • Microsoft Authenticator
  • Authy
  • 1Password
  • Bitwarden
  • LastPass Authenticator
  • FreeOTP
  • And many others

API reference

Strategy implementation

The TOTP strategy is implemented as (see selfservice/strategy/totp/strategy.go:77-79):
type Strategy struct{ d dependencies }

func NewStrategy(d dependencies) *Strategy { 
    return &Strategy{d: d} 
}
  • Strategy ID: totp (as identity.CredentialsType)
  • Node Group: totp group in UI nodes
  • AAL Level: AAL2 (second factor only)

Credential counting

TOTP is only counted as a second-factor credential (see selfservice/strategy/totp/strategy.go:81-99):
func (s *Strategy) CountActiveFirstFactorCredentials(
  _ context.Context, 
  _ map[identity.CredentialsType]identity.Credentials,
) (count int, err error) {
    return 0, nil  // TOTP is 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.CredentialsTOTPConfig
            if err = json.Unmarshal(c.Config, &conf); err != nil {
                return 0, errors.WithStack(err)
            }
            
            _, err := otp.NewKeyFromURL(conf.TOTPURL)
            if len(conf.TOTPURL) > 0 && err == nil {
                count++
            }
        }
    }
    return
}

Configuration reference

selfservice:
  methods:
    password:
      enabled: true
    totp:
      enabled: true
      config:
        issuer: My App

Troubleshooting

Code not accepted

If TOTP codes are consistently rejected:
  1. Check server time synchronization (use NTP)
  2. Verify the issuer and account name match
  3. Ensure the secret wasn’t corrupted
  4. Try re-registering the TOTP credential

Lost authenticator device

If users lose their authenticator:
  1. Use lookup secrets for backup access
  2. Implement account recovery flows
  3. Allow TOTP removal via account recovery

Next steps

Build docs developers (and LLMs) love