Osy#betaRun it on Osyrin: osyrin.com

Reference / Function

Totp.NewSecret, Totp.Uri, Totp.Verify, Totp.Code

Totp.NewSecret() → string · Totp.Uri(secret, issuer, account) → string · Totp.Verify(secret, code, lastUsedStep) → long · Totp.Code(secret[, at]) → string

Authenticator-app codes (RFC 6238 — the six-digit, thirty-second codes every authenticator app shows), which is what a second factor is built on. `NewSecret` mints the secret, `Uri` is the address its QR code carries, and `Verify` checks a code and answers the step it matched, so a code used once is refused if it comes again.

preview1 example compiled by CIfunctionstdlibsecurityauthentication

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) — the otpauth://totp/… address to show as a QR code (draw it with Qr.Encode); issuer is your app's name in the phone's list, account which 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) -> string

Description#

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 step

Keeping 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#

Related

Qr.Encode

A QR code for a piece of text, as rows of modules a page draws with ordinary boxes — level M, the smallest version that…

The UserAccounts kit — sign-in done properly, once

`Osysharp.UserAccounts` is the default way into an app: sign-in with a password or a provider (Google, GitHub, …)…

Security.* — hashing, verifying, tickets, random ids

The calls an authentication flow needs: `HashPassword` (salted, one-way), `VerifyPassword` (constant-work comparison…

Crypto.Encrypt and Crypto.Decrypt

Encrypt and decrypt values under your app's key, which the platform mints, protects, and rotates for you. There is no…