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)
183 ▼
184┌──────────────────────────────────────────┐
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└──────────────────────────────────────────┘
227 │
228 │ HTTP-only session cookie
229 ▼
230┌─────────────────┐
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**