# @slices/oauth [AIP](https://github.com/graze-social/aip) OAuth 2.1 client with PKCE support and Device Authorization Grant (RFC 8628) for TypeScript. ## Features - OAuth 2.1 with PKCE flow - Device Authorization Grant (RFC 8628) support - Token refresh handling - Pluggable storage backends (SQLite, Deno KV, others can be added) - Automatic token management and refresh ## Installation ### Deno ```bash deno add jsr:@slices/oauth ``` ```typescript import { OAuthClient, SQLiteOAuthStorage } from "@slices/oauth"; ``` For node based package managers: ```bash # pnpm (10.9+) pnpm install jsr:@slices/oauth # Yarn (4.9+) yarn add jsr:@slices/oauth ``` ## Usage ### Standard OAuth Flow (Web Applications) OAuth 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. ```typescript import { OAuthClient, SQLiteOAuthStorage } from "@slices/oauth"; // Set up storage const storage = new SQLiteOAuthStorage("oauth.db"); // OAuth configuration const config = { clientId: "your-client-id", clientSecret: "your-client-secret", authBaseUrl: "https://auth.example.com", redirectUri: "http://localhost:8000/oauth/callback", scopes: ["atproto"], }; // Start authorization flow (before we have a session) const tempClient = new OAuthClient(config, storage, "temp"); const result = await tempClient.authorize({ loginHint: "user.bsky.social" }); // Redirect user to result.authorizationUrl // Handle callback const tokens = await tempClient.handleCallback({ code, state }); // Create session and store tokens by sessionId // (typically done via @slices/session - see Session Integration below) const sessionId = "user-session-id"; // from session store const sessionClient = new OAuthClient(config, storage, sessionId); await storage.setTokens(tokens, sessionId); // Use the session-scoped client for API calls const userInfo = await sessionClient.getUserInfo(); ``` ### Session-Scoped Pattern The key concept: **one OAuth client per session**. This enables: - Multiple active sessions per user (different devices) - Independent token management per session - Proper logout isolation (logging out one device doesn't affect others) ```typescript // Each session gets its own OAuth client function createSessionClient(sessionId: string) { return new OAuthClient(config, storage, sessionId); } // Session 1 (laptop) const laptopClient = createSessionClient("session-laptop-123"); // Session 2 (phone) - completely independent tokens const phoneClient = createSessionClient("session-phone-456"); ``` ### Device Authorization Grant (CLI Applications) The Device Authorization Grant flow is perfect for CLI applications, TVs, or other devices without a browser or with limited input capabilities. ```typescript import { DeviceCodeClient } from "@slices/oauth"; // Create device code client const client = new DeviceCodeClient({ clientId: "your-cli-client-id", authBaseUrl: "https://auth.example.com", scopes: ["atproto", "transition:generic", "repo:*"], }); // Start device code flow const deviceAuth = await client.startDeviceAuth(); // Display the user code to the user console.log(`Please visit: ${deviceAuth.verification_uri_complete}`); console.log( `Or go to ${deviceAuth.verification_uri} and enter code: ${deviceAuth.user_code}`, ); // Poll for tokens (handles the polling interval automatically) const tokens = await client.pollForTokens(deviceAuth); // Store the tokens securely console.log("Authentication successful!"); console.log("Access token:", tokens.access_token); console.log("DID:", tokens.sub); ``` #### Device Code Flow Features - **Automatic polling**: The client handles the polling interval specified by the server - **User-friendly codes**: Short, easy-to-type codes for manual entry - **Browser integration**: Can automatically open the browser to the verification URL - **Timeout handling**: Automatically handles device code expiration - **Error recovery**: Handles slow_down responses and other polling errors #### Typical Flow 1. **Device requests authorization**: The device (CLI app) requests a device code and user code 2. **User authorizes**: The user visits the verification URL and enters the code 3. **Device polls for tokens**: The device polls the authorization server until the user completes authorization 4. **Tokens received**: Once authorized, the device receives access and refresh tokens #### Example: CLI Application with Device Flow ```typescript import { DeviceCodeClient } from "@slices/oauth"; export class CLIAuthManager { private client: DeviceCodeClient; constructor(clientId: string, authBaseUrl: string) { this.client = new DeviceCodeClient({ clientId, authBaseUrl, scopes: [ "atproto", "transition:generic", "repo:network.slices.slice", "repo:network.slices.lexicon", ], }); } async login(): Promise { // Start device authorization const deviceAuth = await this.client.startDeviceAuth(); // Show user instructions console.log("🔐 Authenticate with Slices"); console.log(""); console.log(`Please visit: ${deviceAuth.verification_uri_complete}`); console.log(""); console.log("Or manually:"); console.log(`1. Go to: ${deviceAuth.verification_uri}`); console.log(`2. Enter code: ${deviceAuth.user_code}`); console.log(""); console.log("Waiting for authorization..."); // Optionally open browser automatically if (Deno.build.os !== "linux") { await this.openBrowser(deviceAuth.verification_uri_complete); } // Poll for tokens try { const tokens = await this.client.pollForTokens(deviceAuth); console.log("✅ Authentication successful!"); // Get user info to find out who authenticated const userInfo = await this.getUserInfo(tokens); console.log("Authenticated as:", userInfo?.sub); return tokens; } catch (error) { if (error.message.includes("expired")) { throw new Error("Device code expired. Please try again."); } throw error; } } private async openBrowser(url: string): Promise { const cmd = Deno.build.os === "darwin" ? "open" : Deno.build.os === "windows" ? "start" : "xdg-open"; try { await new Deno.Command(cmd, { args: [url] }).output(); } catch { // Ignore errors - user can manually open the URL } } } ``` ### Token Management Both OAuth flows support automatic token refresh. The client handles token refresh automatically: ```typescript // The client ensures tokens are valid before making requests const sessionClient = new OAuthClient(config, storage, sessionId); // Automatically refreshes if needed const tokens = await sessionClient.ensureValidToken(); // Get user info (automatically uses valid tokens) const userInfo = await sessionClient.getUserInfo(); console.log("Authenticated as:", userInfo.sub); // Manual refresh if needed const refreshedTokens = await sessionClient.refreshAccessToken(); ``` ### Storage Backends The library provides pluggable storage backends. All storage backends store tokens by `sessionId`: ```typescript // SQLite storage (persistent) import { SQLiteOAuthStorage } from "@slices/oauth"; const sqliteStorage = new SQLiteOAuthStorage("oauth.db"); // Deno KV storage (persistent) import { DenoKVOAuthStorage } from "@slices/oauth"; const kvStorage = new DenoKVOAuthStorage(await Deno.openKv()); // Use with OAuthClient (requires sessionId) const client = new OAuthClient(config, storage, sessionId); ``` **Note:** Tokens are stored with `sessionId` as the key, enabling multiple independent sessions per user.