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:
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
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
Define which trait serves as the TOTP account 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" : {
"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
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>"
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
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
Verify TOTP code
User submits the 6-digit code from their authenticator: 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": "totp",
"totp_code": "123456"
}'
TOTP activated
Once verified, TOTP is activated for the user’s account.
Login with TOTP
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-i d > \
-H "Content-Type: application/json" \
-d '{
"method": "password",
"identifier": "[email protected] ",
"password": "user-password"
}'
TOTP challenge
If the user has TOTP configured and AAL2 is required, Kratos presents a TOTP challenge.
Submit TOTP code
User submits the current 6-digit code: curl -X POST https://your-kratos-instance/self-service/login?flow= < flow-i d > \
-H "Content-Type: application/json" \
-d '{
"method": "totp",
"totp_code": "123456"
}'
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-i d > \
-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:
Secrets are stored hashed in the database
QR codes should only be displayed over HTTPS
Users should not share QR codes or secret keys
Consider time-limiting QR code display
Backup codes
Always configure backup authentication methods:
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:
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
Minimal
With AAL2 enforcement
With backup codes
selfservice :
methods :
password :
enabled : true
totp :
enabled : true
config :
issuer : My App
Troubleshooting
Code not accepted
If TOTP codes are consistently rejected:
Check server time synchronization (use NTP)
Verify the issuer and account name match
Ensure the secret wasn’t corrupted
Try re-registering the TOTP credential
Lost authenticator device
If users lose their authenticator:
Use lookup secrets for backup access
Implement account recovery flows
Allow TOTP removal via account recovery
Next steps