# Auth Access Control Implementation Plan > **For agentic workers:** Execute tasks in order and keep scope fixed to the approved spec. Use checkbox (`- [ ]`) tracking and verify each task before moving on. **Goal:** Implement login-first access control for both pages and APIs, with code-based allowlists, safe login redirect handling, and dual-mode homepage rendering support. **Architecture:** Introduce centralized route rule utilities, a global page guard (`auth.global`), a server API guard middleware (`10.auth-guard`), and a unified auth session composable. Default policy is deny-by-default for pages and `/api/**`, with narrow allowlists. **Tech Stack:** Nuxt 4/Nitro middleware, existing session-cookie auth service, TypeScript utilities, Bun dev/build workflow. --- ## File Structure Map - Create: `app/utils/auth-routes.ts` - Create: `app/composables/useAuthSession.ts` - Create: `app/middleware/auth.global.ts` - Create: `server/utils/auth-api-routes.ts` - Create: `server/middleware/10.auth-guard.ts` - Modify: `app/pages/login/index.vue` (handle redirect after login success) - Modify: `app/pages/index/index.vue` (guest/authenticated split rendering) - Optional small adjustments: shared HTTP wrapper for uniform 401 handling if needed --- ### Task 1: Build shared page-route auth rules and redirect sanitizer **Files:** - Create: `app/utils/auth-routes.ts` - Test: utility behavior via unit-like local checks (or temporary assertions) - [ ] **Step 1: Define route policy constants** Include: - public routes exact set (initial: `/`, `/login`, `/register`) - guest-only routes exact set (initial: `/login`, `/register`) - default authenticated landing path - [ ] **Step 2: Implement safe `redirect` parser** Implement helper like `normalizeSafeRedirect(input, fallback)`: - allow only same-site relative paths - must start with `/` - reject `//`, protocol-like payloads (`http:`, `https:`, `javascript:`), empty strings - return fallback when invalid - [ ] **Step 3: Implement route match helpers** Provide concise helpers: - `isPublicRoute(path)` - `isGuestOnlyRoute(path)` - matching mode: exact + explicit prefix helper only (no broad regex) - [ ] **Step 4: Verify sanitizer edge cases** Validate examples: - accept: `/dashboard`, `/a?b=1` - reject: `http://evil.com`, `//evil.com`, `javascript:alert(1)` --- ### Task 2: Add unified auth session composable **Files:** - Create: `app/composables/useAuthSession.ts` - Test: manual behavior from multiple pages without duplicated calls - [ ] **Step 1: Implement canonical session state** Expose: - `loggedIn` - `user` - `pending` - `refresh()` - `clear()` Use `/api/auth/me` as source of truth. - [ ] **Step 2: Handle unauthorized consistently** When `/api/auth/me` returns 401: - clear local state - return `loggedIn=false` instead of throwing noisy UI errors - [ ] **Step 3: Ensure composable is reusable** Guarantee all pages/middleware consume this composable, not ad-hoc login checks. --- ### Task 3: Implement global page guard middleware (default deny) **Files:** - Create: `app/middleware/auth.global.ts` - Reuse: `app/utils/auth-routes.ts`, `app/composables/useAuthSession.ts` - [ ] **Step 1: Enforce default login-required policy** For every route: - if route is not public and user not logged in, navigate to `/login?redirect=` - [ ] **Step 2: Enforce guest-only behavior** If logged-in user visits guest-only routes: - resolve safe redirect target - prioritize validated `redirect` query - fallback to default authenticated landing path - [ ] **Step 3: Prevent redirect loops** Guarantee no self-loop on `/login` and `/register`. - [ ] **Step 4: Manual route-flow verification** Check: - unauth -> protected page -> login redirect with query - logged-in -> `/login` -> redirected away - public routes remain reachable when unauthenticated --- ### Task 4: Implement server API auth guard middleware (default deny for `/api/**`) **Files:** - Create: `server/utils/auth-api-routes.ts` - Create: `server/middleware/10.auth-guard.ts` - [ ] **Step 1: Define API allowlist rules** Initial allowlist: - `/api/auth/login` - `/api/auth/register` - required public-read APIs (e.g. `GET /api/config/global`) Use exact + explicit prefix helpers only. - [ ] **Step 2: Guard `/api/**` requests** Middleware logic: - non-API request -> skip - allowlisted API -> skip - others -> require valid session from `event.context.auth.getCurrent()` - [ ] **Step 3: Return unified unauthorized response** On unauthorized: - return HTTP 401 - use generic status message - do not leak auth internals - [ ] **Step 4: Validate API behavior** Check with/without cookie: - protected APIs return 401 when unauthenticated - allowlisted APIs still work --- ### Task 5: Wire login/home pages to new access-control behavior **Files:** - Modify: `app/pages/login/index.vue` - Modify: `app/pages/index/index.vue` - [ ] **Step 1: Login page post-success redirect** After successful login: - call `useAuthSession().refresh()` - read `redirect` query and sanitize - navigate to safe target/fallback - [ ] **Step 2: Homepage dual-mode rendering** On `/`: - unauthenticated: show guest-facing UI - authenticated: show signed-in UI - rely on `useAuthSession` state - [ ] **Step 3: Session-loss UX consistency** If session expires during interaction: - state clears - protected route access re-routes to login --- ### Task 6: Verification matrix and polish **Files:** - Test-focused task; no intended new feature files - [ ] **Step 1: Route verification matrix** Run through: - unauth + public route - unauth + protected route - auth + guest-only route - auth + protected route - [ ] **Step 2: Redirect security matrix** Try login URL examples: - `/login?redirect=/safe/path` (accepted) - `/login?redirect=http://evil.com` (rejected) - `/login?redirect=//evil.com` (rejected) - [ ] **Step 3: API verification matrix** Confirm: - unauth -> protected `/api/**` => 401 - allowlisted APIs accessible unauthenticated - expired session behaves as unauthorized - [ ] **Step 4: Build/lint sanity** Run: - `bun run build` - lint/type checks used by the repo Fix only issues introduced by this scope. --- ## Delivery Constraints - Keep allowlists in code (no remote config source for auth policy) - No RBAC/role granularity in this iteration - No OAuth/SSO/password reset in this iteration - Avoid broad route regexes that can accidentally expand exposure ## Done Criteria All below must be true: - Page policy is deny-by-default with explicit public routes - API policy is deny-by-default under `/api/**` with explicit allowlist - Redirect handling is safe against open-redirect payloads - Login and homepage behavior matches agreed UX - Verification matrix passes without known regressions