Highly ambitious ATProtocol AppView service and sdks
at fix-postgres 259 lines 7.8 kB view raw view rendered
1# @slices/oauth 2 3[AIP](https://github.com/graze-social/aip) OAuth 2.1 client with PKCE support 4and Device Authorization Grant (RFC 8628) for TypeScript. 5 6## Features 7 8- OAuth 2.1 with PKCE flow 9- Device Authorization Grant (RFC 8628) support 10- Token refresh handling 11- Pluggable storage backends (SQLite, Deno KV, others can be added) 12- Automatic token management and refresh 13 14## Installation 15 16### Deno 17 18```bash 19deno add jsr:@slices/oauth 20``` 21 22```typescript 23import { OAuthClient, SQLiteOAuthStorage } from "@slices/oauth"; 24``` 25 26For node based package managers: 27 28```bash 29# pnpm (10.9+) 30pnpm install jsr:@slices/oauth 31 32# Yarn (4.9+) 33yarn add jsr:@slices/oauth 34``` 35 36## Usage 37 38### Standard OAuth Flow (Web Applications) 39 40OAuth clients are session-scoped. Each client instance is tied to a specific session ID, enabling proper multi-device support where each session has independent OAuth tokens. 41 42```typescript 43import { OAuthClient, SQLiteOAuthStorage } from "@slices/oauth"; 44 45// Set up storage 46const storage = new SQLiteOAuthStorage("oauth.db"); 47 48// OAuth configuration 49const config = { 50 clientId: "your-client-id", 51 clientSecret: "your-client-secret", 52 authBaseUrl: "https://auth.example.com", 53 redirectUri: "http://localhost:8000/oauth/callback", 54 scopes: ["atproto"], 55}; 56 57// Start authorization flow (before we have a session) 58const tempClient = new OAuthClient(config, storage, "temp"); 59const result = await tempClient.authorize({ loginHint: "user.bsky.social" }); 60// Redirect user to result.authorizationUrl 61 62// Handle callback 63const tokens = await tempClient.handleCallback({ code, state }); 64 65// Create session and store tokens by sessionId 66// (typically done via @slices/session - see Session Integration below) 67const sessionId = "user-session-id"; // from session store 68const sessionClient = new OAuthClient(config, storage, sessionId); 69await storage.setTokens(tokens, sessionId); 70 71// Use the session-scoped client for API calls 72const userInfo = await sessionClient.getUserInfo(); 73``` 74 75### Session-Scoped Pattern 76 77The key concept: **one OAuth client per session**. This enables: 78- Multiple active sessions per user (different devices) 79- Independent token management per session 80- Proper logout isolation (logging out one device doesn't affect others) 81 82```typescript 83// Each session gets its own OAuth client 84function createSessionClient(sessionId: string) { 85 return new OAuthClient(config, storage, sessionId); 86} 87 88// Session 1 (laptop) 89const laptopClient = createSessionClient("session-laptop-123"); 90 91// Session 2 (phone) - completely independent tokens 92const phoneClient = createSessionClient("session-phone-456"); 93``` 94 95### Device Authorization Grant (CLI Applications) 96 97The Device Authorization Grant flow is perfect for CLI applications, TVs, or 98other devices without a browser or with limited input capabilities. 99 100```typescript 101import { DeviceCodeClient } from "@slices/oauth"; 102 103// Create device code client 104const client = new DeviceCodeClient({ 105 clientId: "your-cli-client-id", 106 authBaseUrl: "https://auth.example.com", 107 scopes: ["atproto", "transition:generic", "repo:*"], 108}); 109 110// Start device code flow 111const deviceAuth = await client.startDeviceAuth(); 112 113// Display the user code to the user 114console.log(`Please visit: ${deviceAuth.verification_uri_complete}`); 115console.log( 116 `Or go to ${deviceAuth.verification_uri} and enter code: ${deviceAuth.user_code}`, 117); 118 119// Poll for tokens (handles the polling interval automatically) 120const tokens = await client.pollForTokens(deviceAuth); 121 122// Store the tokens securely 123console.log("Authentication successful!"); 124console.log("Access token:", tokens.access_token); 125console.log("DID:", tokens.sub); 126``` 127 128#### Device Code Flow Features 129 130- **Automatic polling**: The client handles the polling interval specified by 131 the server 132- **User-friendly codes**: Short, easy-to-type codes for manual entry 133- **Browser integration**: Can automatically open the browser to the 134 verification URL 135- **Timeout handling**: Automatically handles device code expiration 136- **Error recovery**: Handles slow_down responses and other polling errors 137 138#### Typical Flow 139 1401. **Device requests authorization**: The device (CLI app) requests a device 141 code and user code 1422. **User authorizes**: The user visits the verification URL and enters the code 1433. **Device polls for tokens**: The device polls the authorization server until 144 the user completes authorization 1454. **Tokens received**: Once authorized, the device receives access and refresh 146 tokens 147 148#### Example: CLI Application with Device Flow 149 150```typescript 151import { DeviceCodeClient } from "@slices/oauth"; 152 153export class CLIAuthManager { 154 private client: DeviceCodeClient; 155 156 constructor(clientId: string, authBaseUrl: string) { 157 this.client = new DeviceCodeClient({ 158 clientId, 159 authBaseUrl, 160 scopes: [ 161 "atproto", 162 "transition:generic", 163 "repo:network.slices.slice", 164 "repo:network.slices.lexicon", 165 ], 166 }); 167 } 168 169 async login(): Promise<AuthTokens> { 170 // Start device authorization 171 const deviceAuth = await this.client.startDeviceAuth(); 172 173 // Show user instructions 174 console.log("🔐 Authenticate with Slices"); 175 console.log(""); 176 console.log(`Please visit: ${deviceAuth.verification_uri_complete}`); 177 console.log(""); 178 console.log("Or manually:"); 179 console.log(`1. Go to: ${deviceAuth.verification_uri}`); 180 console.log(`2. Enter code: ${deviceAuth.user_code}`); 181 console.log(""); 182 console.log("Waiting for authorization..."); 183 184 // Optionally open browser automatically 185 if (Deno.build.os !== "linux") { 186 await this.openBrowser(deviceAuth.verification_uri_complete); 187 } 188 189 // Poll for tokens 190 try { 191 const tokens = await this.client.pollForTokens(deviceAuth); 192 console.log("✅ Authentication successful!"); 193 194 // Get user info to find out who authenticated 195 const userInfo = await this.getUserInfo(tokens); 196 console.log("Authenticated as:", userInfo?.sub); 197 198 return tokens; 199 } catch (error) { 200 if (error.message.includes("expired")) { 201 throw new Error("Device code expired. Please try again."); 202 } 203 throw error; 204 } 205 } 206 207 private async openBrowser(url: string): Promise<void> { 208 const cmd = Deno.build.os === "darwin" 209 ? "open" 210 : Deno.build.os === "windows" 211 ? "start" 212 : "xdg-open"; 213 214 try { 215 await new Deno.Command(cmd, { args: [url] }).output(); 216 } catch { 217 // Ignore errors - user can manually open the URL 218 } 219 } 220} 221``` 222 223### Token Management 224 225Both OAuth flows support automatic token refresh. The client handles token refresh automatically: 226 227```typescript 228// The client ensures tokens are valid before making requests 229const sessionClient = new OAuthClient(config, storage, sessionId); 230 231// Automatically refreshes if needed 232const tokens = await sessionClient.ensureValidToken(); 233 234// Get user info (automatically uses valid tokens) 235const userInfo = await sessionClient.getUserInfo(); 236console.log("Authenticated as:", userInfo.sub); 237 238// Manual refresh if needed 239const refreshedTokens = await sessionClient.refreshAccessToken(); 240``` 241 242### Storage Backends 243 244The library provides pluggable storage backends. All storage backends store tokens by `sessionId`: 245 246```typescript 247// SQLite storage (persistent) 248import { SQLiteOAuthStorage } from "@slices/oauth"; 249const sqliteStorage = new SQLiteOAuthStorage("oauth.db"); 250 251// Deno KV storage (persistent) 252import { DenoKVOAuthStorage } from "@slices/oauth"; 253const kvStorage = new DenoKVOAuthStorage(await Deno.openKv()); 254 255// Use with OAuthClient (requires sessionId) 256const client = new OAuthClient(config, storage, sessionId); 257``` 258 259**Note:** Tokens are stored with `sessionId` as the key, enabling multiple independent sessions per user.