forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
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.