+23
CHANGELOG.md
+23
CHANGELOG.md
···
2
2
3
3
All notable changes to this project will be documented in this file.
4
4
5
+
## [2.0.0] - 2025-11-29
6
+
7
+
### Breaking Changes
8
+
9
+
- **Removed mobile OAuth support**: The following features have been removed as
10
+
they were unused (the Anchor iOS app uses cookie-based auth via a WebView):
11
+
- `mobileScheme` config option - No longer needed
12
+
- `mobile` query parameter on `/login` - Removed
13
+
- `code_challenge` query parameter on `/login` - Removed
14
+
- Mobile callback with `session_token` - Removed
15
+
- Bearer token authentication in `getSessionFromRequest()` - Removed (now
16
+
cookie-only)
17
+
- **Removed types**: `MobileOAuthStartRequest`, `MobileOAuthStartResponse`
18
+
- **Simplified `OAuthState`**: Removed `mobile` and `codeChallenge` fields
19
+
20
+
The library now focuses solely on cookie-based session management for web
21
+
applications. Mobile apps should use app-specific WebView flows with cookie
22
+
authentication.
23
+
24
+
### Changed
25
+
26
+
- Updated `@tijs/atproto-sessions` dependency to 2.0.0
27
+
5
28
## [1.1.1] - 2025-11-28
6
29
7
30
### Fixed
+2
-2
deno.json
+2
-2
deno.json
···
1
1
{
2
2
"$schema": "https://jsr.io/schema/config-file.v1.json",
3
3
"name": "@tijs/atproto-oauth",
4
-
"version": "1.1.1",
4
+
"version": "2.0.0",
5
5
"license": "MIT",
6
6
"exports": "./mod.ts",
7
7
"publish": {
···
11
11
"imports": {
12
12
"@std/assert": "jsr:@std/assert@1.0.16",
13
13
"@tijs/oauth-client-deno": "jsr:@tijs/oauth-client-deno@4.0.2",
14
-
"@tijs/atproto-sessions": "jsr:@tijs/atproto-sessions@1.0.1",
14
+
"@tijs/atproto-sessions": "jsr:@tijs/atproto-sessions@2.0.0",
15
15
"@tijs/atproto-storage": "jsr:@tijs/atproto-storage@0.1.1",
16
16
"@atproto/syntax": "npm:@atproto/syntax@0.3.0"
17
17
},
-2
mod.ts
-2
mod.ts
-5
src/oauth.ts
-5
src/oauth.ts
···
19
19
/** Default session TTL: 7 days in seconds */
20
20
const DEFAULT_SESSION_TTL = 60 * 60 * 24 * 7;
21
21
22
-
/** Default mobile callback scheme */
23
-
const DEFAULT_MOBILE_SCHEME = "app://auth-callback";
24
-
25
22
/**
26
23
* Create a complete ATProto OAuth integration for any framework.
27
24
*
···
108
105
// Normalize baseUrl
109
106
const baseUrl = config.baseUrl.replace(/\/$/, "");
110
107
const sessionTtl = config.sessionTtl ?? DEFAULT_SESSION_TTL;
111
-
const mobileScheme = config.mobileScheme ?? DEFAULT_MOBILE_SCHEME;
112
108
const logger: Logger = config.logger ?? noopLogger;
113
109
114
110
// Create OAuth client (Logger interfaces now match)
···
143
139
oauthSessions,
144
140
storage: config.storage,
145
141
sessionTtl,
146
-
mobileScheme,
147
142
logger,
148
143
});
149
144
+11
-111
src/routes.ts
+11
-111
src/routes.ts
···
26
26
oauthSessions: OAuthSessions;
27
27
storage: OAuthStorage;
28
28
sessionTtl: number;
29
-
mobileScheme: string;
30
29
logger: Logger;
31
30
}
32
31
···
48
47
oauthSessions,
49
48
storage,
50
49
sessionTtl,
51
-
mobileScheme,
52
50
logger,
53
51
} = config;
54
52
···
57
55
*
58
56
* Query parameters:
59
57
* - handle: User's AT Protocol handle (required)
60
-
* - redirect: Relative path to redirect after web OAuth (optional)
61
-
* - mobile: "true" to enable mobile flow with configured mobileScheme redirect (optional)
62
-
* - code_challenge: PKCE code_challenge from mobile client (optional, for future use)
63
-
*
64
-
* Security: Mobile redirects always use the server-configured mobileScheme.
65
-
* Client-specified redirect schemes are NOT allowed to prevent OAuth redirect attacks.
58
+
* - redirect: Relative path to redirect after OAuth (optional)
66
59
*/
67
60
async function handleLogin(request: Request): Promise<Response> {
68
61
const url = new URL(request.url);
69
62
const handle = url.searchParams.get("handle");
70
63
const redirect = url.searchParams.get("redirect");
71
-
const mobile = url.searchParams.get("mobile") === "true";
72
-
const codeChallenge = url.searchParams.get("code_challenge");
73
64
74
65
if (!handle || typeof handle !== "string") {
75
66
return new Response("Invalid handle", { status: 400 });
···
85
76
timestamp: Date.now(),
86
77
};
87
78
88
-
// Mobile flow configuration
89
-
if (mobile) {
90
-
state.mobile = true;
91
-
logger.info(`Starting mobile OAuth flow for handle: ${handle}`);
92
-
93
-
// Store PKCE code_challenge for mobile (library generates its own, but
94
-
// in the future we could support external challenges for native apps)
95
-
if (codeChallenge) {
96
-
state.codeChallenge = codeChallenge;
97
-
}
98
-
} else {
99
-
// Web flow - store redirect path (validate it's a relative path)
100
-
if (redirect) {
101
-
// Security: Only allow relative paths starting with /
102
-
if (redirect.startsWith("/") && !redirect.startsWith("//")) {
103
-
state.redirectPath = redirect;
104
-
} else {
105
-
logger.warn(`Invalid redirect path ignored: ${redirect}`);
106
-
}
79
+
// Store redirect path (validate it's a relative path)
80
+
if (redirect) {
81
+
// Security: Only allow relative paths starting with /
82
+
if (redirect.startsWith("/") && !redirect.startsWith("//")) {
83
+
state.redirectPath = redirect;
84
+
} else {
85
+
logger.warn(`Invalid redirect path ignored: ${redirect}`);
107
86
}
108
87
}
109
88
···
167
146
lastAccessed: now,
168
147
});
169
148
170
-
// Handle mobile callback
171
-
if (state.mobile) {
172
-
const sealedToken = await sessionManager.sealToken({ did });
173
-
174
-
// Always use server-configured mobileScheme for security
175
-
const mobileCallbackUrl = new URL(mobileScheme);
176
-
mobileCallbackUrl.searchParams.set("session_token", sealedToken);
177
-
mobileCallbackUrl.searchParams.set("did", did);
178
-
mobileCallbackUrl.searchParams.set("handle", state.handle);
179
-
180
-
if (oauthSession.accessToken) {
181
-
mobileCallbackUrl.searchParams.set(
182
-
"access_token",
183
-
oauthSession.accessToken,
184
-
);
185
-
}
186
-
if (oauthSession.refreshToken) {
187
-
mobileCallbackUrl.searchParams.set(
188
-
"refresh_token",
189
-
oauthSession.refreshToken,
190
-
);
191
-
}
192
-
193
-
logger.info(
194
-
`Mobile OAuth callback complete for ${did}, redirecting to ${mobileScheme}`,
195
-
);
196
-
197
-
return new Response(null, {
198
-
status: 302,
199
-
headers: {
200
-
Location: mobileCallbackUrl.toString(),
201
-
"Set-Cookie": setCookieHeader,
202
-
},
203
-
});
204
-
}
205
-
206
-
// Web callback - redirect to stored path or home
149
+
// Redirect to stored path or home
207
150
const redirectPath = state.redirectPath || "/";
208
151
209
152
return new Response(null, {
···
254
197
}
255
198
256
199
/**
257
-
* Get OAuth session from request (cookie or Bearer token)
200
+
* Get OAuth session from request (cookie-based)
258
201
*/
259
202
async function getSessionFromRequest(
260
203
request: Request,
261
204
): Promise<OAuthSessionFromRequestResult> {
262
-
// Check for Bearer token first (mobile)
263
-
const authHeader = request.headers.get("Authorization");
264
-
if (authHeader && authHeader.startsWith("Bearer ")) {
265
-
const tokenResult = await sessionManager.validateBearerToken(authHeader);
266
-
if (tokenResult.data?.did) {
267
-
try {
268
-
const oauthSession = await oauthSessions.getOAuthSession(
269
-
tokenResult.data.did,
270
-
);
271
-
if (oauthSession) {
272
-
return { session: oauthSession };
273
-
}
274
-
return {
275
-
session: null,
276
-
error: {
277
-
type: "SESSION_EXPIRED",
278
-
message: "OAuth session not found in storage",
279
-
},
280
-
};
281
-
} catch (error) {
282
-
return {
283
-
session: null,
284
-
error: {
285
-
type: "OAUTH_ERROR",
286
-
message: error instanceof Error
287
-
? error.message
288
-
: "OAuth session restore failed",
289
-
details: error,
290
-
},
291
-
};
292
-
}
293
-
}
294
-
return {
295
-
session: null,
296
-
error: tokenResult.error
297
-
? {
298
-
type: "INVALID_COOKIE",
299
-
message: tokenResult.error.message,
300
-
}
301
-
: { type: "INVALID_COOKIE", message: "Invalid token" },
302
-
};
303
-
}
304
-
305
-
// Check for session cookie (web)
205
+
// Check for session cookie
306
206
const sessionResult = await sessionManager.getSessionFromRequest(request);
307
207
if (!sessionResult.data?.did) {
308
208
return {
+1
-25
src/types.ts
+1
-25
src/types.ts
···
110
110
/** Display name for OAuth consent screen */
111
111
appName: string;
112
112
113
-
/** Custom URL scheme for mobile app callbacks (default: "app://auth-callback") */
114
-
mobileScheme?: string;
115
-
116
113
/** URL to app logo for OAuth consent screen */
117
114
logoUri?: string;
118
115
···
167
164
valid: boolean;
168
165
did?: string;
169
166
handle?: string;
170
-
}
171
-
172
-
/**
173
-
* Mobile OAuth start request
174
-
*/
175
-
export interface MobileOAuthStartRequest {
176
-
handle: string;
177
-
code_challenge: string;
178
-
}
179
-
180
-
/**
181
-
* Mobile OAuth start response
182
-
*/
183
-
export interface MobileOAuthStartResponse {
184
-
success: boolean;
185
-
authUrl?: string;
186
-
error?: string;
187
167
}
188
168
189
169
/**
···
318
298
export interface OAuthState {
319
299
handle: string;
320
300
timestamp: number;
321
-
/** Whether this is a mobile OAuth flow */
322
-
mobile?: boolean;
323
-
/** PKCE code_challenge for mobile flows (generated by mobile client) */
324
-
codeChallenge?: string;
325
-
/** Redirect path for web flows */
301
+
/** Redirect path after successful web OAuth */
326
302
redirectPath?: string;
327
303
}
328
304