TOTP (time-based one-time password)
TOTP is the second factor FerrisKey challenges for after a password. The user’s authenticator app and FerrisKey share a secret and derive the same six-digit code from it every 30 seconds, as described in RFC 6238. Any compliant app works: Google Authenticator, Authy, 1Password, Bitwarden.
Parameters
The parameters are fixed. The otpauth:// URI FerrisKey hands out always carries them.
| Parameter | Value |
|---|---|
algorithm | SHA-1 |
digits | 6 |
period | 30 seconds |
| Secret | 20 random bytes, base32-encoded |
issuer | <base URL>/realms/<realm_name>, the realm’s issuer URL |
| Label | The user’s email address |
There is no per-realm setting for the issuer name an authenticator app displays.
Enrolment
A user gets an authenticator in one of two ways, and both end in the same credential. At sign-in, the configure_otp required action sends them through the steps below. From the account page, they follow the self-service flow described in Account security, which needs a fresh password check first.
Enrolment at sign-in
Start the enrolment
The portal calls GET /login-actions/setup-otp with the step token. FerrisKey generates the secret and keeps it on the server for 5 minutes. The response carries secret, otpauth_url and issuer.
Show the QR code
The client renders otpauth_url as a QR code. The URI has this shape, with the label and the issuer URL-encoded:
otpauth://totp/alice%40example.com?secret=JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP&issuer=https%3A%2F%2Fiam.example.com%2Frealms%2Facme&algorithm=SHA1&digits=6&period=30 Confirm with a code
The user scans the code and types the six digits the app shows. The client sends POST /login-actions/verify-otp with the code and a label for the device. The request does not repeat the secret: FerrisKey reads the one it stored, so the client cannot choose its own.
The stored enrolment is single-use. The call consumes it whether or not the code is right, so a wrong code means starting again from the first step. FerrisKey also refuses the call with a 403 unless configure_otp is actually due for this sign-in.
Credential stored
On a valid code, FerrisKey saves the credential (type otp), replaces any authenticator the user already had, removes the configure_otp required action and lets the sign-in continue.
Verification at sign-in
After the password, a user who has an authenticator gets this:
POST /login-actions/authenticateanswers withstatus: "RequiresOtpChallenge"and the user’semail, and sets the step token cookie.- The client collects the six-digit code.
- The client sends
POST /login-actions/challenge-otpwith{ "code": "123456" }. The code must be exactly six characters. - FerrisKey accepts the code of the current 30-second window and of the windows just before and after it, so a device whose clock is one step off still passes.
- On success the response carries the
urlwith the authorization code, or therequired_actionsstill due.
Time synchronization
TOTP depends on FerrisKey and the authenticator app agreeing on the time. If the server clock drifts by more than one step, valid codes fail. Keep the server clock synchronized with NTP.
Lockout
A wrong code counts as a failed sign-in. It feeds the same counter as a wrong password: once the counter reaches the realm’s lockout_threshold (10 by default), the account is locked for lockout_duration_seconds (900 by default). A locked account is refused at the challenge, and every other sign-in. A correct code resets the counter.
Where the secret is stored
The secret lives in the credential’s secret_data column. FerrisKey seals it with AES-256-GCM only when SECRETS_ENCRYPTION_KEYS is set. Without the variable the base32 secret is stored in clear, and so is a secret that is still waiting for its first code. See Secrets at rest for the key format and rotation.
Making TOTP mandatory
Set require_mfa on the realm, or on a role. Any user it covers who has no authenticator picks up configure_otp at the next sign-in and cannot finish signing in until they clear it. You can also add configure_otp to a single user’s required actions from the console or the user API.
Losing the authenticator
A user who still holds a recovery code can answer the challenge with it. Otherwise an administrator deletes the one-time password credential from the user’s page in the console, and the user enrols a new authenticator.