Framework-agnostic OAuth integration for AT Protocol (Bluesky) applications.
at main 260 lines 6.5 kB view raw view rendered
1# Web Authentication Guide 2 3This guide covers implementing AT Protocol OAuth for web applications using 4`@tijs/atproto-oauth`. 5 6## Overview 7 8Web authentication uses a standard OAuth 2.0 flow with PKCE: 9 101. User clicks "Login" and enters their Bluesky handle 112. Your server redirects to the user's authorization server 123. User approves access 134. Authorization server redirects back with an authorization code 145. Your server exchanges the code for tokens 156. Session cookie is set for subsequent requests 16 17## Setup 18 19### Installation 20 21```typescript 22import { createATProtoOAuth } from "jsr:@tijs/atproto-oauth"; 23import { SQLiteStorage, valTownAdapter } from "jsr:@tijs/atproto-storage"; 24``` 25 26### Configuration 27 28```typescript 29const oauth = createATProtoOAuth({ 30 baseUrl: "https://myapp.example.com", 31 appName: "My App", 32 cookieSecret: Deno.env.get("COOKIE_SECRET")!, // At least 32 characters 33 storage: new SQLiteStorage(valTownAdapter(sqlite)), 34 sessionTtl: 60 * 60 * 24 * 14, // 14 days (max for public clients) 35 logoUri: "https://myapp.example.com/logo.png", // Optional 36 policyUri: "https://myapp.example.com/privacy", // Optional 37 logger: console, // Optional, for debugging 38}); 39``` 40 41## Route Handlers 42 43Mount these routes in your web framework. Examples shown for Hono: 44 45### Login Route 46 47Starts the OAuth flow. Accepts `handle` as a query parameter. 48 49```typescript 50app.get("/login", (c) => oauth.handleLogin(c.req.raw)); 51``` 52 53**Request:** `GET /login?handle=alice.bsky.social` 54 55The user is redirected to their authorization server (e.g., bsky.social). 56 57### Callback Route 58 59Handles the OAuth callback after user authorization. 60 61```typescript 62app.get("/oauth/callback", (c) => oauth.handleCallback(c.req.raw)); 63``` 64 65On success, sets a session cookie and redirects to `/` (or a custom path). 66 67### Client Metadata Route 68 69Required by AT Protocol OAuth. Serves your app's OAuth client metadata. 70 71```typescript 72app.get("/oauth-client-metadata.json", () => oauth.handleClientMetadata()); 73``` 74 75### Logout Route 76 77Clears the session cookie and OAuth tokens. 78 79```typescript 80app.post("/api/auth/logout", (c) => oauth.handleLogout(c.req.raw)); 81``` 82 83## Protecting Routes 84 85Use `getSessionFromRequest()` to check authentication: 86 87```typescript 88app.get("/api/profile", async (c) => { 89 const { session, setCookieHeader, error } = await oauth.getSessionFromRequest( 90 c.req.raw, 91 ); 92 93 if (!session) { 94 return c.json({ error: error?.message || "Not authenticated" }, 401); 95 } 96 97 // Make authenticated API call to user's PDS 98 const response = await session.makeRequest( 99 "GET", 100 `${session.pdsUrl}/xrpc/app.bsky.actor.getProfile?actor=${session.did}`, 101 ); 102 103 const profile = await response.json(); 104 105 // Important: refresh the session cookie 106 const res = c.json(profile); 107 if (setCookieHeader) { 108 res.headers.set("Set-Cookie", setCookieHeader); 109 } 110 return res; 111}); 112``` 113 114### Session Object 115 116The `session` object provides: 117 118```typescript 119interface SessionInterface { 120 did: string; // User's DID (e.g., "did:plc:abc123") 121 handle?: string; // User's handle (e.g., "alice.bsky.social") 122 pdsUrl: string; // User's PDS URL 123 accessToken: string; // Current access token 124 refreshToken?: string; // Refresh token (if available) 125 126 // Make authenticated requests with automatic DPoP handling 127 makeRequest( 128 method: string, 129 url: string, 130 options?: RequestInit, 131 ): Promise<Response>; 132} 133``` 134 135### Error Handling 136 137When `session` is null, check `error` for details: 138 139```typescript 140error?: { 141 type: "NO_COOKIE" | "INVALID_COOKIE" | "SESSION_EXPIRED" | "OAUTH_ERROR" | "UNKNOWN"; 142 message: string; 143 details?: unknown; 144} 145``` 146 147## Custom Redirect After Login 148 149Pass a `redirect` query parameter to return users to a specific page: 150 151```typescript 152// Start login with redirect 153const loginUrl = `/login?handle=${handle}&redirect=/dashboard`; 154``` 155 156Only relative paths starting with `/` are allowed for security. 157 158## Session Endpoint 159 160You'll typically want a session check endpoint for your frontend: 161 162```typescript 163app.get("/api/auth/session", async (c) => { 164 const { session, setCookieHeader } = await oauth.getSessionFromRequest( 165 c.req.raw, 166 ); 167 168 if (!session) { 169 return c.json({ authenticated: false }); 170 } 171 172 const res = c.json({ 173 authenticated: true, 174 did: session.did, 175 handle: session.handle, 176 }); 177 178 if (setCookieHeader) { 179 res.headers.set("Set-Cookie", setCookieHeader); 180 } 181 return res; 182}); 183``` 184 185## Frontend Integration 186 187### Login Form 188 189```html 190<form action="/login" method="get"> 191 <input type="text" name="handle" placeholder="alice.bsky.social" required /> 192 <button type="submit">Sign in with Bluesky</button> 193</form> 194``` 195 196### Check Authentication (JavaScript) 197 198```javascript 199async function checkAuth() { 200 const response = await fetch("/api/auth/session", { 201 credentials: "include", 202 }); 203 const data = await response.json(); 204 205 if (data.authenticated) { 206 console.log(`Logged in as ${data.handle}`); 207 } else { 208 console.log("Not logged in"); 209 } 210} 211``` 212 213### Logout 214 215```javascript 216async function logout() { 217 await fetch("/api/auth/logout", { 218 method: "POST", 219 credentials: "include", 220 }); 221 window.location.href = "/"; 222} 223``` 224 225## Security Considerations 226 2271. **Cookie Secret**: Use a strong, random secret of at least 32 characters. 228 Store it securely (environment variable, secrets manager). 229 2302. **HTTPS**: Always use HTTPS in production. The session cookie has `Secure` 231 flag set. 232 2333. **Session TTL**: AT Protocol spec limits public client sessions to 14 days 234 maximum. 235 2364. **CORS**: If your API is on a different domain, configure CORS appropriately. 237 Session cookies require `credentials: "include"` on fetch requests. 238 239## Complete Example 240 241See the [Hono example](../README.md#hono-integration) in the main README for a 242complete working setup. 243 244## Resources 245 246### AT Protocol Documentation 247 248- [OAuth Specification](https://atproto.com/specs/oauth) - Full OAuth spec for 249 AT Protocol 250- [OAuth Introduction](https://atproto.com/guides/oauth) - Overview of OAuth 251 patterns and app types 252- [Building Applications Guide](https://atproto.com/guides/applications) - Quick 253 start guide for AT Protocol apps 254 255### Example Implementations 256 257- [Go OAuth Web App](https://github.com/bluesky-social/cookbook/tree/main/go-oauth-web-app) - 258 Official Bluesky web app example in Go 259- [Python OAuth Web App](https://github.com/bluesky-social/cookbook/tree/main/python-oauth-web-app) - 260 Official Bluesky web app example in Python