docs(auth): reflect single-cookie architecture; close A13-3
dev-docs/AUTH_ARCHITECTURE.md (v1.0 → v2.0): - Title section updated to single-SPA / single-cookie reality - Client Applications table collapsed to one row - Cookie Specification table collapsed to one row (crewli_app_token) - Token Lifecycle / Validation section: Origin-based resolution language removed; middleware described as origin-agnostic - Cross-app isolation paragraph removed (no second app) - Configuration Reference table marks FRONTEND_PORTAL_URL as legacy, pointing at TECH-FRONTEND-URL-CONSOLIDATE - New §11 "History" preserves the pre-WS-3 dual-cookie context for future readers, mentions PR-B2a + PR-B2b roles in the unwind dev-docs/BACKLOG.md — three new entries: - TECH-FRONTEND-URL-CONSOLIDATE: refactor email controllers to drop per-app URL map (EmailChangeController, PasswordResetController, PersonController) — low priority, code-cleanliness only - TECH-DOCS-APPS-PORTAL-PURGE: sweep apps/portal references from briefing/tooling docs (.cursor/, MASTER_PROMPT_*, SETUP, dev-guide, CLAUDE_CODE_TOOLING) — single chore(docs) PR, low priority - OPS — DNS retirement of portal.crewli.app — operational task, deferred until traffic monitoring confirms zero usage dev-docs/SECURITY_AUDIT.md: - A13-1 narrative actualised: pre-WS-3 dual-cookie context kept as history, status flipped to RESOLVED (the localStorage→httpOnly migration shipped earlier in the consolidation arc) - A13-3: status flipped to RESOLVED by WS-3 PR-B2b; description rewritten to reflect the new postLoginRedirect.ts validator and the 16 spec coverage - Priority remediation table item 8 strikes through A13-3 Backend test suite: 1487 passed (unchanged from Commit 2 baseline). Frontend: 223 passed (unchanged from Commit 1 baseline). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,38 +1,36 @@
|
||||
# Crewli — Authentication Architecture
|
||||
|
||||
> Version: 1.0 — April 2026
|
||||
> Version: 2.0 — May 2026 (post WS-3 PR-B2b: single-cookie consolidation)
|
||||
> Audience: security auditors, backend developers
|
||||
|
||||
---
|
||||
|
||||
## 1. Authentication Overview
|
||||
|
||||
Crewli uses **stateless token-based authentication** via Laravel Sanctum. Two SPA clients communicate with a single REST API. Tokens are stored exclusively in **httpOnly cookies** set by the server — they are never exposed to JavaScript via response bodies, localStorage, or JS-readable cookies.
|
||||
Crewli uses **stateless token-based authentication** via Laravel Sanctum. A single SPA client communicates with a single REST API. Tokens are stored exclusively in **httpOnly cookies** set by the server — they are never exposed to JavaScript via response bodies, localStorage, or JS-readable cookies.
|
||||
|
||||
### Client Applications
|
||||
### Client Application
|
||||
|
||||
| App | URL (dev) | URL (prod) | Purpose |
|
||||
|-----|-----------|------------|---------|
|
||||
| App | localhost:5174 | crewli.app | Organiser dashboard + platform admin (`/platform/*` for super_admin) |
|
||||
| Portal | localhost:5175 | portal.crewli.app | Volunteers, artists, suppliers |
|
||||
| SPA | localhost:5174 | crewli.app | Organizers, volunteers, crew, super_admin (context-routed in-app) |
|
||||
|
||||
### Access Modes
|
||||
|
||||
The Portal supports two access modes:
|
||||
The SPA supports two access modes:
|
||||
|
||||
1. **Cookie-based** (`auth:sanctum`): volunteers and crew who have a `user_id` — login with email/password, httpOnly cookie set on login
|
||||
1. **Cookie-based** (`auth:sanctum`): organizers, volunteers, crew — login with email/password, httpOnly cookie set on login
|
||||
2. **Token-based** (`portal.token` middleware): artists, suppliers, press — stateless per-request token via `Authorization: Bearer` header or `?token=` query parameter. No cookies involved.
|
||||
|
||||
---
|
||||
|
||||
## 2. Cookie Specification
|
||||
|
||||
| App | Cookie Name | Domain | Secure | httpOnly | SameSite | Max-Age |
|
||||
|-----|-------------|--------|--------|----------|----------|---------|
|
||||
| App | `crewli_app_token` | `.crewli.app` (prod) / `localhost` (dev) | Yes (prod) | Yes | Strict | 7 days |
|
||||
| Portal | `crewli_portal_token` | `.crewli.app` (prod) / `localhost` (dev) | Yes (prod) | Yes | Strict | 7 days |
|
||||
| Cookie Name | Domain | Secure | httpOnly | SameSite | Max-Age |
|
||||
|-------------|--------|--------|----------|----------|---------|
|
||||
| `crewli_app_token` | `.crewli.app` (prod) / `localhost` (dev) | Yes (prod) | Yes | Strict | 7 days |
|
||||
|
||||
Each SPA gets its own cookie name to prevent shared auth state between apps. The cookie domain is configured via `SESSION_DOMAIN` in `.env`.
|
||||
A single cookie covers all cookie-authenticated traffic. The cookie domain is configured via `SESSION_DOMAIN` in `.env`.
|
||||
|
||||
---
|
||||
|
||||
@@ -41,23 +39,19 @@ Each SPA gets its own cookie name to prevent shared auth state between apps. The
|
||||
### Creation
|
||||
|
||||
On successful login (`POST /auth/login`), the server:
|
||||
1. Validates credentials via `Auth::attempt()`
|
||||
1. Validates credentials
|
||||
2. Creates a Sanctum personal access token
|
||||
3. Resolves the cookie name from the `Origin` header
|
||||
4. Returns user data in the JSON body (no token in body)
|
||||
5. Attaches the token as a `Set-Cookie` header with httpOnly flag
|
||||
3. Returns user data in the JSON body (no token in body)
|
||||
4. Attaches the token as a `Set-Cookie` header with httpOnly flag (cookie name: `crewli_app_token`)
|
||||
|
||||
### Validation
|
||||
|
||||
The `CookieBearerToken` middleware (registered before `auth:sanctum` in the API middleware stack):
|
||||
1. Reads the `Origin` (or `Referer`) header to identify which app is making the request
|
||||
2. Resolves the correct cookie name for that app (e.g. portal origin → `crewli_portal_token`)
|
||||
3. Reads only that cookie and sets `Authorization: Bearer` on the request
|
||||
4. Sanctum's existing token validation processes the header normally
|
||||
1. Skips if an `Authorization` header is already present (portal-token flow, server-to-server callers)
|
||||
2. Reads the `crewli_app_token` cookie and sets `Authorization: Bearer <token>` on the request
|
||||
3. Sanctum's existing token validation processes the header normally
|
||||
|
||||
**Cross-app isolation:** In local development, both SPAs share `localhost` (different ports). Browsers do not scope cookies by port, so both app cookies are sent with every API request. The middleware prevents cross-app authentication by only reading the cookie that matches the requesting app's Origin header. Without this, logging into one app would authenticate the other.
|
||||
|
||||
If the `Origin` header is absent (e.g. server-to-server requests), the middleware falls back to the first available cookie. If an `Authorization` header is already present (e.g. from the portal token flow), the middleware skips cookie injection entirely.
|
||||
The middleware is origin-agnostic — there is no Origin/Referer parsing or per-app cookie resolution. With only one SPA, cross-app isolation is not a concern.
|
||||
|
||||
### Rotation
|
||||
|
||||
@@ -149,12 +143,12 @@ This flow is separate from the httpOnly cookie system and is NOT affected by thi
|
||||
|
||||
```
|
||||
Request
|
||||
→ CookieBearerToken (reads cookie → injects Authorization header)
|
||||
→ CookieBearerToken (cookie → Authorization header)
|
||||
→ auth:sanctum (validates bearer token)
|
||||
→ Controller
|
||||
```
|
||||
|
||||
For portal token routes:
|
||||
For portal-token routes (artists / suppliers / press):
|
||||
```
|
||||
Request
|
||||
→ portal.token (validates portal-specific token)
|
||||
@@ -168,8 +162,8 @@ Request
|
||||
| Setting | Location | Purpose |
|
||||
|---------|----------|---------|
|
||||
| `SESSION_DOMAIN` | `.env` | Cookie domain (`.crewli.app` in prod, `localhost` in dev) |
|
||||
| `FRONTEND_APP_URL` | `.env` / `config/app.php` | App SPA origin |
|
||||
| `FRONTEND_PORTAL_URL` | `.env` / `config/app.php` | Portal SPA origin |
|
||||
| `FRONTEND_APP_URL` | `.env` / `config/app.php` | SPA origin |
|
||||
| `FRONTEND_PORTAL_URL` | `.env` / `config/app.php` | Legacy — set to the same value as `FRONTEND_APP_URL` post-WS-3. Still consumed by outbound-email controllers (password-reset, email-change, person-create) for per-app URL maps; refactor tracked as `TECH-FRONTEND-URL-CONSOLIDATE`. |
|
||||
| `sanctum.expiration` | `config/sanctum.php` | Token TTL (7 days = 10080 minutes) |
|
||||
|
||||
---
|
||||
@@ -400,3 +394,11 @@ This applies to **all** activity log entries, not just impersonation-specific ev
|
||||
| `app/Http/Requests/Admin/StartImpersonationRequest.php` | Validation for start request |
|
||||
| `app/Models/ImpersonationSession.php` | Eloquent model with `HasUlids`, `scopeActive()` |
|
||||
| `app/Http/Resources/Admin/ImpersonationSessionResource.php` | API resource for session data |
|
||||
|
||||
---
|
||||
|
||||
## 11. History — pre-WS-3 dual-cookie architecture
|
||||
|
||||
Pre-WS-3 (April 2026), Crewli ran two separate SPAs (`apps/app` for organizers, `apps/portal` for crew/volunteers) and the auth layer maintained per-app cookies (`crewli_app_token`, `crewli_portal_token`) with Origin-based resolution in both `CookieBearerToken` middleware and the `SetAuthCookie` controller trait.
|
||||
|
||||
WS-3 PR-B (April–May 2026) consolidated to a single SPA workspace. PR-B2a unified the frontend stores, axios factory, route guards, and `ContextSwitcher`. PR-B2b retired the dual-cookie machinery on the server: `crewli_portal_token` is fully purged, the Origin-resolution code paths are gone, and the auth cookie is unconditional. The Portal Token-Based Flow for artists/suppliers (described in §6) is unchanged — that mechanism is independent of the cookie flow and remains the canonical way to authenticate per-token portal links.
|
||||
|
||||
Reference in New Issue
Block a user