Summary#
Three calls are the whole of an authenticator-app factor:
Totp.NewSecret()— a fresh secret, in base32 (the form an app is given).Totp.Uri(secret, issuer, account)— theotpauth://totp/…address to show as a QR code (draw it withQr.Encode);issueris your app's name in the phone's list,accountwhich account it is.Totp.Verify(secret, code, lastUsedStep)— the step the code matched, or-1. Store the step and pass it back next time: a code for a step at or before it is refused, so a code cannot be used twice.Totp.Code(secret)— the code the phone shows now (Totp.Code(secret, at)at an instant): the device's half, which is how a test drives a second factor end to end.
If what you want is sign-in with a second factor, you do not need these directly — the UserAccounts kit has it, pages and all.
Signature#
Totp.NewSecret() -> string
Totp.Uri(<string> secret, <string> issuer, <string> account) -> string
Totp.Verify(<string> secret, <string> code, <long> lastUsedStep) -> long
Totp.Code(<string> secret) -> string
Totp.Code(<string> secret, <DateTime> at) -> stringDescription#
What it speaks#
Exactly what the authenticator apps people carry speak: HMAC-SHA-1, six digits, a thirty-second step, and a
160-bit secret written in base32. Verify accepts the step before and after the current one (±30 s) for a phone whose
clock drifts, ignores spaces in the code, and treats anything that is not six digits as simply no match. It compares
in fixed time, so how long a wrong code takes says nothing about which digits were right.
Refusing a replayed code#
A code stays valid for up to ninety seconds, so a code read over a shoulder would work a second time inside that
window. Verify answers the step it matched; keep it on the account and hand it back:
var step = Totp.Verify(secret, code, account.LastCodeStep);
if (step < 0) { return false; } // wrong, or already used
account.LastCodeStep = step; // the next code must be for a later stepKeeping the secret#
The secret IS the factor: whoever reads it can make every future code. Store it encrypted with
Crypto.Encrypt (the app's own platform-managed key), decrypt it only to verify, and
deny read the column from everyone but the flow that checks codes. Uri carries the secret too — show it once, to
the person enrolling, and never store it.
Testing a flow that asks for a code#
Totp.Code is what the phone would show, so a test can play the phone. Use it for the step it checks and no other:
check a code with Verify, never by comparing with Code. A code used once is refused next time, so a test that needs
two in a row asks for the next step's — Totp.Code(key, DateTime.UtcNow.AddSeconds(30)) — which Verify's ±1 window
accepts now.
Why it runs on the server#
All four are server-only: a browser that could check a code would have to hold the secret the code proves
possession of. NewSecret draws from the host's RNG and Verify reads the clock, so a durable replay sees the first
run's answers.
Examples#
entity Device {
[MaxLength(400)] string Sealed; // Crypto.Encrypt(secret)
long LastStep = -1;
security { allow read, create, update when IsAuthenticated; }
}
class Enrolment { public string Key = ""; public List<string> Qr = []; }
/// The key to type in and the QR code to scan — shown once.
Enrolment Start(string account) {
var secret = Totp.NewSecret();
new Device { Sealed = Crypto.Encrypt(secret) };
return new Enrolment { Key = secret, Qr = Qr.Encode(Totp.Uri(secret, "Acme", account)) };
}
bool Check(Device device, string code) {
var step = Totp.Verify(Crypto.Decrypt(device.Sealed), code, device.LastStep);
if (step < 0) { return false; }
device.LastStep = step;
return true;
}See also#
- The UserAccounts kit — sign-in done properly, once — two-step sign-in, enrolment and recovery codes, built on these
- Qr.Encode — drawing the enrolment QR code
- Crypto.Encrypt and Crypto.Decrypt — keeping the secret encrypted at rest
- Security.* — hashing, verifying, tickets, random ids — hashing, random ids and the rest of the credential surface