# ATProto OAuth React Demo ## Tech Stack - **Framework**: React 18 with Vite - **Language**: TypeScript 5.3 (strict mode) - **Styling**: Tailwind CSS 3.4 - **State Management**: React Context API for auth - **Routing**: React Router v6 - **HTTP Client**: Fetch API with custom hooks - **Authentication**: ATProtocol OAuth 2.1 with PKCE, DPoP, and PAR - **ATProto Libraries**: - @atproto/oauth-client-browser for OAuth - @atproto/api for XRPC and RichText - @atproto/identity for handle/DID resolution ## Project Structure ``` src/ ├── components/ │ ├── auth/ # Authentication components │ ├── post/ # Post creation components │ └── layout/ # Layout and navigation components ├── contexts/ # React contexts (AuthContext) ├── hooks/ # Custom hooks (useAuth, useATProto) ├── lib/ # ATProtocol client setup ├── utils/ # Utility functions (crypto, DPoP) └── types/ # TypeScript type definitions ``` ## Authentication Architecture ### OAuth 2.1 Flow 1. **Discovery**: Handle → DID → PDS → Authorization Server 2. **Session Secrets**: PKCE verifier, DPoP keypair, state token 3. **PAR**: Pushed Authorization Request with nonce discovery 4. **Authorization**: User consent at PDS 5. **Callback**: Verify state and issuer 6. **Token Exchange**: Code for tokens with DPoP proof 7. **Identity Verification**: Verify sub (DID) matches expected ### Token Management - **Access Tokens**: 15-30 min lifetime, DPoP-bound - **Storage**: sessionStorage (never localStorage) - **DPoP Keys**: IndexedDB, non-exportable CryptoKeyPair - **Refresh Tokens**: Single-use with mandatory rotation - **Auto-refresh**: Before access token expiry ### Security Requirements - PKCE with S256 (mandatory) - DPoP with ES256 (mandatory) - PAR (Pushed Authorization Requests) - State parameter for CSRF protection - Issuer verification in callback - Bidirectional handle/DID verification ## Coding Standards ### TypeScript ```typescript // ✅ Good interface AuthState { user: ProfileView | null; loading: boolean; error: Error | null; } // ❌ Bad interface AuthState { user: any; loading: any; error: any; } ``` ### React Patterns - Functional components with hooks only - Custom hooks for reusable logic - Context for auth state management - Error boundaries for graceful failures - Suspense for code splitting ### Error Handling ```typescript try { const result = await oauthClient.authorize(handle); // Handle success } catch (error) { console.error('OAuth authorization failed:', error); // User-friendly error message setError('Failed to authenticate. Please try again.'); } ``` ### Security Best Practices - **Never** store tokens in localStorage - **Always** verify DID matches expected identity - **Always** generate fresh DPoP proof per request - **Always** validate state parameter in callback - **Never** export DPoP private keys ## API Patterns ### XRPC Requests ```typescript // Every request needs Authorization and DPoP headers const response = await fetch(`${pdsUrl}/xrpc/${nsid}`, { method: 'POST', headers: { 'Authorization': `DPoP ${accessToken}`, 'DPoP': dpopProof, 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ``` ### Record Creation ```typescript const record = { repo: userDid, collection: 'app.bsky.feed.post', record: { $type: 'app.bsky.feed.post', text: postText, createdAt: new Date().toISOString() } }; ``` ## Testing Guidelines ### Unit Tests - Crypto utilities (PKCE, DPoP generation) - DID resolution logic - Token refresh logic - Input validation ### Integration Tests - Complete OAuth flow (mocked) - XRPC requests with DPoP - Protected route navigation ## Environment Variables ```env VITE_CLIENT_ID=https://oauth-spa.smokesignal.tools/client-metadata.json VITE_REDIRECT_URI=https://oauth-spa.smokesignal.tools/oauth/callback VITE_DEFAULT_PDS=https://bsky.social ``` ## Do's and Don'ts ### Do's ✅ Use sessionStorage for tokens ✅ Generate fresh DPoP proof for each request ✅ Verify DID matches expected identity ✅ Handle token refresh proactively ✅ Clear all storage on logout ✅ Use RichText library for text processing ✅ Implement proper error boundaries ✅ Follow TypeScript strict mode ### Don'ts ❌ Use localStorage for sensitive data ❌ Skip DPoP implementation ❌ Forget bidirectional handle/DID verification ❌ Use the same DPoP proof twice ❌ Export DPoP private keys ❌ Trust user input without validation ❌ Use console.log in production ❌ Create commits unless asked ## Development Commands ```bash npm run dev # Start dev server npm run build # Build for production npm run preview # Preview production build npm run test # Run tests ``` ## Key Files - `public/client-metadata.json` - OAuth client metadata - `src/lib/atproto-client.ts` - ATProto client setup - `src/utils/crypto-utils.ts` - PKCE and DPoP utilities - `src/contexts/AuthContext.tsx` - Authentication state - `src/hooks/useAuth.ts` - Auth hook for components