Skip to content

Security

An overview of the security measures built into bluefox-auth.

Password handling

  • bcrypt — passwords are hashed with bcrypt directly (no wrapper libraries)
  • 72-byte limit — bcrypt silently truncates at 72 bytes; bluefox-auth validates this at the schema level and rejects passwords exceeding the limit
  • Minimum 8 characters — enforced on registration and password reset
  • Timing-safe login — when a user is not found, a dummy bcrypt hash is still verified to prevent timing-based enumeration

JWT tokens

All tokens include:

  • jti — unique token ID (UUID hex)
  • iat — issued-at timestamp
  • exp — expiration timestamp
  • aud — audience claim (default: "bluefox")
  • type — token type (access, refresh, password_reset, email_verify)
  • sub — user ID

Token types are enforced on decode — a refresh token cannot be used as an access token, and vice versa.

Refresh token rotation

Each login creates a token family. When a refresh token is used:

  1. The old token is marked as revoked
  2. A new token is issued in the same family
  3. The new token is stored server-side in the refresh_tokens table

Reuse detection: if a revoked token is replayed, the entire family is revoked — invalidating all tokens from that login session. This protects against token theft scenarios where an attacker and legitimate user race to use the same token.

When using cookie transport, tokens are set with:

Cookie HttpOnly Secure SameSite Path
Access token Yes Yes* Lax /
Refresh token Yes Yes* Lax Auth prefix
CSRF token No Yes* Lax /

*Secure defaults to True. Set cookie_secure=False for local development.

The refresh token cookie is scoped to the auth prefix path, limiting its exposure to only the auth endpoints.

CSRF protection

bluefox-auth uses the plain double-submit cookie pattern:

  1. On login/refresh, a CSRF token is set as a non-HttpOnly cookie (so JavaScript can read it)
  2. For mutating requests (POST/PUT/PATCH/DELETE) using cookie transport, the client must send the CSRF token value in the X-CSRF-Token header
  3. The server compares the cookie value and header value using secrets.compare_digest

Bearer requests bypass CSRF — the Authorization: Bearer <token> header is proof of JavaScript access to the token, which a CSRF attacker wouldn't have.

Safe methods (GET, HEAD, OPTIONS) skip CSRF validation — they shouldn't have side effects.

Secret key validation

AuthSettings validates the secret key on startup:

  • Must be at least 32 characters
  • Cannot be empty or a known placeholder ("change-me-in-production", "secret", "changeme")
  • Excluded from repr() and model_dump() to prevent accidental logging

Password reset

  • Silent on unknown email — always returns the same response regardless of whether the email exists, preventing enumeration
  • Silent on inactive users — no email is sent for inactive accounts
  • One-time use — enforced by checking updated_at > created_at (the record was modified after initial creation) and updated_at >= iat (the modification happened after the token was issued); once the password is changed, the token cannot be replayed
  • 1 hour expiry — tokens expire after 60 minutes
  • Type-checked — password reset tokens cannot be used for email verification, and vice versa

Email verification

  • One-time use — once email_verified is set to True, the same token cannot be used again
  • 24 hour expiry — tokens expire after 24 hours
  • Requires authentication — the request endpoint requires a valid access token
  • Short-circuits — if the user is already verified, returns immediately without sending another email