---
title: "Make sign-in and reset links single-use · Agent Wranglers"
url: https://agentwranglers.se/playbook/accounts/sign-in-methods/sign-in-links
updated: 2026-10-21
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Make sign-in and reset links single-use and short-lived

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Sign-in methods](https://agentwranglers.se/playbook/accounts/sign-in-methods.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers). Updated 2026-10-21.

- ID: ACC-69
- Level: Required
- Chapter: [Sign-in methods](https://agentwranglers.se/playbook/accounts/sign-in-methods)
- Rule: A link that signs someone in or resets a password works once, expires within an hour, and holds a long random token. A sign-in code expires within 15 minutes.
- Risk: An account or system taken over
- Applies when: The project sends email, or has user accounts.
- How: Make the token at least 128 random bits from a secure random source. Store only a hash of the token, tied to the account and the request. Mark it used on first success. A typed sign-in code dies after 5 wrong tries, and the person asks for a new one. Open a page with a button instead of acting on the first visit, because mail scanners open links on their own. Send `Referrer-Policy: no-referrer` on that page so the token never leaks. A link only proves access to the inbox. NIST SP 800-63B forbids email as an authentication factor, so ask for the second factor too where the account has one.
- Check: Use a reset link twice. The second use fails. Wait past the expiry with a fresh link, and it fails too. Fetching the link with `curl` changes nothing.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### Plain text example

```text
GET  /reset?token=…   → shows "Set a new password" form, changes nothing
POST /reset           → checks token, sets password, marks token used
```

### JavaScript example

```js
import { randomBytes, createHash } from 'node:crypto';

const token = randomBytes(32).toString('base64url');
const tokenHash = createHash('sha256').update(token).digest('hex');
await db.saveResetToken({ userId, tokenHash, expiresAt: Date.now() + 60 * 60 * 1000 });
```

### Python example

```python
import hashlib, secrets, time

token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(token.encode()).hexdigest()
db.save_reset_token(user_id=user_id, token_hash=token_hash, expires_at=time.time() + 60 * 60)
```

## Sources

- [OWASP Forgot Password Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html)
- [NIST SP 800-63B, Authentication and Authenticator Management](https://pages.nist.gov/800-63-4/sp800-63b.html)

## Related rules

- [Keep personal data out of addresses](https://agentwranglers.se/playbook/website/privacy/personal-data-out-of-urls) in Website
- [Keep secrets out of addresses](https://agentwranglers.se/playbook/api-design/api-security/no-secrets-in-urls) in API design
- [Retry with backoff and give up in time](https://agentwranglers.se/playbook/email/unsubscribe-and-bounces/retry-and-expire) in Email
- [Make password resets safe](https://agentwranglers.se/playbook/adversarial-testing/access-and-accounts/password-reset) in Security testing
- [Make tokens from a secure random source](https://agentwranglers.se/playbook/adversarial-testing/access-and-accounts/secure-random-tokens) in Security testing
- [Prove the email address before trusting it](https://agentwranglers.se/playbook/accounts/sign-up/email-verification) in Accounts
- [Weigh email links before making them the only way in](https://agentwranglers.se/playbook/accounts/sign-in-methods/email-sign-in-trade-offs) in Accounts
- [Make invitations expire and work once](https://agentwranglers.se/playbook/accounts/organisations-and-teams/invitations) in Accounts
- [Keep the old email until the new one is confirmed](https://agentwranglers.se/playbook/accounts/profile-and-account-lifecycle/email-change) in Accounts
- [Format code texts for automatic fill](https://agentwranglers.se/playbook/notifications/text-messages/sms-code-autofill) in Notifications

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/accounts/sign-in-methods/sign-in-links">Agent Wranglers accounts playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Hold every sign-in path to the same checks](https://agentwranglers.se/playbook/accounts/sign-in-methods/same-checks-every-path)
- [Sign-in methods](https://agentwranglers.se/playbook/accounts/sign-in-methods)
