# oauth-21-server - Work Plan ## TL;DR (For humans) **What you'll get:** A fully compliant OAuth 2.1 authorization server with a discovery endpoint, PKCE-mandatory authorization flows, opaque tokens stored in Redis, and a login/consent UI. All endpoints follow RFC 8414 metadata and OAuth 2.1 draft-15 with the highest security posture — no legacy grants, no plain PKCE, exact redirect URI matching, and HTTPS enforcement. **Why this approach:** Using ioredis for all expiring data (tokens, auth codes, sessions) and SQLite3 only for persistent records (clients, users, scopes) cleanly separates concerns and leverages Redis TTL for automatic expiration. SvelteKit's `+server.js` handlers provide a clean API layer on the existing Node.js adapter. **What it will NOT do:** It will not support JWT tokens, implicit grants, password grants, client_credentials grants, or query-string bearer tokens. It will not persist auth codes or tokens in SQLite3. It does not include OpenID Connect. **Effort:** Large **Risk:** Medium — Redis dependency adds operational surface; PKCE and PAR enforcement must be exact **Decisions to sanity-check:** Token format (opaque), grant types (authorization_code + refresh_token only), Redis as sole store for expiring data. Your next move: Start work with `/start-work`. --- > TL;DR (machine): Large effort, Medium risk — OAuth 2.1 AS with discovery, PKCE, PAR, opaque tokens in Redis, sqlite3 for persistent data, SvelteKit +server.js handlers. ## Scope ### Must have - `/.well-known/oauth-authorization-server` metadata endpoint (RFC 8414) - OAuth 2.1 core protocol endpoints: `/oauth/authorize`, `/oauth/token`, `/oauth/introspect`, `/oauth/revoke` - Client registration endpoint `/oauth/register` (RFC 7591) - PKCE S256 mandatory enforcement (no `plain`) - Pushed Authorization Requests (PAR) endpoint `/oauth/par` (RFC 9126) with `require_pushed_authorization_requests: true` - Opaque token generation and storage in Redis with TTL - Authorization code generation and storage in Redis with TTL - Refresh token generation and storage in Redis with TTL - Client authentication at token endpoint: `client_secret_basic`, `client_secret_post`, `private_key_jwt`, `tls_client_auth` - Introspection endpoint with client auth (not bearer-only) - Exact redirect URI matching - User login page (`/oauth/login`) - User consent page (`/oauth/consent`) - `config.json` updates: `baseUrl`, `redis` connection options (host, port, password, database number) - `ioredis` dependency and Redis client module - SQLite3 schema extension: `clients` table only - `scopes_supported` dynamically from SQLite3 `scopes` table - `ui_locales_supported`: `["en-US", "es-MX"]` - Documentation moved from `.omo/ulw-research/` to `docs/` ### Must NOT have (guardrails, anti-slop, scope boundaries) - JWT access tokens (opaque only) - Implicit grant (`response_type=token`) - Resource Owner Password Credentials grant - `client_credentials` grant - `plain` PKCE method - Auth codes, access tokens, or refresh tokens stored in SQLite3 - Bearer tokens in query strings - Wildcard or prefix redirect URI matching - OpenID Connect (ID tokens, userinfo endpoint) - Device authorization grant (RFC 8628) - CORS on authorization endpoint ## Verification strategy > Zero human intervention - all verification is agent-executed. - Test decision: tests-after + agent-executed QA via curl/httpie against running dev server - Evidence: `.omo/evidence/task--oauth-21-server.` ## Execution strategy ### Parallel execution waves > Target 5-8 todos per wave. **Wave 1: Foundation** — Config, dependencies, Redis client, database schema **Wave 2: Discovery & Client Registration** — Metadata endpoint, client registration **Wave 3: Core OAuth Flow** — Authorization, token, introspection, revocation endpoints **Wave 4: User UI & Security Hardening** — Login/consent pages, PAR, JWKS **Wave 5: Documentation & Final QA** — Move docs, integration testing ### Dependency matrix | Todo | Depends on | Blocks | Can parallelize with | | --- | --- | --- | --- | | 1 | — | 2,3,4,5 | — | | 2 | 1 | 6,7,8,9,10 | 3,4,5 | | 3 | 1 | 6,7,8,9,10 | 2,4,5 | | 4 | 1 | 6,7,8,9,10 | 2,3,5 | | 5 | 1 | — | 2,3,4 | | 6 | 2,3,4 | 7 | — | | 7 | 6 | 8,9,10 | — | | 8 | 7 | — | 9,10 | | 9 | 7 | — | 8,10 | | 10 | 7 | — | 8,9 | | 11 | 1 | 12 | — | | 12 | 11 | — | — | | 13 | — | — | all | | 14 | all | — | — | ## Todos - [x] 1. Update `config.json.example` with `baseUrl` and `redis` sections What to do / Must NOT do: Add `baseUrl` (string, e.g., `https://localhost:3000`) and `redis` object with `host`, `port`, `password`, `db` (database number) to `config.json.example`. Must NOT remove existing database/password sections. Parallelization: Wave 1 | Blocked by: — | Blocks: 2,3,4,5 References: `config.json.example:1-25`, `src/lib/config.js:1-19` Acceptance criteria: `cat config.json.example | grep -q baseUrl && cat config.json.example | grep -q redis` QA scenarios: Verify JSON is valid (`node -e "JSON.parse(require('fs').readFileSync('config.json.example'))"`). Happy: valid JSON. Failure: invalid JSON syntax. Commit: Y | feat(config): add baseUrl and redis connection options - [x] 2. Install `ioredis` dependency and create Redis client module What to do / Must NOT do: `pnpm add ioredis`. Create `src/lib/redis.js` exporting a configured ioredis client using `config.redis`. Must NOT use `redis` (node-redis) package. Must handle connection errors gracefully. Parallelization: Wave 1 | Blocked by: 1 | Blocks: 6,7,8,9,10 References: `package.json:29-32`, `src/lib/config.js:1-19`, ioredis docs Acceptance criteria: `node -e "import('./src/lib/redis.js').then(m => console.log(typeof m.default))"` returns "object" or similar QA scenarios: Happy: Redis client connects and responds to PING. Failure: Invalid config throws error with helpful message. Commit: Y | feat(redis): add ioredis client module - [x] 3. Extend SQLite3 schema with `clients` table What to do / Must NOT do: Add `clients` table creation to `seedInfo()` in `src/lib/sqlite3/database.js`. Schema: `id`, `client_id` (TEXT UNIQUE), `client_secret` (TEXT), `redirect_uris` (TEXT, JSON array), `grant_types` (TEXT), `response_types` (TEXT), `scope` (TEXT), `token_endpoint_auth_method` (TEXT), `registration_access_token` (TEXT), `created_at`, `updated_at`. Must NOT create auth_codes, access_tokens, or refresh_tokens tables in SQLite3. Parallelization: Wave 1 | Blocked by: 1 | Blocks: 6,7,8,9,10 References: `src/lib/sqlite3/database.js:16-47` Acceptance criteria: After `rm data/app.db && npm run dev`, `clients` table exists with correct columns. QA scenarios: Happy: Fresh DB initializes with clients table. Failure: Duplicate table creation does not error (idempotent). Commit: Y | feat(db): add clients table for OAuth 2.1 - [x] 4. Create token generation utilities What to do / Must NOT do: Create `src/lib/oauth/tokens.js` with functions: `generateAuthorizationCode()`, `generateAccessToken()`, `generateRefreshToken()` — all return cryptographically random strings (use `crypto.randomBytes`). Must NOT use `Math.random()`. Must NOT create JWTs. Parallelization: Wave 1 | Blocked by: 1 | Blocks: 6,7,8,9,10 References: Node.js `crypto` module docs, `src/lib/crypto.js:1-13` Acceptance criteria: `node -e "import('./src/lib/oauth/tokens.js').then(m => console.log(m.generateAccessToken().length > 20))"` prints true QA scenarios: Happy: Generated tokens are unique across 1000 calls. Failure: Tokens are predictable or too short. Commit: Y | feat(oauth): add opaque token generation utilities - [x] 5. Create PKCE validation utilities What to do / Must NOT do: Create `src/lib/oauth/pkce.js` with `generateCodeChallenge(verifier)` (S256 SHA256 base64url) and `verifyCodeChallenge(verifier, challenge)` (constant-time comparison). Must NOT support `plain` method. Must use `crypto.createHash('sha256')`. Parallelization: Wave 1 | Blocked by: 1 | Blocks: 6,7 References: RFC 7636 Section 4, Node.js `crypto` docs Acceptance criteria: `node -e "import('./src/lib/oauth/pkce.js').then(m => m.verifyCodeChallenge('test', m.generateCodeChallenge('test')))"` prints true QA scenarios: Happy: Valid verifier matches challenge. Failure: Invalid verifier fails, wrong method fails, timing attack resistant. Commit: Y | feat(oauth): add PKCE S256 utilities - [x] 6. Create `/.well-known/oauth-authorization-server` endpoint What to do / Must NOT do: Create `src/routes/.well-known/oauth-authorization-server/+server.js` returning JSON metadata. Must query `scopes_supported` from SQLite3 `scopes` table dynamically. Must use `config.baseUrl` for all endpoint URLs. Must include: issuer, authorization_endpoint, token_endpoint, revocation_endpoint, introspection_endpoint, registration_endpoint, pushed_authorization_request_endpoint, require_pushed_authorization_requests (true), response_types_supported (["code"]), grant_types_supported (["authorization_code", "refresh_token"]), token_endpoint_auth_methods_supported, revocation_endpoint_auth_methods_supported, introspection_endpoint_auth_methods_supported, code_challenge_methods_supported (["S256"]), scopes_supported, service_documentation, ui_locales_supported (["en-US", "es-MX"]). Must NOT include "plain" in code_challenge_methods_supported. Must NOT include "bearer" alone in introspection auth methods. Parallelization: Wave 2 | Blocked by: 2,3 | Blocks: 7 References: `src/lib/config.js:1-19`, `src/lib/sqlite3/database.js:1-91`, RFC 8414 Section 2 Acceptance criteria: `curl -s http://localhost:3000/.well-known/oauth-authorization-server | jq '.issuer'` returns non-null QA scenarios: Happy: Returns valid JSON with all required fields. Failure: Missing baseUrl config throws 500. Scopes query returns correct list from DB. Commit: Y | feat(oauth): add OAuth 2.1 discovery metadata endpoint - [x] 7. Create `/oauth/register` client registration endpoint What to do / Must NOT do: Create `src/routes/oauth/register/+server.js` handling POST for dynamic client registration (RFC 7591). Store clients in SQLite3 `clients` table. Generate `client_id`, `client_secret` (if confidential client), `registration_access_token`. Return client metadata with `client_id_issued_at`. Must validate `redirect_uris` (exact, HTTPS). Must NOT allow `http://` except localhost. Must NOT register clients with unsupported grant types. Parallelization: Wave 2 | Blocked by: 2,3 | Blocks: 8,9,10 References: RFC 7591, `src/lib/sqlite3/database.js`, `src/routes/.well-known/oauth-authorization-server/+server.js` Acceptance criteria: `curl -s -X POST http://localhost:3000/oauth/register -H "Content-Type: application/json" -d '{"redirect_uris":["https://example.com/callback"],"client_name":"Test"}' | jq '.client_id'` returns non-null QA scenarios: Happy: Valid registration returns client_id. Failure: Missing redirect_uris returns 400. Invalid redirect URI returns 400. HTTP redirect URI (non-localhost) returns 400. Commit: Y | feat(oauth): add dynamic client registration endpoint - [x] 8. Create `/oauth/authorize` authorization endpoint What to do / Must NOT do: Create `src/routes/oauth/authorize/+server.js` handling GET. Validate `response_type=code`, `client_id`, `redirect_uri` (exact match against registered), `scope` (must be subset of registered), `code_challenge` (S256 only, required), `code_challenge_method` (must be "S256"). Store auth code in Redis with TTL (10 minutes) keyed by code. Value includes: client_id, redirect_uri, user_id, scope, code_challenge, expires_at. Redirect to `redirect_uri` with `?code=...&state=...&iss=...` (RFC 9207). Must NOT support `response_type=token`. Must NOT accept missing `code_challenge`. Parallelization: Wave 3 | Blocked by: 5,6,7 | Blocks: 9 References: draft-ietf-oauth-v2-1-15 Section 3.1, RFC 7636, `src/lib/oauth/pkce.js`, `src/lib/redis.js` Acceptance criteria: `curl -s "http://localhost:3000/oauth/authorize?response_type=code&client_id=TEST&redirect_uri=https://example.com/callback&scope=user&code_challenge=abc123&code_challenge_method=S256&state=xyz" -w "%{http_code}"` returns 302 QA scenarios: Happy: Valid request redirects with code. Failure: Missing PKCE returns 400. Invalid redirect_uri returns 400. Unsupported response_type returns 400. Expired client_id returns 400. Commit: Y | feat(oauth): add authorization endpoint with mandatory PKCE - [x] 9. Create `/oauth/token` token endpoint What to do / Must NOT do: Create `src/routes/oauth/token/+server.js` handling POST. Support `grant_type=authorization_code` and `refresh_token`. For authorization_code: validate code (from Redis), client auth, redirect_uri (required if in auth request), verify PKCE code_verifier against stored code_challenge. Issue opaque access_token and refresh_token, store in Redis with TTL. Return JSON with `access_token`, `token_type`, `expires_in`, `refresh_token`, `scope`. Must NOT support password grant. Must NOT support implicit. Must NOT issue tokens for invalid PKCE. Parallelization: Wave 3 | Blocked by: 6,7,8 | Blocks: 10 References: draft-ietf-oauth-v2-1-15 Section 3.2, `src/lib/oauth/tokens.js`, `src/lib/redis.js`, `src/lib/oauth/pkce.js` Acceptance criteria: `curl -s -X POST http://localhost:3000/oauth/token -d "grant_type=authorization_code&code=VALID_CODE&redirect_uri=https://example.com/callback&client_id=TEST&code_verifier=VERIFIER" | jq '.access_token'` returns non-null QA scenarios: Happy: Valid code exchange returns tokens. Failure: Invalid code returns 400. Missing code_verifier returns 400. Wrong redirect_uri returns 400. Expired code returns 400. Invalid client auth returns 401. Commit: Y | feat(oauth): add token endpoint with PKCE verification - [x] 10. Create `/oauth/introspect` and `/oauth/revoke` endpoints What to do / Must NOT do: Create `src/routes/oauth/introspect/+server.js` (POST) and `src/routes/oauth/revoke/+server.js` (POST). Introspection: authenticate client, look up token in Redis, return `{active: true/false, ...}` with token metadata if active. Revocation: authenticate client, delete token from Redis, return 200. Both must use client auth methods (client_secret_basic, client_secret_post). Must NOT accept bearer-only auth on introspection. Parallelization: Wave 3 | Blocked by: 6,7,9 | Blocks: — References: RFC 7662, RFC 7009, `src/lib/redis.js` Acceptance criteria: `curl -s -X POST http://localhost:3000/oauth/introspect -u "CLIENT_ID:SECRET" -d "token=VALID_TOKEN" | jq '.active'` returns true/false QA scenarios: Happy: Valid token introspection returns active=true with metadata. Valid revocation deletes token. Failure: Missing client auth returns 401. Invalid token returns active=false. Revoked token introspection returns active=false. Commit: Y | feat(oauth): add token introspection and revocation endpoints - [x] 11. Create `/oauth/par` Pushed Authorization Request endpoint What to do / Must NOT do: Create `src/routes/oauth/par/+server.js` handling POST. Accepts same parameters as authorize endpoint. Stores request in Redis with TTL (short-lived, e.g., 60s). Returns `request_uri` (urn:ietf:params:oauth:request_uri:...). Authorize endpoint must accept `request_uri` parameter and lookup stored request. Must set `require_pushed_authorization_requests: true` in discovery metadata. Parallelization: Wave 4 | Blocked by: 6,8 | Blocks: 12 References: RFC 9126, `src/lib/redis.js`, `src/routes/oauth/authorize/+server.js` Acceptance criteria: `curl -s -X POST http://localhost:3000/oauth/par -d "client_id=TEST&response_type=code&redirect_uri=https://example.com/callback&scope=user&code_challenge=abc123&code_challenge_method=S256" | jq '.request_uri'` returns non-null QA scenarios: Happy: PAR request returns request_uri. Authorize with request_uri redirects with code. Failure: Invalid client_id returns 400. Expired request_uri returns invalid_request. Commit: Y | feat(oauth): add PAR endpoint - [x] 12. Create `/oauth/login` and `/oauth/consent` UI pages What to do / Must NOT do: Create `src/routes/oauth/login/+page.svelte` and `src/routes/oauth/consent/+page.svelte`. Login: email/password form, validate against SQLite3 users table using existing Argon2id. Consent: display client name, requested scopes, allow/deny buttons. On allow, redirect to authorize endpoint with user context. Use Svelte 5 runes. Must NOT use TypeScript. Must use Flowbite components where appropriate. Parallelization: Wave 4 | Blocked by: 1 | Blocks: — References: `src/routes/+page.svelte:1-39`, `src/lib/crypto.js:1-13`, `src/lib/sqlite3/database.js:1-91` Acceptance criteria: `curl -s http://localhost:3000/oauth/login` returns HTML with login form. `curl -s http://localhost:3000/oauth/consent` returns HTML with consent UI. QA scenarios: Happy: Valid credentials log in. Consent allow redirects to authorize. Consent deny returns access_denied. Failure: Invalid credentials show error. Missing session redirects to login. Commit: Y | feat(ui): add OAuth login and consent pages - [ ] 13. Move `.omo/ulw-research/` artifacts to `docs/` What to do / Must NOT do: Copy or move files from `.omo/ulw-research/` to `docs/oauth-research/`. Must NOT delete originals (they are under `.omo/` which may be gitignored). Must ensure docs are readable. Parallelization: Wave 5 | Blocked by: — | Blocks: — References: `.omo/ulw-research/` Acceptance criteria: `ls docs/oauth-research/` shows files from `.omo/ulw-research/` QA scenarios: Happy: Files present in docs. Failure: Missing files or broken paths. Commit: Y | docs: move OAuth research to docs directory - [ ] 14. Final integration QA and security verification What to do / Must NOT do: Run dev server. Execute full OAuth 2.1 flow: register client -> PAR -> authorize (with login/consent) -> token exchange -> introspect -> revoke. Verify discovery metadata matches implementation. Verify no unsupported grants work. Verify no plain PKCE accepted. Verify tokens are opaque (not JWT). Must NOT skip negative testing. Parallelization: Wave 5 | Blocked by: all previous | Blocks: — References: All endpoint files, `src/routes/.well-known/oauth-authorization-server/+server.js` Acceptance criteria: Full flow completes successfully from register to revoke. QA scenarios: Happy: Complete end-to-end OAuth 2.1 flow. Failure: Unsupported grant returns 400. Plain PKCE rejected. Missing PKCE rejected. Invalid redirect URI rejected. Token stored in Redis (not sqlite3). Commit: Y | test(qa): verify full OAuth 2.1 integration ## Final verification wave > Runs in parallel after ALL todos. ALL must APPROVE. - [ ] F1. Plan compliance audit — Verify every must-have is implemented and every must-not-have is absent - [ ] F2. Code quality review — Verify Svelte 5 runes, consistent patterns, no hardcoded secrets - [ ] F3. Real manual QA — Execute full OAuth 2.1 flow end-to-end with curl/httpie - [ ] F4. Scope fidelity — Confirm no scope creep, no JWTs, no implicit grant, no sqlite3 token storage ## Commit strategy - One commit per todo (atomic commits) - Prefix: `feat(config):`, `feat(redis):`, `feat(db):`, `feat(oauth):`, `feat(ui):`, `docs:`, `test(qa):` - Clear, descriptive messages referencing the component ## Success criteria 1. `/.well-known/oauth-authorization-server` returns valid RFC 8414 metadata with `scopes_supported` from DB and `ui_locales_supported: ["en-US", "es-MX"]` 2. `/oauth/authorize` enforces PKCE S256 and exact redirect URI matching 3. `/oauth/token` issues opaque tokens stored in Redis with TTL 4. `/oauth/introspect` and `/oauth/revoke` require client authentication 5. `/oauth/par` accepts pushed authorization requests and returns `request_uri` 6. `/oauth/register` creates clients in SQLite3 with valid redirect URIs 7. Login and consent pages render and handle auth flow correctly 8. No auth codes, access tokens, or refresh tokens exist in SQLite3 9. Redis is the sole store for all expiring OAuth data 10. Full end-to-end flow executes successfully from registration to token revocation