diff --git a/auth-migration/INVARIANTS.md b/auth-migration/INVARIANTS.md new file mode 100644 index 0000000..326542c --- /dev/null +++ b/auth-migration/INVARIANTS.md @@ -0,0 +1,51 @@ +# Authentication & Authorization Invariants — auth-migration + +## 1. Authentication Rules + +| ID | Rule | Trigger | Expected Behavior | Error Code | +|----|------|---------|-------------------|------------| +| AUTH-001 | JWT must contain `sub` claim | Any protected endpoint | Reject with 401 if `sub` missing/empty | `AUTH_MISSING_SUB` | +| AUTH-002 | JWT `exp` must be in future | Any protected endpoint | Reject with 401 if expired | `AUTH_TOKEN_EXPIRED` | +| AUTH-003 | JWT signature must verify against secret | Any protected endpoint | Reject with 401 if signature invalid | `AUTH_INVALID_SIGNATURE` | +| AUTH-004 | Bearer token required in Authorization header | Any protected endpoint | Reject with 401 if header missing | `AUTH_MISSING_TOKEN` | +| AUTH-005 | Token format must be `Bearer ` | Any protected endpoint | Reject with 401 if malformed | `AUTH_MALFORMED_TOKEN` | + +## 2. Authorization Rules + +| ID | Rule | Scope | Expected Behavior | Error Code | +|----|------|-------|-------------------|------------| +| AUTH-010 | ADMIN role required for user management | `/api/admin/**` | Reject with 403 if not ADMIN | `AUTH_FORBIDDEN_ADMIN` | +| AUTH-011 | Resource owner or ADMIN can modify | `/api/users/{id}/**` | Reject with 403 if neither | `AUTH_FORBIDDEN_RESOURCE` | +| AUTH-012 | Any authenticated user can read public profiles | `/api/users/public/**` | Allow 200 if authenticated | — | +| AUTH-013 | Role hierarchy: ADMIN > MANAGER > USER | All role-gated endpoints | Higher role inherits lower permissions | — | + +## 3. Error Response Schema + +| HTTP Status | Error Code | Response Body Shape | +|-------------|------------|---------------------| +| 401 | `AUTH_MISSING_TOKEN` | `{"error":"AUTH_MISSING_TOKEN","message":"Authorization header required","timestamp":}` | +| 401 | `AUTH_MALFORMED_TOKEN` | `{"error":"AUTH_MALFORMED_TOKEN","message":"Bearer token format required","timestamp":}` | +| 401 | `AUTH_TOKEN_EXPIRED` | `{"error":"AUTH_TOKEN_EXPIRED","message":"Token has expired","timestamp":}` | +| 401 | `AUTH_INVALID_SIGNATURE` | `{"error":"AUTH_INVALID_SIGNATURE","message":"Token signature verification failed","timestamp":}` | +| 401 | `AUTH_MISSING_SUB` | `{"error":"AUTH_MISSING_SUB","message":"Token missing subject claim","timestamp":}` | +| 403 | `AUTH_FORBIDDEN_ADMIN` | `{"error":"AUTH_FORBIDDEN_ADMIN","message":"Admin role required","timestamp":}` | +| 403 | `AUTH_FORBIDDEN_RESOURCE` | `{"error":"AUTH_FORBIDDEN_RESOURCE","message":"Not authorized for this resource","timestamp":}` | + +## 4. Session / Cookie Semantics + +| ID | Rule | Behavior | +|----|------|----------| +| SESS-001 | Session cookie `HttpOnly` | Must be `true` | +| SESS-002 | Session cookie `Secure` | Must be `true` in production | +| SESS-003 | Session cookie `SameSite` | Must be `Lax` or `Strict` | +| SESS-004 | CSRF token required for state-changing ops | POST/PUT/DELETE without CSRF → 403 | + +## 5. Migration Checklist + +- [ ] Spring Security filter chain preserves order: JWT filter before session filter +- [ ] `@PreAuthorize` annotations match existing role-gate logic +- [ ] `AuthenticationEntryPoint` returns exact error schema (AUTH_* codes) +- [ ] `AccessDeniedHandler` returns exact error schema (AUTH_FORBIDDEN_* codes) +- [ ] Cookie attributes (`HttpOnly`, `Secure`, `SameSite`) set via `CookieSecurityConfigurer` +- [ ] CSRF protection enabled for state-changing endpoints +- [ ] Role hierarchy bean `RoleHierarchyImpl` wired with ADMIN > MANAGER > USER