Sign-in methods Recommended
Weigh email links before making them the only way in
Use email links or codes as the only sign-in for low-risk accounts only. Bind each link to the browser that asked for it, and offer a passkey after the first sign-in.
How to do it
An email link proves access to an inbox, not who someone is. NIST SP 800-63B-4 section 3.1.3.1 forbids email for out-of-band authentication. Mail also arrives late, lands in spam, or opens on a phone when sign-in began on a laptop. Set a random cookie in the browser that asks. When the link opens in a browser without that cookie, show a 6-digit code to type in the first browser instead of signing in. Make the token as the sign-in links rule says. Require a passkey or second factor for admins and accounts that hold money or business data.
Example
app.get('/sign-in/link', async (req, res) => {
const request = await signInRequests.findUnusedByTokenHash(sha256(req.query.token));
if (!request) return res.status(410).render('link-expired');
const sameBrowser = safeEqual(req.cookies.signin_nonce ?? '', request.browserNonce);
res.render(sameBrowser ? 'confirm-sign-in' : 'type-this-code', { code: request.code });
});
@app.get("/sign-in/link")
def open_sign_in_link():
sign_in = sign_in_requests.find_unused_by_token_hash(sha256(request.args["token"]))
if sign_in is None:
return render_template("link-expired.html"), 410
same_browser = hmac.compare_digest(request.cookies.get("signin_nonce", ""), sign_in.browser_nonce)
page = "confirm-sign-in.html" if same_browser else "type-this-code.html"
return render_template(page, code=sign_in.code)
How to check
Ask for a link in one browser and open it in another. The second browser is not signed in and shows a code. Typing the code in the first browser signs it in.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers accounts playbook: https://agentwranglers.se/playbook/accounts/sign-in-methods/email-sign-in-trade-offs.