@silkweave/auth 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +17 -18
- package/.turbo/turbo-check.log +12 -13
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-prepack.log +8 -8
- package/README.md +2 -2
- package/build/index.d.ts +41 -42
- package/build/index.js +253 -218
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/errors.ts +0 -4
- package/src/provider/index.ts +0 -1
- package/src/provider/proxy.ts +309 -253
- package/src/provider/types.ts +7 -1
- package/src/types.ts +2 -8
package/build/index.js
CHANGED
|
@@ -6,6 +6,8 @@ var AuthError = class extends Error {
|
|
|
6
6
|
this.statusCode = statusCode;
|
|
7
7
|
this.name = "AuthError";
|
|
8
8
|
}
|
|
9
|
+
code;
|
|
10
|
+
statusCode;
|
|
9
11
|
};
|
|
10
12
|
function invalidToken(message = "Invalid token") {
|
|
11
13
|
return new AuthError(message, "invalid_token", 401);
|
|
@@ -13,9 +15,6 @@ function invalidToken(message = "Invalid token") {
|
|
|
13
15
|
function insufficientScope(message = "Insufficient scope") {
|
|
14
16
|
return new AuthError(message, "insufficient_scope", 403);
|
|
15
17
|
}
|
|
16
|
-
function missingToken(message = "No authorization provided") {
|
|
17
|
-
return new AuthError(message, "missing_token", 401);
|
|
18
|
-
}
|
|
19
18
|
|
|
20
19
|
// src/extract.ts
|
|
21
20
|
function extractBearerToken(header) {
|
|
@@ -48,6 +47,10 @@ function generateProtectedResourceMetadata(resourceUrl, authorizationServers) {
|
|
|
48
47
|
};
|
|
49
48
|
}
|
|
50
49
|
|
|
50
|
+
// src/provider/proxy.ts
|
|
51
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
52
|
+
import { SignJWT, jwtVerify } from "jose";
|
|
53
|
+
|
|
51
54
|
// src/store/memory-store.ts
|
|
52
55
|
function withTtl(map, key) {
|
|
53
56
|
const item = map.get(key);
|
|
@@ -120,10 +123,6 @@ function createMemoryStore() {
|
|
|
120
123
|
};
|
|
121
124
|
}
|
|
122
125
|
|
|
123
|
-
// src/provider/proxy.ts
|
|
124
|
-
import { randomBytes, randomUUID } from "crypto";
|
|
125
|
-
import { SignJWT, jwtVerify } from "jose";
|
|
126
|
-
|
|
127
126
|
// src/provider/uri.ts
|
|
128
127
|
function matchRedirectUri(uri, patterns) {
|
|
129
128
|
return patterns.some((pattern) => {
|
|
@@ -142,6 +141,233 @@ var CORS_HEADERS = {
|
|
|
142
141
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
143
142
|
"Access-Control-Allow-Headers": "*"
|
|
144
143
|
};
|
|
144
|
+
function generateCode() {
|
|
145
|
+
return randomBytes(32).toString("base64url");
|
|
146
|
+
}
|
|
147
|
+
async function generatePkce() {
|
|
148
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
149
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
150
|
+
const challenge = Buffer.from(hash).toString("base64url");
|
|
151
|
+
return { verifier, challenge };
|
|
152
|
+
}
|
|
153
|
+
async function verifyPkce(verifier, challenge) {
|
|
154
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
155
|
+
const computed = Buffer.from(hash).toString("base64url");
|
|
156
|
+
return computed === challenge;
|
|
157
|
+
}
|
|
158
|
+
function jsonResponse(status, body, headers = {}) {
|
|
159
|
+
return {
|
|
160
|
+
status,
|
|
161
|
+
headers: { "Content-Type": "application/json", ...CORS_HEADERS, ...headers },
|
|
162
|
+
body
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function redirectResponse(url) {
|
|
166
|
+
return {
|
|
167
|
+
status: 302,
|
|
168
|
+
headers: { Location: url, ...CORS_HEADERS },
|
|
169
|
+
body: void 0
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function errorResponse(status, error, description) {
|
|
173
|
+
return jsonResponse(status, { error, error_description: description });
|
|
174
|
+
}
|
|
175
|
+
async function signAccessToken(key, opts) {
|
|
176
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
177
|
+
return new SignJWT({ scope: opts.scopes.join(" "), email: opts.email }).setProtectedHeader({ alg: "HS256" }).setSubject(opts.sub ?? opts.email ?? opts.clientId).setIssuer(opts.issuer).setAudience(opts.issuer).setIssuedAt(now).setExpirationTime(now + opts.ttl).sign(key);
|
|
178
|
+
}
|
|
179
|
+
async function handleRegister(req, store, allowedRedirectUris) {
|
|
180
|
+
const body = req.body;
|
|
181
|
+
if (!body) {
|
|
182
|
+
return errorResponse(400, "invalid_request", "Missing request body");
|
|
183
|
+
}
|
|
184
|
+
const redirectUris = body.redirect_uris;
|
|
185
|
+
if (!redirectUris) {
|
|
186
|
+
return errorResponse(400, "invalid_request", "redirect_uris is required");
|
|
187
|
+
}
|
|
188
|
+
let uris;
|
|
189
|
+
try {
|
|
190
|
+
uris = typeof redirectUris === "string" ? JSON.parse(redirectUris) : redirectUris;
|
|
191
|
+
} catch {
|
|
192
|
+
uris = [redirectUris];
|
|
193
|
+
}
|
|
194
|
+
if (!Array.isArray(uris) || uris.length === 0) {
|
|
195
|
+
return errorResponse(400, "invalid_request", "redirect_uris must be a non-empty array");
|
|
196
|
+
}
|
|
197
|
+
for (const uri of uris) {
|
|
198
|
+
if (!matchRedirectUri(uri, allowedRedirectUris)) {
|
|
199
|
+
return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const clientId = randomUUID();
|
|
203
|
+
const clientSecret = randomBytes(32).toString("base64url");
|
|
204
|
+
const registration = {
|
|
205
|
+
clientId,
|
|
206
|
+
clientSecret,
|
|
207
|
+
redirectUris: uris,
|
|
208
|
+
clientName: body.client_name,
|
|
209
|
+
createdAt: Date.now() / 1e3
|
|
210
|
+
};
|
|
211
|
+
await store.saveClient(clientId, registration);
|
|
212
|
+
return jsonResponse(201, {
|
|
213
|
+
client_id: clientId,
|
|
214
|
+
client_secret: clientSecret,
|
|
215
|
+
redirect_uris: uris,
|
|
216
|
+
client_name: body.client_name,
|
|
217
|
+
token_endpoint_auth_method: "none"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
async function resolveClient(clientId, store, allowedRedirectUris) {
|
|
221
|
+
const existing = await store.getClient(clientId);
|
|
222
|
+
if (existing) {
|
|
223
|
+
return existing;
|
|
224
|
+
}
|
|
225
|
+
if (!clientId.startsWith("https://")) {
|
|
226
|
+
return errorResponse(400, "invalid_client", "Unknown client_id");
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const metaRes = await fetch(clientId);
|
|
230
|
+
if (!metaRes.ok) {
|
|
231
|
+
return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
|
|
232
|
+
}
|
|
233
|
+
const meta = await metaRes.json();
|
|
234
|
+
const metaRedirectUris = meta.redirect_uris;
|
|
235
|
+
if (!Array.isArray(metaRedirectUris) || metaRedirectUris.length === 0) {
|
|
236
|
+
return errorResponse(400, "invalid_client", "Client metadata must include redirect_uris");
|
|
237
|
+
}
|
|
238
|
+
for (const uri of metaRedirectUris) {
|
|
239
|
+
if (!matchRedirectUri(uri, allowedRedirectUris)) {
|
|
240
|
+
return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const client = {
|
|
244
|
+
clientId,
|
|
245
|
+
clientSecret: "",
|
|
246
|
+
redirectUris: metaRedirectUris,
|
|
247
|
+
clientName: meta.client_name,
|
|
248
|
+
createdAt: Date.now() / 1e3
|
|
249
|
+
};
|
|
250
|
+
await store.saveClient(clientId, client);
|
|
251
|
+
return client;
|
|
252
|
+
} catch {
|
|
253
|
+
return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function exchangeUpstreamCode(code, config, callbackPath, pkceVerifier) {
|
|
257
|
+
const tokenBody = new URLSearchParams({
|
|
258
|
+
grant_type: "authorization_code",
|
|
259
|
+
code,
|
|
260
|
+
redirect_uri: `${config.resourceUrl}${callbackPath}`,
|
|
261
|
+
client_id: config.clientId,
|
|
262
|
+
client_secret: config.clientSecret,
|
|
263
|
+
code_verifier: pkceVerifier
|
|
264
|
+
});
|
|
265
|
+
const tokenResponse = await fetch(config.tokenUrl, {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
268
|
+
body: tokenBody.toString()
|
|
269
|
+
});
|
|
270
|
+
if (!tokenResponse.ok) {
|
|
271
|
+
const errorBody = await tokenResponse.text();
|
|
272
|
+
console.error("Upstream token exchange failed:", errorBody);
|
|
273
|
+
return errorResponse(502, "upstream_error", "Failed to exchange code with upstream provider");
|
|
274
|
+
}
|
|
275
|
+
const tokens = await tokenResponse.json();
|
|
276
|
+
return {
|
|
277
|
+
accessToken: tokens.access_token,
|
|
278
|
+
idToken: tokens.id_token
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
async function fetchUserinfo(userinfoUrl, accessToken) {
|
|
282
|
+
if (!userinfoUrl || !accessToken) {
|
|
283
|
+
return {};
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const res = await fetch(userinfoUrl, {
|
|
287
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
288
|
+
});
|
|
289
|
+
if (res.ok) {
|
|
290
|
+
const userinfo = await res.json();
|
|
291
|
+
return { email: userinfo.email, sub: userinfo.sub };
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
return {};
|
|
296
|
+
}
|
|
297
|
+
async function handleRefreshToken(body, store, getSigningKey, config) {
|
|
298
|
+
const refreshToken = body.refresh_token;
|
|
299
|
+
if (!refreshToken) {
|
|
300
|
+
return errorResponse(400, "invalid_request", "Missing refresh_token");
|
|
301
|
+
}
|
|
302
|
+
const tokenData = await store.getRefreshToken(refreshToken);
|
|
303
|
+
if (!tokenData) {
|
|
304
|
+
return errorResponse(400, "invalid_grant", "Invalid or expired refresh token");
|
|
305
|
+
}
|
|
306
|
+
const key = await getSigningKey();
|
|
307
|
+
const accessToken = await signAccessToken(key, {
|
|
308
|
+
scopes: tokenData.scopes,
|
|
309
|
+
email: tokenData.email,
|
|
310
|
+
sub: tokenData.sub,
|
|
311
|
+
clientId: tokenData.clientId,
|
|
312
|
+
issuer: config.resourceUrl,
|
|
313
|
+
ttl: config.tokenTtl
|
|
314
|
+
});
|
|
315
|
+
return jsonResponse(200, {
|
|
316
|
+
access_token: accessToken,
|
|
317
|
+
token_type: "Bearer",
|
|
318
|
+
expires_in: config.tokenTtl,
|
|
319
|
+
scope: tokenData.scopes.join(" "),
|
|
320
|
+
refresh_token: refreshToken
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
async function handleAuthorizationCode(body, store, getSigningKey, config) {
|
|
324
|
+
const code = body.code;
|
|
325
|
+
const redirectUri = body.redirect_uri;
|
|
326
|
+
const clientId = body.client_id;
|
|
327
|
+
const codeVerifier = body.code_verifier;
|
|
328
|
+
if (!code || !codeVerifier || !clientId) {
|
|
329
|
+
return errorResponse(400, "invalid_request", "Missing required parameters");
|
|
330
|
+
}
|
|
331
|
+
const authCode = await store.getAuthCode(code);
|
|
332
|
+
if (!authCode) {
|
|
333
|
+
return errorResponse(400, "invalid_grant", "Invalid or expired authorization code");
|
|
334
|
+
}
|
|
335
|
+
await store.deleteAuthCode(code);
|
|
336
|
+
if (authCode.clientId !== clientId) {
|
|
337
|
+
return errorResponse(400, "invalid_grant", "client_id mismatch");
|
|
338
|
+
}
|
|
339
|
+
if (redirectUri && authCode.redirectUri !== redirectUri) {
|
|
340
|
+
return errorResponse(400, "invalid_grant", "redirect_uri mismatch");
|
|
341
|
+
}
|
|
342
|
+
const pkceValid = await verifyPkce(codeVerifier, authCode.codeChallenge);
|
|
343
|
+
if (!pkceValid) {
|
|
344
|
+
return errorResponse(400, "invalid_grant", "PKCE verification failed");
|
|
345
|
+
}
|
|
346
|
+
const key = await getSigningKey();
|
|
347
|
+
const accessToken = await signAccessToken(key, {
|
|
348
|
+
scopes: authCode.scopes,
|
|
349
|
+
email: authCode.email,
|
|
350
|
+
sub: authCode.sub,
|
|
351
|
+
clientId: authCode.clientId,
|
|
352
|
+
issuer: config.resourceUrl,
|
|
353
|
+
ttl: config.tokenTtl
|
|
354
|
+
});
|
|
355
|
+
const refreshToken = randomBytes(32).toString("base64url");
|
|
356
|
+
await store.saveRefreshToken(refreshToken, {
|
|
357
|
+
clientId: authCode.clientId,
|
|
358
|
+
scopes: authCode.scopes,
|
|
359
|
+
email: authCode.email,
|
|
360
|
+
sub: authCode.sub,
|
|
361
|
+
expiresAt: Math.floor(Date.now() / 1e3) + 30 * 24 * 3600
|
|
362
|
+
});
|
|
363
|
+
return jsonResponse(200, {
|
|
364
|
+
access_token: accessToken,
|
|
365
|
+
token_type: "Bearer",
|
|
366
|
+
expires_in: config.tokenTtl,
|
|
367
|
+
scope: authCode.scopes.join(" "),
|
|
368
|
+
refresh_token: refreshToken
|
|
369
|
+
});
|
|
370
|
+
}
|
|
145
371
|
function createOAuthProxy(config) {
|
|
146
372
|
const store = config.store ?? createMemoryStore();
|
|
147
373
|
const callbackPath = config.callbackPath ?? "/auth/callback";
|
|
@@ -159,38 +385,7 @@ function createOAuthProxy(config) {
|
|
|
159
385
|
}
|
|
160
386
|
return signingKey;
|
|
161
387
|
}
|
|
162
|
-
|
|
163
|
-
return randomBytes(32).toString("base64url");
|
|
164
|
-
}
|
|
165
|
-
async function generatePkce() {
|
|
166
|
-
const verifier = randomBytes(32).toString("base64url");
|
|
167
|
-
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
168
|
-
const challenge = Buffer.from(hash).toString("base64url");
|
|
169
|
-
return { verifier, challenge };
|
|
170
|
-
}
|
|
171
|
-
async function verifyPkce(verifier, challenge) {
|
|
172
|
-
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
173
|
-
const computed = Buffer.from(hash).toString("base64url");
|
|
174
|
-
return computed === challenge;
|
|
175
|
-
}
|
|
176
|
-
function jsonResponse(status, body, headers = {}) {
|
|
177
|
-
return {
|
|
178
|
-
status,
|
|
179
|
-
headers: { "Content-Type": "application/json", ...CORS_HEADERS, ...headers },
|
|
180
|
-
body
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
function redirectResponse(url) {
|
|
184
|
-
return {
|
|
185
|
-
status: 302,
|
|
186
|
-
headers: { Location: url, ...CORS_HEADERS },
|
|
187
|
-
body: void 0
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
function errorResponse(status, error, description) {
|
|
191
|
-
return jsonResponse(status, { error, error_description: description });
|
|
192
|
-
}
|
|
193
|
-
const provider = {
|
|
388
|
+
return {
|
|
194
389
|
metadata() {
|
|
195
390
|
return jsonResponse(200, {
|
|
196
391
|
issuer: config.resourceUrl,
|
|
@@ -204,46 +399,8 @@ function createOAuthProxy(config) {
|
|
|
204
399
|
scopes_supported: scopes
|
|
205
400
|
}, { "Cache-Control": "max-age=3600" });
|
|
206
401
|
},
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (!body) {
|
|
210
|
-
return errorResponse(400, "invalid_request", "Missing request body");
|
|
211
|
-
}
|
|
212
|
-
const redirectUris = body.redirect_uris;
|
|
213
|
-
if (!redirectUris) {
|
|
214
|
-
return errorResponse(400, "invalid_request", "redirect_uris is required");
|
|
215
|
-
}
|
|
216
|
-
let uris;
|
|
217
|
-
try {
|
|
218
|
-
uris = typeof redirectUris === "string" ? JSON.parse(redirectUris) : redirectUris;
|
|
219
|
-
} catch {
|
|
220
|
-
uris = [redirectUris];
|
|
221
|
-
}
|
|
222
|
-
if (!Array.isArray(uris) || uris.length === 0) {
|
|
223
|
-
return errorResponse(400, "invalid_request", "redirect_uris must be a non-empty array");
|
|
224
|
-
}
|
|
225
|
-
for (const uri of uris) {
|
|
226
|
-
if (!matchRedirectUri(uri, config.redirectUris)) {
|
|
227
|
-
return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
const clientId = randomUUID();
|
|
231
|
-
const clientSecret = randomBytes(32).toString("base64url");
|
|
232
|
-
const registration = {
|
|
233
|
-
clientId,
|
|
234
|
-
clientSecret,
|
|
235
|
-
redirectUris: uris,
|
|
236
|
-
clientName: body.client_name,
|
|
237
|
-
createdAt: Date.now() / 1e3
|
|
238
|
-
};
|
|
239
|
-
await store.saveClient(clientId, registration);
|
|
240
|
-
return jsonResponse(201, {
|
|
241
|
-
client_id: clientId,
|
|
242
|
-
client_secret: clientSecret,
|
|
243
|
-
redirect_uris: uris,
|
|
244
|
-
client_name: body.client_name,
|
|
245
|
-
token_endpoint_auth_method: "none"
|
|
246
|
-
});
|
|
402
|
+
register(req) {
|
|
403
|
+
return handleRegister(req, store, config.redirectUris);
|
|
247
404
|
},
|
|
248
405
|
async authorize(req) {
|
|
249
406
|
const params = req.url.searchParams;
|
|
@@ -267,38 +424,11 @@ function createOAuthProxy(config) {
|
|
|
267
424
|
if (!codeChallenge || codeChallengeMethod !== "S256") {
|
|
268
425
|
return errorResponse(400, "invalid_request", "PKCE with S256 is required");
|
|
269
426
|
}
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
const metaRes = await fetch(clientId);
|
|
274
|
-
if (!metaRes.ok) {
|
|
275
|
-
return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
|
|
276
|
-
}
|
|
277
|
-
const meta = await metaRes.json();
|
|
278
|
-
const metaRedirectUris = meta.redirect_uris;
|
|
279
|
-
if (!Array.isArray(metaRedirectUris) || metaRedirectUris.length === 0) {
|
|
280
|
-
return errorResponse(400, "invalid_client", "Client metadata must include redirect_uris");
|
|
281
|
-
}
|
|
282
|
-
for (const uri of metaRedirectUris) {
|
|
283
|
-
if (!matchRedirectUri(uri, config.redirectUris)) {
|
|
284
|
-
return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
client = {
|
|
288
|
-
clientId,
|
|
289
|
-
clientSecret: "",
|
|
290
|
-
redirectUris: metaRedirectUris,
|
|
291
|
-
clientName: meta.client_name,
|
|
292
|
-
createdAt: Date.now() / 1e3
|
|
293
|
-
};
|
|
294
|
-
await store.saveClient(clientId, client);
|
|
295
|
-
} catch {
|
|
296
|
-
return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
if (!client) {
|
|
300
|
-
return errorResponse(400, "invalid_client", "Unknown client_id");
|
|
427
|
+
const result = await resolveClient(clientId, store, config.redirectUris);
|
|
428
|
+
if ("status" in result) {
|
|
429
|
+
return result;
|
|
301
430
|
}
|
|
431
|
+
const client = result;
|
|
302
432
|
if (!client.redirectUris.includes(redirectUri)) {
|
|
303
433
|
return errorResponse(400, "invalid_redirect_uri", "redirect_uri does not match registered URIs");
|
|
304
434
|
}
|
|
@@ -350,42 +480,11 @@ function createOAuthProxy(config) {
|
|
|
350
480
|
}
|
|
351
481
|
await store.deletePendingAuth(proxyState);
|
|
352
482
|
await store.deletePkceVerifier(proxyState);
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
redirect_uri: `${config.resourceUrl}${callbackPath}`,
|
|
357
|
-
client_id: config.clientId,
|
|
358
|
-
client_secret: config.clientSecret,
|
|
359
|
-
code_verifier: pkceVerifier
|
|
360
|
-
});
|
|
361
|
-
const tokenResponse = await fetch(config.tokenUrl, {
|
|
362
|
-
method: "POST",
|
|
363
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
364
|
-
body: tokenBody.toString()
|
|
365
|
-
});
|
|
366
|
-
if (!tokenResponse.ok) {
|
|
367
|
-
const errorBody = await tokenResponse.text();
|
|
368
|
-
console.error("Upstream token exchange failed:", errorBody);
|
|
369
|
-
return errorResponse(502, "upstream_error", "Failed to exchange code with upstream provider");
|
|
370
|
-
}
|
|
371
|
-
const tokens = await tokenResponse.json();
|
|
372
|
-
const upstreamAccessToken = tokens.access_token;
|
|
373
|
-
const upstreamIdToken = tokens.id_token;
|
|
374
|
-
let email;
|
|
375
|
-
let sub;
|
|
376
|
-
if (config.userinfoUrl && upstreamAccessToken) {
|
|
377
|
-
try {
|
|
378
|
-
const userinfoResponse = await fetch(config.userinfoUrl, {
|
|
379
|
-
headers: { Authorization: `Bearer ${upstreamAccessToken}` }
|
|
380
|
-
});
|
|
381
|
-
if (userinfoResponse.ok) {
|
|
382
|
-
const userinfo = await userinfoResponse.json();
|
|
383
|
-
email = userinfo.email;
|
|
384
|
-
sub = userinfo.sub;
|
|
385
|
-
}
|
|
386
|
-
} catch {
|
|
387
|
-
}
|
|
483
|
+
const upstream = await exchangeUpstreamCode(upstreamCode, config, callbackPath, pkceVerifier);
|
|
484
|
+
if ("status" in upstream) {
|
|
485
|
+
return upstream;
|
|
388
486
|
}
|
|
487
|
+
const userinfo = await fetchUserinfo(config.userinfoUrl, upstream.accessToken);
|
|
389
488
|
const mcpCode = generateCode();
|
|
390
489
|
await store.saveAuthCode(mcpCode, {
|
|
391
490
|
clientId: pending.clientId,
|
|
@@ -393,10 +492,10 @@ function createOAuthProxy(config) {
|
|
|
393
492
|
codeChallenge: pending.codeChallenge,
|
|
394
493
|
codeChallengeMethod: pending.codeChallengeMethod,
|
|
395
494
|
scopes: pending.scope.split(" ").filter(Boolean),
|
|
396
|
-
upstreamAccessToken,
|
|
397
|
-
upstreamIdToken,
|
|
398
|
-
email,
|
|
399
|
-
sub,
|
|
495
|
+
upstreamAccessToken: upstream.accessToken,
|
|
496
|
+
upstreamIdToken: upstream.idToken,
|
|
497
|
+
email: userinfo.email,
|
|
498
|
+
sub: userinfo.sub,
|
|
400
499
|
expiresAt: Date.now() / 1e3 + 600
|
|
401
500
|
});
|
|
402
501
|
const clientRedirect = new URL(pending.redirectUri);
|
|
@@ -411,76 +510,14 @@ function createOAuthProxy(config) {
|
|
|
411
510
|
if (!body) {
|
|
412
511
|
return errorResponse(400, "invalid_request", "Missing request body");
|
|
413
512
|
}
|
|
414
|
-
const
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
if (!refreshToken2) {
|
|
418
|
-
return errorResponse(400, "invalid_request", "Missing refresh_token");
|
|
419
|
-
}
|
|
420
|
-
const tokenData = await store.getRefreshToken(refreshToken2);
|
|
421
|
-
if (!tokenData) {
|
|
422
|
-
return errorResponse(400, "invalid_grant", "Invalid or expired refresh token");
|
|
423
|
-
}
|
|
424
|
-
const key2 = await getSigningKey();
|
|
425
|
-
const now2 = Math.floor(Date.now() / 1e3);
|
|
426
|
-
const accessToken2 = await new SignJWT({
|
|
427
|
-
scope: tokenData.scopes.join(" "),
|
|
428
|
-
email: tokenData.email
|
|
429
|
-
}).setProtectedHeader({ alg: "HS256" }).setSubject(tokenData.sub ?? tokenData.email ?? tokenData.clientId).setIssuer(config.resourceUrl).setAudience(config.resourceUrl).setIssuedAt(now2).setExpirationTime(now2 + tokenTtl).sign(key2);
|
|
430
|
-
return jsonResponse(200, {
|
|
431
|
-
access_token: accessToken2,
|
|
432
|
-
token_type: "Bearer",
|
|
433
|
-
expires_in: tokenTtl,
|
|
434
|
-
scope: tokenData.scopes.join(" "),
|
|
435
|
-
refresh_token: refreshToken2
|
|
436
|
-
});
|
|
513
|
+
const tokenConfig = { resourceUrl: config.resourceUrl, tokenTtl };
|
|
514
|
+
if (body.grant_type === "refresh_token") {
|
|
515
|
+
return handleRefreshToken(body, store, getSigningKey, tokenConfig);
|
|
437
516
|
}
|
|
438
|
-
if (
|
|
517
|
+
if (body.grant_type !== "authorization_code") {
|
|
439
518
|
return errorResponse(400, "unsupported_grant_type", "Supported grant types: authorization_code, refresh_token");
|
|
440
519
|
}
|
|
441
|
-
|
|
442
|
-
const redirectUri = body.redirect_uri;
|
|
443
|
-
const clientId = body.client_id;
|
|
444
|
-
const codeVerifier = body.code_verifier;
|
|
445
|
-
if (!code || !codeVerifier || !clientId) {
|
|
446
|
-
return errorResponse(400, "invalid_request", "Missing required parameters");
|
|
447
|
-
}
|
|
448
|
-
const authCode = await store.getAuthCode(code);
|
|
449
|
-
if (!authCode) {
|
|
450
|
-
return errorResponse(400, "invalid_grant", "Invalid or expired authorization code");
|
|
451
|
-
}
|
|
452
|
-
await store.deleteAuthCode(code);
|
|
453
|
-
if (authCode.clientId !== clientId) {
|
|
454
|
-
return errorResponse(400, "invalid_grant", "client_id mismatch");
|
|
455
|
-
}
|
|
456
|
-
if (redirectUri && authCode.redirectUri !== redirectUri) {
|
|
457
|
-
return errorResponse(400, "invalid_grant", "redirect_uri mismatch");
|
|
458
|
-
}
|
|
459
|
-
const pkceValid = await verifyPkce(codeVerifier, authCode.codeChallenge);
|
|
460
|
-
if (!pkceValid) {
|
|
461
|
-
return errorResponse(400, "invalid_grant", "PKCE verification failed");
|
|
462
|
-
}
|
|
463
|
-
const key = await getSigningKey();
|
|
464
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
465
|
-
const accessToken = await new SignJWT({
|
|
466
|
-
scope: authCode.scopes.join(" "),
|
|
467
|
-
email: authCode.email
|
|
468
|
-
}).setProtectedHeader({ alg: "HS256" }).setSubject(authCode.sub ?? authCode.email ?? authCode.clientId).setIssuer(config.resourceUrl).setAudience(config.resourceUrl).setIssuedAt(now).setExpirationTime(now + tokenTtl).sign(key);
|
|
469
|
-
const refreshToken = randomBytes(32).toString("base64url");
|
|
470
|
-
await store.saveRefreshToken(refreshToken, {
|
|
471
|
-
clientId: authCode.clientId,
|
|
472
|
-
scopes: authCode.scopes,
|
|
473
|
-
email: authCode.email,
|
|
474
|
-
sub: authCode.sub,
|
|
475
|
-
expiresAt: now + 30 * 24 * 3600
|
|
476
|
-
});
|
|
477
|
-
return jsonResponse(200, {
|
|
478
|
-
access_token: accessToken,
|
|
479
|
-
token_type: "Bearer",
|
|
480
|
-
expires_in: tokenTtl,
|
|
481
|
-
scope: authCode.scopes.join(" "),
|
|
482
|
-
refresh_token: refreshToken
|
|
483
|
-
});
|
|
520
|
+
return handleAuthorizationCode(body, store, getSigningKey, tokenConfig);
|
|
484
521
|
},
|
|
485
522
|
async verifyToken(token) {
|
|
486
523
|
try {
|
|
@@ -501,7 +538,6 @@ function createOAuthProxy(config) {
|
|
|
501
538
|
}
|
|
502
539
|
}
|
|
503
540
|
};
|
|
504
|
-
return provider;
|
|
505
541
|
}
|
|
506
542
|
|
|
507
543
|
// src/provider/google.ts
|
|
@@ -777,7 +813,6 @@ export {
|
|
|
777
813
|
insufficientScope,
|
|
778
814
|
invalidToken,
|
|
779
815
|
matchRedirectUri,
|
|
780
|
-
missingToken,
|
|
781
816
|
validateToken
|
|
782
817
|
};
|
|
783
818
|
//# sourceMappingURL=index.js.map
|