WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
1# OAuth Implementation Summary (ATB-14) 2 3**Status:** Complete 4**Linear Issue:** [ATB-14](https://linear.app/atbb/issue/ATB-14/implement-at-proto-oauth-flow) 5**Implementation Date:** February 2026 6**Tested With:** bsky.social PDS 7 8## Overview 9 10This document summarizes the implementation of AT Protocol OAuth authentication for the atBB forum AppView. The implementation enables users to log in with their AT Protocol identity (DID) and grants the AppView delegated access to write records on the user's behalf. 11 12## What Was Built 13 14### 1. OAuth Client Integration 15 16**Implementation:** Official `@atproto/oauth-client-node` library (v0.3.16) 17 18The OAuth implementation uses AT Protocol's official Node.js OAuth client library, which handles: 19 20- **Multi-PDS Discovery:** Automatic handle resolution across any AT Protocol PDS (not limited to bsky.social) 21- **PKCE (Proof Key for Code Exchange):** Secure public client flow without client secrets 22- **State Parameter Validation:** CSRF protection during authorization callback 23- **Token Management:** Automatic token refresh, DPoP-bound tokens, secure storage 24- **Handle Resolution:** DNS, .well-known, and DID document resolution 25 26**Key Benefits of Using the Library:** 27- Battle-tested security (used by Bluesky official clients) 28- Automatic token refresh eliminates expired session issues 29- Multi-PDS support without manual resolution code 30- DPoP implementation handled correctly 31- Future-proof as AT Protocol evolves 32 33**Key Files:** 34- `apps/appview/src/routes/auth.ts` — OAuth endpoints (login, callback, logout, session) 35- `apps/appview/src/lib/oauth-stores.ts` — OAuth state/session storage adapters 36- `apps/appview/src/lib/cookie-session-store.ts` — HTTP cookie to DID mapping 37- `apps/appview/src/lib/app-context.ts` — NodeOAuthClient initialization 38 39### 2. Session Management 40 41**Implementation:** Two-layer session architecture 42 43The session system uses a dual-store architecture: 44 451. **OAuth Session Store:** Library-managed OAuth sessions (indexed by DID) 46 - Stores OAuth tokens, DPoP keys, refresh tokens 47 - Handles automatic token refresh 48 - Implemented via `OAuthSessionStore` adapter (in-memory MVP) 49 502. **Cookie Session Store:** Maps HTTP cookies to DIDs 51 - Lightweight mapping from random cookie token → user DID 52 - Allows OAuth session restoration via `oauthClient.restore(did)` 53 - Stores user handle for display purposes 54 - Automatic cleanup of expired sessions 55 56**Session Security:** 57- **httpOnly Cookies:** JavaScript cannot access session tokens (XSS protection) 58- **Secure Flag:** Cookies only sent over HTTPS in production 59- **SameSite=Lax:** CSRF protection for state-changing operations 60- **Configurable TTL:** Default 7 days, configurable via SESSION_TTL_DAYS 61- **Automatic Cleanup:** Expired sessions removed every 5 minutes 62 63**Key Files:** 64- `apps/appview/src/lib/cookie-session-store.ts` — CookieSessionStore implementation 65- `apps/appview/src/lib/oauth-stores.ts` — OAuthStateStore and OAuthSessionStore adapters 66- `apps/appview/src/lib/app-context.ts` — Session store initialization 67 68### 3. Authentication Middleware 69 70**Implementation:** Hono middleware for route protection 71 72Two middleware functions provide flexible authentication: 73 74#### `requireAuth(ctx)` — Enforce Authentication 75 76Requires valid session for route access: 77- Extracts session cookie 78- Restores OAuth session from library 79- Creates authenticated Agent with DPoP 80- Attaches user to context: `c.get('user')` 81- Returns 401 if session missing/invalid 82- Returns 500 on unexpected errors 83 84#### `optionalAuth(ctx)` — Conditional Authentication 85 86Validates session if present, allows unauthenticated access: 87- Extracts session cookie if present 88- Restores OAuth session if available 89- Attaches user to context if authenticated 90- Cleans up invalid cookies automatically 91- Never returns errors (fails silently) 92- Useful for public pages with auth-dependent features 93 94**Key Files:** 95- `apps/appview/src/middleware/auth.ts` — requireAuth and optionalAuth implementations 96- `apps/appview/src/types.ts` — AuthenticatedUser and Variables type definitions 97 98### 4. OAuth Endpoints 99 100**Implementation:** RESTful API routes for authentication flow 101 102Four endpoints handle the complete authentication lifecycle: 103 104#### `GET /api/auth/login?handle={handle}` 105 106Initiates OAuth flow: 1071. Validates handle parameter (returns 400 if missing) 1082. Calls `oauthClient.authorize(handle)` to: 109 - Resolve handle → DID → PDS URL (multi-PDS support) 110 - Generate PKCE verifier and challenge 111 - Create state parameter for CSRF protection 112 - Build authorization URL 1133. Redirects user to their PDS authorization endpoint 1144. PDS presents authorization UI to user 115 116**Error Handling:** 117- 400 Bad Request: Invalid handle or PDS not found (client error) 118- 500 Internal Server Error: Network failures, unexpected errors (server error) 119- Logs all errors with structured context 120 121#### `GET /api/auth/callback?code={code}&state={state}&iss={issuer}` 122 123Handles OAuth callback from PDS: 1241. Checks for user denial (`error=access_denied`) 1252. Parses callback parameters 1263. Calls `oauthClient.callback(params)` to: 127 - Validate state parameter (CSRF check) 128 - Verify PKCE code challenge 129 - Exchange authorization code for tokens with DPoP 130 - Store OAuth session in library's session store 1314. Fetches user profile to get handle 1325. Creates cookie session mapping (cookie token → DID + handle) 1336. Sets HTTP-only session cookie 1347. Redirects to homepage 135 136**Error Handling:** 137- 400 Bad Request: CSRF/PKCE validation failures (security errors logged) 138- 500 Internal Server Error: Token exchange failures, network errors 139- Fails login if handle fetch fails (no silent fallbacks) 140 141#### `GET /api/auth/session` 142 143Returns current session information: 1441. Checks for session cookie 1452. Restores OAuth session via `oauthClient.restore(did)` 1463. Gets token info (library handles expiry checks and refresh) 1474. Returns `{ authenticated: true, did, sub }` or `{ authenticated: false }` (401) 148 149**Error Handling:** 150- 401 Unauthorized: No cookie or session expired/invalid 151- 500 Internal Server Error: Unexpected errors during restoration 152- Does not delete cookie on transient errors (may be temporary) 153 154#### `GET /api/auth/logout` 155 156Clears user session and revokes tokens: 1571. Extracts session cookie 1582. Restores OAuth session 1593. Calls `oauthSession.signOut()` to revoke tokens at PDS 1604. Deletes cookie session locally 1615. Clears session cookie 1626. Returns success or redirects 163 164**Error Handling:** 165- Continues with local cleanup even if PDS revocation fails 166- Logs errors but does not fail the logout request 167- Validates redirect parameter to prevent open redirect attacks 168 169**Key Files:** 170- `apps/appview/src/routes/auth.ts` — All OAuth route handlers 171 172## Architecture 173 174### Component Diagram 175 176``` 177┌─────────────────┐ 178│ User's PDS │ 179│ (any PDS) │ 180└────────┬────────┘ 181 │ OAuth 2.1 Flow 182 │ (PKCE + DPoP) 183184┌──────────────────────────────────────────┐ 185│ AppView (Port 3000) │ 186│ │ 187│ ┌────────────────────────────────────┐ │ 188│ │ NodeOAuthClient │ │ 189│ │ (@atproto/oauth-client-node) │ │ 190│ │ - Multi-PDS handle resolution │ │ 191│ │ - PKCE generation/validation │ │ 192│ │ - State management (CSRF) │ │ 193│ │ - Token exchange with DPoP │ │ 194│ │ - Automatic token refresh │ │ 195│ └────────────────────────────────────┘ │ 196│ │ 197│ ┌────────────────────────────────────┐ │ 198│ │ OAuth Session Stores │ │ 199│ │ - OAuthStateStore (PKCE/state) │ │ 200│ │ - OAuthSessionStore (tokens) │ │ 201│ │ - In-memory Map (MVP) │ │ 202│ └────────────────────────────────────┘ │ 203│ │ 204│ ┌────────────────────────────────────┐ │ 205│ │ Cookie Session Store │ │ 206│ │ - Maps cookie token → DID │ │ 207│ │ - Stores handle for display │ │ 208│ │ - Automatic expiry cleanup │ │ 209│ └────────────────────────────────────┘ │ 210│ │ 211│ ┌────────────────────────────────────┐ │ 212│ │ Auth Middleware │ │ 213│ │ - requireAuth (enforce) │ │ 214│ │ - optionalAuth (conditional) │ │ 215│ │ - Creates Agent with DPoP │ │ 216│ │ - Injects user to context │ │ 217│ └────────────────────────────────────┘ │ 218│ │ 219│ ┌────────────────────────────────────┐ │ 220│ │ Auth Routes │ │ 221│ │ - /login (initiate) │ │ 222│ │ - /callback (complete) │ │ 223│ │ - /logout (revoke + clear) │ │ 224│ │ - /session (check) │ │ 225│ └────────────────────────────────────┘ │ 226└──────────────────────────────────────────┘ 227228 │ HTTP-only session cookie 229230┌─────────────────┐ 231│ Web UI │ 232│ (Port 3001) │ 233└─────────────────┘ 234``` 235 236### Data Flow 237 238**Login Flow:** 2391. User visits Web UI, clicks "Login", enters handle 2402. Web UI redirects to `GET /api/auth/login?handle=user.bsky.social` 2413. AppView calls `oauthClient.authorize(handle)`: 242 - Library resolves handle → DID → PDS URL (works with any PDS) 243 - Generates PKCE verifier (stored in OAuthStateStore) 244 - Creates state parameter (stored in OAuthStateStore) 245 - Builds authorization URL with all parameters 2464. AppView redirects browser to user's PDS authorization endpoint 2475. User approves access at their PDS 2486. PDS redirects to `GET /api/auth/callback?code=...&state=...&iss=...` 2497. AppView calls `oauthClient.callback(params)`: 250 - Validates state parameter (CSRF check) 251 - Verifies PKCE code challenge 252 - Exchanges authorization code for access/refresh tokens with DPoP 253 - Stores tokens in OAuthSessionStore 2548. AppView fetches user profile to get handle 2559. AppView creates cookie session (random token → DID + handle) 25610. AppView sets HTTP-only session cookie 25711. AppView redirects to Web UI homepage 25812. Web UI calls `GET /api/auth/session` to verify login 259 260**Authenticated Request Flow:** 2611. Web UI makes request with session cookie 2622. Auth middleware extracts cookie token 2633. Middleware gets DID from CookieSessionStore 2644. Middleware calls `oauthClient.restore(did)`: 265 - Library checks OAuthSessionStore for session 266 - Automatically refreshes tokens if near expiry 267 - Returns OAuthSession with valid tokens 2685. Middleware creates Agent with OAuthSession (DPoP-enabled) 2696. Middleware attaches AuthenticatedUser to context 2707. Route handler accesses `c.get('user')` 271 272**Logout Flow:** 2731. User clicks "Logout" in Web UI 2742. Web UI calls `GET /api/auth/logout` 2753. AppView restores OAuth session 2764. AppView calls `oauthSession.signOut()`: 277 - Library revokes tokens at the PDS 278 - Cleans up local OAuth session 2795. AppView deletes cookie session 2806. AppView clears session cookie 2817. Web UI redirects to homepage 282 283## Security Features 284 285### OAuth Security 286 287- **PKCE Flow:** Prevents authorization code interception attacks (S256 challenge) 288- **State Parameter:** CSRF protection during callback 289- **DPoP Tokens:** Token-bound to cryptographic key, prevents token theft/replay 290- **No Client Secrets:** Public client pattern (safe for web apps) 291- **Multi-PDS Support:** Works with any AT Protocol PDS, not hardcoded to bsky.social 292- **Automatic Token Rotation:** Library refreshes tokens before expiry 293 294### Session Security 295 296- **httpOnly Cookies:** JavaScript cannot access session tokens (XSS protection) 297- **Secure Flag:** Cookies only sent over HTTPS in production 298- **SameSite=Lax:** CSRF protection for state-changing operations 299- **Session Expiry:** Configurable TTL (default 7 days) 300- **Automatic Cleanup:** Expired sessions removed every 5 minutes 301- **No Token Logging:** Access tokens never written to logs 302- **Cookie Cleanup:** Invalid cookies deleted to prevent repeated validation 303 304### Error Handling 305 306- **Proper HTTP Status Codes:** 400 for client errors, 401 for auth errors, 500 for server errors 307- **Security Logging:** CSRF/PKCE failures logged with high severity 308- **No Silent Fallbacks:** Errors fail explicitly rather than fabricating data 309- **Expected vs Unexpected:** Session-not-found returns null, network errors throw 310- **User-Friendly Messages:** Generic errors for users, detailed logs for debugging 311 312### Code Security 313 314- **Type Safety:** Full TypeScript coverage with strict types 315- **Input Validation:** Handle and state parameters validated 316- **No Sensitive Data in Errors:** Generic error messages for users 317- **Open Redirect Prevention:** Redirect parameter validated in logout 318 319## Known Limitations (MVP) 320 321### 1. In-Memory Session Storage 322 323**Limitation:** Sessions stored in JavaScript `Map` are lost on server restart. 324 325**Impact:** 326- Users logged out on AppView restart 327- Cannot scale to multiple AppView instances (no shared session state) 328 329**Mitigation Path:** Replace with PostgreSQL or Redis-backed stores (both implement same SimpleStore<K, V> interface) 330 331**Tracked In:** Post-MVP improvements (Priority 1) 332 333### 2. No Session Persistence Across Deployments 334 335**Limitation:** Sessions not persisted to database. 336 337**Impact:** 338- Rolling deployments log out all users 339- Downtime during updates requires re-authentication 340 341**Mitigation Path:** Implement persistent session store 342 343**Tracked In:** Post-MVP improvements (Priority 1) 344 345### 3. Race Conditions in Token Refresh 346 347**Limitation:** `requestLock` implementation uses in-memory Map for single-instance deployments. 348 349**Impact:** 350- Multi-instance deployments may have race conditions during token refresh 351- Multiple AppView instances could refresh tokens simultaneously 352 353**Mitigation Path:** Use Redis-based distributed locking (redlock) for production 354 355**Tracked In:** Code review comments (requestLock documentation) 356 357## Post-MVP Improvements 358 359### Priority 1: Production-Ready Session Storage 360 361**Goal:** Replace in-memory sessions with persistent storage. 362 363**Options:** 3641. **PostgreSQL:** Reuse existing database, add `oauth_sessions` and `cookie_sessions` tables 3652. **Redis:** High-performance key-value store, built for sessions, supports TTL natively 3663. **Drizzle ORM:** Extend current schema with session tables 367 368**Recommendation:** Redis for sessions (performance + TTL), PostgreSQL for forum data. 369 370**Implementation Steps:** 3711. Create Redis-backed implementations of `OAuthSessionStore`, `OAuthStateStore`, `CookieSessionStore` 3722. Implement SimpleStore<K, V> interface for each 3733. Update `AppContext` to use Redis stores when REDIS_URL is set 3744. Test multi-instance deployment with shared session state 3755. Implement distributed locking for token refresh (redlock) 376 377**Estimated Effort:** 2-3 days 378 379### Priority 2: Session Security Hardening 380 381**Goal:** Add production-grade session security features. 382 383**Features:** 384- Session rotation on authentication (prevent session fixation) 385- Session fingerprinting (IP, User-Agent) for suspicious activity detection 386- Session revocation API for users to logout all devices 387- Rate limiting on login attempts 388- Brute force protection 389 390**Estimated Effort:** 2-3 days 391 392### Priority 3: Monitoring and Observability 393 394**Goal:** Track OAuth flow health and session metrics. 395 396**Metrics to Track:** 397- Login success/failure rates by error type 398- Token refresh success/failure rates 399- Session creation/deletion rates 400- Active session count 401- Session duration distribution 402- OAuth flow latency (P50, P95, P99) 403- Error types and frequencies 404 405**Tools:** Prometheus metrics, Grafana dashboards, structured JSON logging 406 407**Estimated Effort:** 1-2 days 408 409### Priority 4: Integration Testing 410 411**Goal:** Automated test suite for authentication flows. 412 413**Test Coverage:** 414- OAuth flow with mock PDS 415- Session CRUD operations 416- Token refresh flow 417- Error scenarios (invalid codes, expired tokens, CSRF) 418- Security tests (open redirect, XSS, cookie theft) 419- Multi-tab session sharing 420 421**Tools:** Vitest, Playwright, mock OAuth server 422 423**Estimated Effort:** 3-5 days 424 425## Environment Variables 426 427### Required Configuration 428 429Add these to `.env`: 430 431```bash 432# OAuth Configuration 433OAUTH_PUBLIC_URL=http://localhost:3000 # Production: https://forum.atbb.space 434SESSION_SECRET=<generate-with-openssl-rand-base64-32> 435SESSION_TTL_DAYS=7 # Optional, defaults to 7 436 437# Optional: Redis for production session storage 438# REDIS_URL=redis://localhost:6379 439``` 440 441### Generating SESSION_SECRET 442 443**Security Requirement:** Must be at least 32 characters. 444 445```bash 446# Generate cryptographically secure random secret 447openssl rand -base64 32 448# Or use Node.js 449node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" 450``` 451 452**Warning:** Never commit SESSION_SECRET to version control. The `.env.example` file intentionally uses an invalid value (`CHANGE_ME_SEE_COMMENT_BELOW`) to force generation. 453 454### Production Considerations 455 456For production deployments: 457 4581. **HTTPS Required:** 459 - Set OAUTH_PUBLIC_URL to https:// URL 460 - Secure cookie flag automatically enabled in production 461 4622. **Public URL Must Match:** 463 - OAUTH_PUBLIC_URL must match the actual deployment URL 464 - Used for OAuth client metadata and redirect URI 465 4663. **Redis Recommended:** 467 - Set REDIS_URL for persistent sessions 468 - Enables multi-instance deployments 469 - Provides automatic TTL and cleanup 470 4714. **Security:** 472 - Generate unique SESSION_SECRET per environment 473 - Rotate SESSION_SECRET periodically (logs out all users) 474 - Monitor failed login attempts 475 476## Migration from Password Auth 477 478### OAuth Implementation 479 480OAuth flow: 481 482Replace with OAuth flow: 483```typescript 484// User initiates login via web UI 485// AppView handles OAuth flow 486// Route handlers receive authenticated user from middleware: 487app.post('/api/posts', requireAuth(ctx), async (c) => { 488 const user = c.get('user'); // { did, handle, pdsUrl, agent } 489 // Agent is pre-configured with DPoP tokens 490 await user.agent.com.atproto.repo.createRecord({ ... }); 491}); 492``` 493 494### Migration Steps 495 4961.**OAuth implemented** — Users can now log in via web UI 4972.**Update write endpoints** — Use OAuth sessions instead of password auth 4983.**Admin login** — Decide: OAuth or keep password for forum service account? 499 500## Next Steps 501 502### Completed Since This Document 503 5041.**ATB-15: Auto-Create Membership** — Membership records now auto-created on first login (PR #27) 5052.**ATB-12: Write Endpoints** — Topic and reply creation endpoints implemented with OAuth sessions 506 507### Immediate (Phase 2 Continuation) 508 5091. **ATB-16: Session Management Enhancements** — Redis-backed session store 5102. **ATB-17: Permission Middleware** — Role-based access control for protected routes 5113. **ATB-18: Forum DID Agent** — Dedicated agent for forum-level operations 512 513### Phase 4: Web UI Integration 514 5151. **Login/Logout UI** — Add login button, logout button, user menu 5162. **Session Display** — Show logged-in user's handle in header 5173. **Protected Actions** — Show/hide compose forms based on auth status 5184. **Error Messages** — Display OAuth errors to users (denied auth, expired session) 519 520## References 521 522### Documentation 523 524- [AT Protocol OAuth Specification](https://atproto.com/specs/oauth) 525- [`@atproto/oauth-client-node` Documentation](https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-node) 526- [OAuth 2.1 Specification (Draft)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07) 527- [OAuth 2.0 PKCE Specification (RFC 7636)](https://datatracker.ietf.org/doc/html/rfc7636) 528- [DPoP Specification (RFC 9449)](https://datatracker.ietf.org/doc/html/rfc9449) 529 530### Code Files 531 532**OAuth Routes:** 533- `apps/appview/src/routes/auth.ts` — OAuth endpoints (login, callback, logout, session) 534 535**Session Management:** 536- `apps/appview/src/lib/oauth-stores.ts` — OAuthStateStore and OAuthSessionStore adapters 537- `apps/appview/src/lib/cookie-session-store.ts` — CookieSessionStore implementation 538 539**Authentication:** 540- `apps/appview/src/middleware/auth.ts` — requireAuth and optionalAuth middleware 541- `apps/appview/src/types.ts` — AuthenticatedUser and Variables types 542 543**Configuration:** 544- `apps/appview/src/lib/app-context.ts` — NodeOAuthClient initialization 545- `apps/appview/src/lib/config.ts` — OAuth configuration validation 546 547**Tests:** 548- `apps/appview/src/lib/__tests__/config.test.ts` — Configuration validation tests 549- `apps/appview/src/lib/__tests__/test-context.ts` — Test helper for OAuth context 550 551### Related Issues 552 553- [ATB-14: Implement AT Proto OAuth flow](https://linear.app/atbb/issue/ATB-14) (Complete ✅) 554- [ATB-15: Auto-create membership on first login](https://linear.app/atbb/issue/ATB-15) (Complete ✅) 555- [ATB-12: Write endpoints implementation](https://linear.app/atbb/issue/ATB-12) (Complete ✅) 556- ATB-16: Redis-backed session storage (Pending) 557- ATB-17: Permission middleware (Pending) 558- ATB-18: Forum DID agent (Pending) 559 560## Contributors 561 562Implementation by Claude Code (Anthropic) with guidance from project maintainer. 563 564**Implementation Period:** February 7-9, 2026 565**Code Review:** February 8-9, 2026 566**Documentation:** February 9, 2026 567 568--- 569 570**End of OAuth Implementation Summary**