Two-factor codes
PHX has TOTP (the six-digit codes of authenticator apps) and QR codes built in:
totp_secret(), qr_svg() and totp_verify(), which tolerates a little clock
drift and tells you which time step matched so the same code cannot be accepted twice.
Scan this with Google Authenticator, 1Password, Authy or similar, then enter the code it shows.
Or type the secret: EL5UDPJ5H4HA33QB4MFF7NHNIHWRYVCW
No authenticator app at hand?
The current code for this secret is 357324 (from totp_code($secret)).
A real site would never show it.
The secret lives only in your server-side session and is gone when it expires. The QR code is drawn as SVG on the server; its markup contains only the modules, not the text.
Source of this page
Read from disk by lib/source.phx as the page is served.
public/totp.phx
<?phx
import std.http.{request, response, session};
import lib.client.client_ip;
import lib.source.sources;
$req = request();
$s = session();
if ($req->isPost() && $req->form("action") == "reset") {
$s->remove("totp_secret");
$s->remove("totp_step");
response()->redirect("/totp");
return;
}
// A secret per visitor, kept in the server-side session.
$secret = $s->get("totp_secret") ?? "";
if ($secret->isEmpty()) {
$secret = totp_secret();
$s->set("totp_secret", $secret);
}
?string $result = null;
$ok = false;
if ($req->isPost()) {
$code = ($req->form("code") ?? "")->replace(" ", "");
if (!rate_limit("totp:" . client_ip(), 10, 60)) {
$result = "Too many attempts; please wait a minute.";
} else {
$step = totp_verify($secret, $code); // ?int: the time step it matched
$last = ($s->get("totp_step") ?? "")->toInt() ?? 0;
if ($step == null) {
$result = "That code is not valid for this secret.";
} elseif ($step <= $last) {
$result = "That code was already used. Wait for the next one.";
} else {
$s->set("totp_step", (string)$step); // so the same code cannot be used twice
$result = "Correct! The code matched time step {$step}.";
$ok = true;
}
}
}
$uri = "otpauth://totp/" . rawurlencode("PHX demo:visitor")
. "?" . http_build_query(["secret" => $secret, "issuer" => "PHX demo"]);
$body = render(
"views/totp.phx.html",
secret: $secret,
qr: qr_svg($uri),
current: totp_code($secret),
result: $result,
ok: $ok,
);
echo render(
"views/layout.phx.html",
title: "Two-factor codes",
path: "/totp",
body: $body,
files: sources(["public/totp.phx", "views/totp.phx.html"]),
);
views/totp.phx.html
<?phx params(string $secret, Html $qr, string $current, ?string $result, bool $ok) ?>
<h1>Two-factor codes</h1>
<p class="lead">PHX has TOTP (the six-digit codes of authenticator apps) and QR codes built in:
<code>totp_secret()</code>, <code>qr_svg()</code> and <code>totp_verify()</code>, which tolerates a little clock
drift and tells you which time step matched so the same code cannot be accepted twice.</p>
<div class="totp">
<div class="qr"><?= $qr ?></div>
<div>
<p>Scan this with Google Authenticator, 1Password, Authy or similar, then enter the code it shows.</p>
<p class="muted small">Or type the secret: <code class="secret"><?= $secret ?></code></p>
<?phx if ($result != null): ?>
<div class="notice <?= $ok ? "ok" : "error" ?>"><?= $result ?></div>
<?phx endif; ?>
<form method="post" action="/totp" class="inline">
<?= csrf_field() ?>
<label for="code">Code</label>
<input id="code" name="code" inputmode="numeric" autocomplete="one-time-code" maxlength="7" size="8" required>
<button>Verify</button>
</form>
<details>
<summary>No authenticator app at hand?</summary>
<p>The current code for this secret is <code><?= $current ?></code> (from <code>totp_code($secret)</code>).
A real site would never show it.</p>
</details>
<form method="post" action="/totp">
<?= csrf_field() ?>
<input type="hidden" name="action" value="reset">
<button class="secondary">New secret</button>
</form>
</div>
</div>
<p class="muted small">The secret lives only in your server-side session and is gone when it expires. The QR code
is drawn as SVG on the server; its markup contains only the modules, not the text.</p>