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.
/.well-known/oauth-authorization-server metadata endpoint (RFC 8414)/oauth/authorize, /oauth/token, /oauth/introspect, /oauth/revoke/oauth/register (RFC 7591)plain)/oauth/par (RFC 9126) with require_pushed_authorization_requests: trueclient_secret_basic, client_secret_post, private_key_jwt, tls_client_auth/oauth/login)/oauth/consent)config.json updates: baseUrl, redis connection options (host, port, password, database number)ioredis dependency and Redis client moduleclients table onlyscopes_supported dynamically from SQLite3 scopes tableui_locales_supported: ["en-US", "es-MX"].omo/ulw-research/ to docs/response_type=token)client_credentials grantplain PKCE methodZero 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-<N>-oauth-21-server.<ext>
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
| 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 | — | — |
[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
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
feat(config):, feat(redis):, feat(db):, feat(oauth):, feat(ui):, docs:, test(qa):/.well-known/oauth-authorization-server returns valid RFC 8414 metadata with scopes_supported from DB and ui_locales_supported: ["en-US", "es-MX"]/oauth/authorize enforces PKCE S256 and exact redirect URI matching/oauth/token issues opaque tokens stored in Redis with TTL/oauth/introspect and /oauth/revoke require client authentication/oauth/par accepts pushed authorization requests and returns request_uri/oauth/register creates clients in SQLite3 with valid redirect URIs