Authentication Architecture
This document describes the authentication and authorization architecture of Neemias, covering the complete login flow, tokens, sessions, and route protection.
Overview
Authentication is based on short-lived JWT access tokens combined with long-lived rotating refresh tokens. The backend (Cloudflare Workers) manages session creation, validation, and revocation. The frontend (React SPA) consumes the tokens and maintains the local session in IndexedDB.
Tokens and durations
| Token | Algorithm | TTL | Storage |
|---|---|---|---|
| Access Token | JWT HS256 (jose) | 15 minutes | Memory + IndexedDB (frontend) |
| Refresh Token | Opaque (SHA-256 hash stored) | 7 days | auth_refresh_sessions (backend), IndexedDB (frontend) |
| CSRF Token | Opaque | Session-bound | IndexedDB (frontend), sent as X-CSRF-Token |
Authentication flow
1. Login
POST /api/v1/auth/login { email, password }- Frontend sends
emailandpasswordto the backend.- If the user types a short identifier (
admin,chamador,relatorios,cadastro), the frontend normalizes it to the corresponding email (admin@neemias.local, etc.).
- If the user types a short identifier (
- Backend validates the password using PBKDF2 (100,000 iterations, SHA-256).
- Hash format:
pbkdf2:<hexSalt>:<hexHash>.
- Hash format:
- Backend creates an auth session:
- Generates refresh token (opaque).
- Stores SHA-256 hash of the refresh token in
auth_refresh_sessions. - Generates CSRF token bound to the session.
- Signs JWT HS256 access token with payload:
userId,role,sessionId,csrfToken.
- Backend returns:
accessTokenaccessTokenExpiresAtrefreshTokencsrfTokenuser(payload withuserId,displayName,role,status)
- Frontend persists session metadata in IndexedDB (
sessionstable) and user data inusers.
2. Authenticated requests
Every authenticated request must include:
| Header | Required | Description |
|---|---|---|
Authorization: Bearer <accessToken> | Yes | JWT access token |
X-Device-Id | Yes | Device identifier (required on all requests) |
X-CSRF-Token | Yes (cookie mode) | CSRF token for mutation routes when AUTH_SESSION_MODE=cookie |
Idempotency-Key | Yes (mutations) | Idempotency key for POST/PATCH |
3. Refresh (token rotation)
POST /api/v1/auth/refresh { refreshToken }- Frontend detects expired access token (or near expiration) and calls the refresh endpoint.
- Backend validates the refresh token hash in
auth_refresh_sessions. - Backend applies refresh token rotation:
- Revokes the previous refresh token (
revoked_at,rotated_at). - Creates a new access/refresh pair.
- Records
replaced_bypointing to the new session.
- Revokes the previous refresh token (
- Frontend replaces local tokens with the new ones.
Malicious reuse detection: If an already-revoked refresh token is reused, the backend detects it and invalidates the user's entire session chain.
4. Revocation
POST /api/v1/auth/revoke { refreshToken }Explicitly revokes a session, marking revoked_at in auth_refresh_sessions.
5. Session validation
POST /api/v1/session/validateChecks if the current session is active and returns its state. Used by the frontend for periodic validation.
Dev Mode
When AUTH_MODE=dev is configured in the backend:
- Pre-configured fixed tokens from environment variables are accepted.
- Real JWT is not required.
- Facilitates local development and testing.
Important: In production, AUTH_MODE=jwt is mandatory (see Production Readiness Gates).
Password hashing
- Algorithm: PBKDF2 (Password-Based Key Derivation Function 2).
- Iterations: 100,000.
- Hash: SHA-256.
- Storage format:
pbkdf2:<hexSalt>:<hexHash>. - History: The legacy backend used scrypt, which proved incompatible with the Cloudflare Workers environment. The migration to PBKDF2 was resolved in ADR-0003.
Session modes
| Mode | Configuration | Behavior |
|---|---|---|
| Bearer | AUTH_SESSION_MODE=bearer (default) | Refresh token sent in response body; frontend stores in IndexedDB |
| Cookie | AUTH_SESSION_MODE=cookie | Refresh token in httpOnly cookie; mutations require X-CSRF-Token |
Module architecture
Backend (workers/src/)
| File | Responsibility |
|---|---|
modules/auth/password.ts | PBKDF2 hashing and verification |
modules/auth/sessionTokens.ts | JWT signing/verification + refresh token |
middleware/auth.ts | JWT verification middleware + dev mode + role guard |
db/queries.ts | Reusable auth and idempotency queries |
routes/auth.ts | Login, refresh, revoke handlers |
Frontend (app/src/)
| File | Responsibility |
|---|---|
app/context/AuthContext.tsx | Auth state, roles, session |
modules/auth/backendAuth.ts | Backend-based authentication mode |
modules/auth/sessionManager.ts | Local session management |
modules/auth/sessionHydrator.ts | Session hydration from IndexedDB |
modules/auth/keyRotationService.ts | Local encryption key rotation |
Sequence diagram
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Frontend │ │ Backend │ │ D1 (SQLite) │
└────┬─────┘ └────┬─────┘ └──────┬───────┘
│ │ │
│ POST /auth/login │ │
│───────────────────▶│ │
│ │ SELECT password_hash│
│ │─────────────────────▶│
│ │◀─────────────────────│
│ │ │
│ │ PBKDF2.verify() │
│ │ │
│ │ INSERT session │
│ │─────────────────────▶│
│ │◀─────────────────────│
│ │ │
│ { accessToken, │ │
│ refreshToken, │ │
│ csrfToken, │ │
│ user } │ │
│◀───────────────────│ │
│ │ │
│ (15 min later) │ │
│ │ │
│ POST /auth/refresh│ │
│───────────────────▶│ │
│ │ SELECT/UPDATE │
│ │ session (rotate) │
│ │─────────────────────▶│
│ │◀─────────────────────│
│ │ │
│ { newAccessToken, │ │
│ newRefreshToken }│ │
│◀───────────────────│ │
│ │ │
│ POST /auth/revoke │ │
│───────────────────▶│ │
│ │ UPDATE revoked_at │
│ │─────────────────────▶│
│ │◀─────────────────────│
│ { ok: true } │ │
│◀───────────────────│ │Related architectural decisions
- ADR-0003: Auth Adapter and Session Token Model: Session model with short-lived access token, local session with TTL, and blocking of protected actions after expiry. Defines that the auth implementation must be encapsulated behind a module interface.
- ADR-0007: Keycloak token validation boundary: Backend validates Bearer tokens via JWKS and issuer/audience checks. Frontend keeps provider-specific details within the auth adapter boundaries.
Sources: workers/CONTEXT.md, ADR-0003, ADR-0007