this repo has no description
at main 185 lines 5.1 kB view raw view rendered
1# ATProto OAuth React Demo 2 3## Tech Stack 4- **Framework**: React 18 with Vite 5- **Language**: TypeScript 5.3 (strict mode) 6- **Styling**: Tailwind CSS 3.4 7- **State Management**: React Context API for auth 8- **Routing**: React Router v6 9- **HTTP Client**: Fetch API with custom hooks 10- **Authentication**: ATProtocol OAuth 2.1 with PKCE, DPoP, and PAR 11- **ATProto Libraries**: 12 - @atproto/oauth-client-browser for OAuth 13 - @atproto/api for XRPC and RichText 14 - @atproto/identity for handle/DID resolution 15 16## Project Structure 17``` 18src/ 19├── components/ 20│ ├── auth/ # Authentication components 21│ ├── post/ # Post creation components 22│ └── layout/ # Layout and navigation components 23├── contexts/ # React contexts (AuthContext) 24├── hooks/ # Custom hooks (useAuth, useATProto) 25├── lib/ # ATProtocol client setup 26├── utils/ # Utility functions (crypto, DPoP) 27└── types/ # TypeScript type definitions 28``` 29 30## Authentication Architecture 31 32### OAuth 2.1 Flow 331. **Discovery**: Handle → DID → PDS → Authorization Server 342. **Session Secrets**: PKCE verifier, DPoP keypair, state token 353. **PAR**: Pushed Authorization Request with nonce discovery 364. **Authorization**: User consent at PDS 375. **Callback**: Verify state and issuer 386. **Token Exchange**: Code for tokens with DPoP proof 397. **Identity Verification**: Verify sub (DID) matches expected 40 41### Token Management 42- **Access Tokens**: 15-30 min lifetime, DPoP-bound 43- **Storage**: sessionStorage (never localStorage) 44- **DPoP Keys**: IndexedDB, non-exportable CryptoKeyPair 45- **Refresh Tokens**: Single-use with mandatory rotation 46- **Auto-refresh**: Before access token expiry 47 48### Security Requirements 49- PKCE with S256 (mandatory) 50- DPoP with ES256 (mandatory) 51- PAR (Pushed Authorization Requests) 52- State parameter for CSRF protection 53- Issuer verification in callback 54- Bidirectional handle/DID verification 55 56## Coding Standards 57 58### TypeScript 59```typescript 60// ✅ Good 61interface AuthState { 62 user: ProfileView | null; 63 loading: boolean; 64 error: Error | null; 65} 66 67// ❌ Bad 68interface AuthState { 69 user: any; 70 loading: any; 71 error: any; 72} 73``` 74 75### React Patterns 76- Functional components with hooks only 77- Custom hooks for reusable logic 78- Context for auth state management 79- Error boundaries for graceful failures 80- Suspense for code splitting 81 82### Error Handling 83```typescript 84try { 85 const result = await oauthClient.authorize(handle); 86 // Handle success 87} catch (error) { 88 console.error('OAuth authorization failed:', error); 89 // User-friendly error message 90 setError('Failed to authenticate. Please try again.'); 91} 92``` 93 94### Security Best Practices 95- **Never** store tokens in localStorage 96- **Always** verify DID matches expected identity 97- **Always** generate fresh DPoP proof per request 98- **Always** validate state parameter in callback 99- **Never** export DPoP private keys 100 101## API Patterns 102 103### XRPC Requests 104```typescript 105// Every request needs Authorization and DPoP headers 106const response = await fetch(`${pdsUrl}/xrpc/${nsid}`, { 107 method: 'POST', 108 headers: { 109 'Authorization': `DPoP ${accessToken}`, 110 'DPoP': dpopProof, 111 'Content-Type': 'application/json' 112 }, 113 body: JSON.stringify(data) 114}); 115``` 116 117### Record Creation 118```typescript 119const record = { 120 repo: userDid, 121 collection: 'app.bsky.feed.post', 122 record: { 123 $type: 'app.bsky.feed.post', 124 text: postText, 125 createdAt: new Date().toISOString() 126 } 127}; 128``` 129 130## Testing Guidelines 131 132### Unit Tests 133- Crypto utilities (PKCE, DPoP generation) 134- DID resolution logic 135- Token refresh logic 136- Input validation 137 138### Integration Tests 139- Complete OAuth flow (mocked) 140- XRPC requests with DPoP 141- Protected route navigation 142 143## Environment Variables 144```env 145VITE_CLIENT_ID=https://oauth-spa.smokesignal.tools/client-metadata.json 146VITE_REDIRECT_URI=https://oauth-spa.smokesignal.tools/oauth/callback 147VITE_DEFAULT_PDS=https://bsky.social 148``` 149 150## Do's and Don'ts 151 152### Do's 153✅ Use sessionStorage for tokens 154✅ Generate fresh DPoP proof for each request 155✅ Verify DID matches expected identity 156✅ Handle token refresh proactively 157✅ Clear all storage on logout 158✅ Use RichText library for text processing 159✅ Implement proper error boundaries 160✅ Follow TypeScript strict mode 161 162### Don'ts 163❌ Use localStorage for sensitive data 164❌ Skip DPoP implementation 165❌ Forget bidirectional handle/DID verification 166❌ Use the same DPoP proof twice 167❌ Export DPoP private keys 168❌ Trust user input without validation 169❌ Use console.log in production 170❌ Create commits unless asked 171 172## Development Commands 173```bash 174npm run dev # Start dev server 175npm run build # Build for production 176npm run preview # Preview production build 177npm run test # Run tests 178``` 179 180## Key Files 181- `public/client-metadata.json` - OAuth client metadata 182- `src/lib/atproto-client.ts` - ATProto client setup 183- `src/utils/crypto-utils.ts` - PKCE and DPoP utilities 184- `src/contexts/AuthContext.tsx` - Authentication state 185- `src/hooks/useAuth.ts` - Auth hook for components