# Authentication & Security This document covers how the API handles authentication, token lifecycle, and access control. --- ## Overview The API uses **JWE (JSON Web Encryption)** tokens — AES-256-GCM encrypted — rather than plain signed JWTs. Clients cannot inspect token claims. The flow is stateless: no server-side sessions. All state is carried in tokens or stored in MongoDB. Two token types are in play: | Token | Lifetime | Purpose | |---|---|---| | Access token | 1 hour | Authenticate API requests via `Authorization: Bearer` | | Refresh token | 30 days | Exchange for a new access token without re-logging in | --- ## Token Structure Both token types share a base structure but carry a `type` claim (`"access"` or `"refresh"`) that prevents cross-use — the API rejects a refresh token presented as a Bearer token and vice versa. **Access token claims:** | Claim | Value | |---|---| | `sub` | User email | | `type` | `"access"` | | `authorities` | Array of roles, e.g. `["ROLE_USER"]` | | `jti` | UUIDv7 — unique token ID used for revocation lookup | | `iat` / `exp` / `iss` | Standard JWT timestamps and issuer | **Refresh token claims:** `sub`, `type`, `jti`, `iat`, `exp`, `iss` (no `authorities`). **Encryption:** `JWE` with `dir` key management and `A256GCM` content encryption, keyed from `JWT_ENCRYPTION_SECRET` (Base64-encoded 32-byte AES key). --- ## Authentication Flow ### Login ``` POST /v1/auth/login Content-Type: application/json { "email": "user@example.com", "password": "..." } ``` 1. User is looked up by email in MongoDB. 2. Password is verified via Argon2 (memory-hard, GPU-resistant). 3. A new access token and a refresh token family are created. 4. Both tokens are returned. ```json { "accessToken": "", "refreshToken": "", "tokenType": "Bearer", "expiresIn": 3600 } ``` Authentication errors always return `401` regardless of whether the email exists, to prevent account enumeration. ### Using the Access Token Pass the access token in every request to a protected endpoint: ``` Authorization: Bearer ``` The `JwtAuthenticationFilter` runs on every request: 1. Extracts and decrypts the Bearer token. 2. Checks the token's `jti` against the MongoDB revocation blocklist. 3. Loads the user and sets the security context. 4. Unauthenticated requests pass through; endpoint authorization rules handle the rejection. ### Token Refresh ``` POST /v1/auth/refresh Content-Type: application/json { "refreshToken": "" } ``` Returns a new access token and a new refresh token. The old refresh token is immediately invalidated. See [Refresh Token Rotation & Theft Detection](#refresh-token-rotation--theft-detection) below. ### Logout ``` POST /v1/auth/logout Authorization: Bearer ``` 1. The current access token's `jti` is added to the revocation blocklist. 2. All refresh tokens for the user are revoked. The blocklist entry lives until the access token's own `exp`, then MongoDB TTL removes it automatically — the blocklist never grows unboundedly. --- ## Refresh Token Rotation & Theft Detection Every login starts a **token family** (tracked by `familyId`). Each refresh produces a new token in the same family; the previous token is marked revoked. **Theft detection:** If a refresh token is presented and it's already marked revoked, but its family still has an active sibling, the system infers a stolen token was reused: 1. The entire token family is revoked (all active sessions from that login). 2. A security event is logged with the attacker's IP. 3. Both the legitimate owner and the attacker are forced to re-authenticate. If a revoked token's family has no active siblings, it's treated as a simple re-consumption (returns `401` without the family wipe). **Session limits:** Each user may have at most 5 active refresh token families (concurrent sessions). When the limit is reached, the oldest session is automatically revoked. --- ## Roles & Access Control Two roles are defined: | Role | Description | |---|---| | `ROLE_USER` | Default for all registered accounts | | `ROLE_ADMIN` | Required for user management operations | Authorization is enforced at two levels: - **Route level** — `SecurityConfig` permits public routes and rejects unauthenticated requests to protected ones. - **Method level** — `@PreAuthorize("hasRole('ROLE_ADMIN')")` on admin controllers. **Endpoint authorization summary:** | Endpoint | Auth | Role | |---|---|---| | `POST /v1/auth/login` | — | — | | `POST /v1/auth/refresh` | — | — | | `POST /v1/auth/logout` | Bearer | Any | | `GET /v1/users/me` | Bearer | Any | | `GET /v1/users` | Bearer | `ROLE_ADMIN` | | `POST /v1/admin/users` | Bearer | `ROLE_ADMIN` | | `GET /v1/health` | — | — | --- ## Password Storage Passwords are hashed with **Argon2** via the `password4j` library. Only the hash is stored; plaintext is never persisted. Comparison uses constant-time evaluation. --- ## MongoDB Collections | Collection | Purpose | TTL | |---|---|---| | `users` | User accounts (email unique-indexed) | — | | `refresh_tokens` | Active and revoked refresh tokens (hash-stored, not plaintext) | `expiresAt` | | `revoked_access_tokens` | Access token blocklist by `jti` | `expiresAt` | TTL indexes on `expiresAt` in both token collections ensure automatic cleanup with no manual housekeeping. --- ## Error Responses All errors return a consistent structure: ```json { "status": 401, "message": "Invalid credentials", "path": "/v1/auth/login" } ``` | Scenario | Status | |---|---| | Bad credentials / unknown email | `401` | | Invalid or expired token | `401` | | Token reuse / security breach | `401` | | Authenticated but insufficient role | `403` | | Validation error | `400` | --- ## Configuration | Variable | Description | |---|---| | `JWT_ENCRYPTION_SECRET` | Base64-encoded 32-byte AES key (required) | | `jwt.access-token-expiration` | Access token TTL in ms (default: `3600000` — 1 hour) | | `jwt.refresh-token-expiration` | Refresh token TTL in ms (default: `2592000000` — 30 days) | | `jwt.issuer` | Issuer claim value (default: `kotlin-api`) | Generate a secret: ```bash openssl rand -base64 32 | tr -d '\n' ```