Skip to content

TOTP (Authenticator Apps)

Time-based One-Time Passwords (TOTP) allow users to authenticate using 6-digit codes generated by apps like Google Authenticator, Authy, or 1Password.

To use TOTP, enable the totp feature in your Cargo.toml:

[dependencies]
authkestra-engine = { version = "0.9", features = ["totp"] }

In Authkestra, TOTP is implemented via the TotpAuthMethod struct. You initialize it by passing a CredentialStore where the secrets will be securely saved.

Because a 6-digit TOTP code alone is generally insufficient as a primary credential, it is strongly recommended to register TOTP as an MFA-only method. You do this by registering it with the EngineBuilder using .with_mfa_method().

use authkestra::Authkestra;
use authkestra_engine::auth::totp::TotpAuthMethod;
// `my_store` implements the `CredentialStore` trait (e.g. `SqlxCredentialStore`)
let engine = Authkestra::builder()
.with_mfa_method(TotpAuthMethod::new(my_store)) // Step-up only!
.build();

By registering it this way, the Engine will automatically block any attempt to use TOTP directly for primary authentication (enforcing that it can only be submitted in an MfaChallenge after a primary method like a password is authenticated).

To enroll a user, keep a reference to your TotpAuthMethod and call register_totp. It automatically generates a new base32 secret and an otpauth:// provisioning URI. The secret is saved to the store internally.

let (secret_b32, uri) = totp_method.register_totp(
"user_123", // user ID
"Authkestra", // Issuer (shows in app)
"alice@example.com" // Account name
).await?;

You can convert the returned uri into a QR code using a frontend library (like qrcode.js) for the user to scan.

Because TOTP is registered as an MFA-only method, you cannot simply pass AuthInput::Totp to engine.authenticate(). If you try, you will receive an AuthError::Internal explaining that the method is not registered for primary authentication.

Instead, TOTP must be provided in response to an AuthResult::MfaRequired returned by a primary authentication method (such as a password).

Using TOTP as a Second Factor (MFA Continuation)

Section titled “Using TOTP as a Second Factor (MFA Continuation)”

If the user logged in with a password and they have TOTP enrolled, the engine will return AuthResult::MfaRequired containing an mfa_token. You complete the login by sending an MfaChallenge:

let result = engine.authenticate(AuthInput::MfaChallenge {
mfa_token: previous_mfa_token,
challenge_input: Box::new(AuthInput::Totp {
user_id: "user_123".to_string(),
code: "123456".to_string(),
}),
}).await?;

Authkestra strictly adheres to RFC 6238. It automatically tracks the highest last_used_step in the credential store, preventing the exact same code from being submitted more than once within the validity window. This completely mitigates replay attacks!