@slices/oauth#
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#
deno add jsr:@slices/oauth
import { OAuthClient, SQLiteOAuthStorage } from "@slices/oauth";
For node based package managers:
# 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.
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)
// 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.
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#
- Device requests authorization: The device (CLI app) requests a device code and user code
- User authorizes: The user visits the verification URL and enters the code
- Device polls for tokens: The device polls the authorization server until the user completes authorization
- Tokens received: Once authorized, the device receives access and refresh tokens
Example: CLI Application with Device Flow#
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<AuthTokens> {
// 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<void> {
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:
// 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:
// 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.