Password-reset, confirmation, and invitation tokens must be unpredictable. Python's secrets module uses the operating system's secure randomness source and is the right choice instead of random for authentication material.

Generate a URL token

import hashlib
import secrets
from datetime import UTC, datetime, timedelta

token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(token.encode()).hexdigest()
expires_at = datetime.now(UTC) + timedelta(minutes=20)

Send the original token once and store only its hash, purpose, user, and expiration. When it returns, hash it and find an unused valid record. Mark it used in the same transaction as the protected change.

Do not log tokens or send them to analytics. Prevent URL leakage through referrers, require HTTPS, and rate-limit attempts. The Python API security guide covers related controls.

secrets.compare_digest() reduces timing differences in direct comparisons. Choose an explicit byte count appropriate to the threat model instead of assuming the library default will never change. Tokens do not replace password hashing, MFA, expiration, or revocation.

The official secrets documentation, accessed July 22, 2026, defines token_bytes, token_hex, token_urlsafe, and compare_digest. Security comes from the complete lifecycle, not merely generating a random string.

Why random is unsuitable

The random module produces pseudorandom sequences for simulation and sampling. Its state can be reconstructed when an attacker learns enough output, and a time-based seed may shrink the search space. It is therefore unsuitable for reset links, API keys, session cookies, invitations, and security nonces.

secrets delegates generation to the operating system's cryptographically secure source. Its compact API avoids fragile choices about seeds and algorithms. It does not secure the whole workflow: expiration, storage, transport, and authorization remain application responsibilities.

Choose the right representation

token_bytes(n) returns raw bytes for protocols or internal processing. token_hex(n) encodes each byte as two hexadecimal characters, so 32 bytes become 64 characters. token_urlsafe(n) uses URL-safe Base64 and produces roughly 1.3 characters per byte.

The argument is a byte count, not final text length. Choose it explicitly and document the threat model. Thirty-two bytes offer a generous margin for many high-value online tokens, though regulatory and product requirements may differ.

raw_value = secrets.token_bytes(32)
hex_value = secrets.token_hex(32)
url_value = secrets.token_urlsafe(32)

Do not truncate output to fit a short database column because truncation removes entropy. Change the schema and validate the limits of the URL, header, or field that carries the value.

Design the complete reset lifecycle

Return equivalent responses for existing and nonexistent accounts to reduce user enumeration. When an account exists, generate a token, compute its digest, and store its purpose, user, expiration, and unused state. Send the original value once through the intended channel.

On confirmation, reject excessive or malformed input before querying storage. Compute the same digest and verify purpose, user, expiration, and unused state. Change the password and mark the token consumed in one transaction. Revoke existing sessions when the product's policy requires it.

def token_digest(value: str) -> str:
    return hashlib.sha256(value.encode("utf-8")).hexdigest()


def expired(instant: datetime, now: datetime) -> bool:
    return instant <= now

A fast hash can digest a high-entropy random token because there is no low-entropy human secret to guess. Passwords require purpose-built, slow, configurable algorithms such as Argon2, scrypt, or bcrypt.

Prevent transport and log leakage

Use HTTPS throughout. Query-string tokens can enter browser history, proxy logs, monitoring systems, and referrer headers. A confirmation page can exchange the token for temporary server-side state and redirect to a clean URL. Apply a restrictive referrer policy and avoid third-party resources on the page that receives the secret.

Never put the value in errors, metrics, or traces. Central redaction helps, but handling code should avoid logging it at all. Rate-limit attempts by account, source, and token without creating an easy denial-of-service path against the victim. Alerts should record anomalous behavior without retaining credentials.

Compare values and create short codes

secrets.compare_digest(a, b) reduces content-related timing variation when comparing values of the same type. It is useful for MACs, digests, and short secrets already available to the application. Constant-time comparison does not correct earlier differences, such as messages that reveal whether an account exists.

When a channel requires a numeric code, secrets.randbelow() avoids bias from improvised transformations:

code = f"{secrets.randbelow(1_000_000):06d}"

A six-digit code has only one million possibilities. It therefore needs a short lifetime, strict attempt limits, and binding to one user and purpose. secrets.choice() can securely select from a defined alphabet, but the resulting space must still be large enough.

Test without weakening production

Tests should not expect exact output from secrets. Check format, approximate length, and lifecycle behavior. Inject a clock to test expiration. If a persistence test needs a known token, replace the generator at the application boundary; never introduce a predictable production seed.

Test concurrency too. Two simultaneous confirmations must not consume one token twice, so storage needs an atomic update or constraint. Cover expired values, wrong purpose, wrong user, excessive input, and already consumed records. These checks test the properties that matter instead of treating one random sample as proof of security.

Production checklist

Document who may issue, validate, and revoke each token type. Different purposes should use distinguishable records or namespaces so an invitation cannot be accepted as a password reset. Confirm that validation checks the associated user and purpose, not only whether a digest exists.

Use timezone-aware server timestamps. The client must never choose expiration. Remove expired records through a controlled job while retaining only what policy and incident investigation require.

Inspect every observability path: web server, proxy, email provider, error monitoring, and analytics. The token must not appear in any of them. Review cache headers, redirects, and error pages too. Finally, document incident actions for suspected leakage, including bulk revocation, communication, rotation of related credentials, and the minimum evidence needed for investigation. Secure generation is valuable only when operations can contain a failure.

Repeat this review whenever the delivery channel, provider, or URL format changes. A small operational change can create a new leakage point. Confirm that backups and database replicas protect stored digests according to their sensitivity and retention policy.

Keep token issuance auditable without recording the token itself. Useful fields include event time, token type, internal user reference, expiration, and outcome. Restrict access to those records and avoid turning an audit trail into a second source of personal data.

Review those permissions periodically and delete audit records when their justified retention period ends.