Framework-agnostic OAuth integration for AT Protocol (Bluesky) applications.
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