this repo has no description

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#

// ✅ 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#

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#

// 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#

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#

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#

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