Token Security & Lifecycle Lifecycle
Relevant source files
The following files were used as context for generating this wiki page:
The token security and lifecycle system within the ProductCompare project governs the generation, storage, and validation of authentication and authorization tokens. It is designed around a "modular monolith" architecture using Phoenix and Elixir, emphasizing the transition from legacy REST endpoints to a GraphQL-first authentication contract. The system enforces strict security boundaries by ensuring that raw secrets are never persisted and that token validity is managed through explicit state transitions and hashed verification.
This framework handles various token types, including session tokens, refresh tokens, and single-use tokens for email verification and password resets. All browser-facing auth flows are orchestrated via GraphQL over /api/graphql, with Phoenix serving as the underlying cookie-backed session authority.
Sources: backend_starting_guide.md:1-10, AGENTS.md:17-25, ARCHITECTURE.md:8-12
Token Storage and Security Contract
Tokens are stored in the PostgreSQL database using specific conventions to ensure high entropy and cryptographic security. Each token record is tied to a unique identifier and a hashed secret, preventing unauthorized access even in the event of a database compromise.
Data Model for Tokens
Tokens are persisted with the following fields and constraints:
- ID: A database-generated
UUIDv7(entropy ID). - Secret Hash: A binary representation of the token secret, hashed using
SHA3-256. - Context: An enum defining the token's purpose (e.g., session, recovery).
- Metadata: Optional fields such as
sent_toandauthenticated_at.
Sources: backend_starting_guide.md:105-112
Cryptographic Handling
The system follows a strict "never persist raw secrets" rule. The lifecycle of a token's secret follows these steps:
- Generation: A 32-byte random secret is generated.
- Hashing: The secret is hashed using
:crypto.hash(:sha3_256, raw_secret). - Persistence: Only the hash is saved to the database.
- Serialization: The transport token is formatted as
"<uuid>.<base64url(raw_secret_without_padding)>". - Validation: Incoming tokens are validated using
Plug.Crypto.secure_compare/2to prevent timing attacks.
Sources: backend_starting_guide.md:113-121
Token Lifecycle and Validity Windows
The project defines explicit validity windows for different token contexts to balance user convenience with security.
| Token Context | Validity Duration | Description |
|---|---|---|
| Access / Session | 14 Days | Standard duration for a logged-in session. |
| Refresh Token | 30 Days | Used to rotate access tokens. |
| Email Magic Link | 15 Minutes | Short-lived link for immediate login/recovery. |
| Email Verification | 7 Days | Window for a user to confirm their account. |
Sources: backend_starting_guide.md:126-131
Rotation and Revocation
The system implements single-use refresh rotation. When a refresh token is used, the old token is consumed atomically, and a new access+refresh pair is issued. Revocation is treated as an idempotent action at the API boundary. The system also records append-only auth_events for significant lifecycle actions, providing an audit trail.
Sources: backend_starting_guide.md:136-141
Authentication Flow and Transport
Authentication is primarily handled via GraphQL mutations. The system supports two primary transport mechanisms: Bearer tokens and session cookies.
Authorization Precedence
When resolving the GraphQL context, the system follows a specific precedence:
- Authorization Header:
Authorization: Bearer ...is checked first. - Session Fallback: If the bearer header is absent, the system falls back to the Phoenix session cookie.
- Strict Failure: If a bearer header is present but malformed/invalid, the system must not fall back to the session.
Sources: backend_starting_guide.md:147-152
Token Exchange Sequence
The following diagram illustrates the sequence for a password reset flow, which utilizes single-use tokens.
Frontend Implementation and Error Handling
The frontend (React/Relay) consumes these tokens via the actions.ts module and route-level logic. It is responsible for extracting tokens from URL search parameters and managing the state during asynchronous mutations.
Error Semantics
The backend provides explicit error reasons that the frontend maps to user-facing alerts.
:invalid_token:expired_token:revoked_token
Sources: backend_starting_guide.md:132-135
Verification Guardrails
In the frontend, routes like ResetPasswordRoute or VerifyEmailRoute implement guards to ensure a token is present before attempting a mutation. If a token is missing from the URL, a local INVALID_TOKEN error is set immediately to disable form submission.
// Example of frontend token validation guard
const missingTokenError: MutationError = {
code: "INVALID_TOKEN",
field: "token",
message: "This reset link is missing or invalid."
};
// ... inside the component
const [errors, setErrors] = useState<MutationError[]>(token ? [] : [missingTokenError]);
Sources: assets/src/routes/auth/reset-password.tsx:11-16, assets/tests/e2e/auth.spec.ts:167-175
Technical Requirements for Implementation
Developers adding new token-based features must adhere to the following architectural rules:
- SHA3-256 Only: No SHA-2 or MD5.
- UUIDv7: Use UUIDv7 for all
users_tokensprimary keys. - Secure Compare: Use constant-time comparison for all secret verification.
- Audit Logging: Every token creation or consumption should be reflected in
auth_events.
Sources: backend_starting_guide.md:18-20, backend_starting_guide.md:113-125
Summary
Token Security & Lifecycle in ProductCompare is a robust system leveraging Elixir's cryptographic libraries and PostgreSQL's UUIDv7 capabilities. By enforcing hashed storage, single-use rotation, and strict transport precedence, the system ensures that user sessions and sensitive operations like password recovery remain secure against common attack vectors while providing clear feedback through structured GraphQL error payloads.