@better-auth/oauth-provider 1.7.0-beta.0 → 1.7.0-beta.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/dist/{client-assertion-DZqo-L5j.mjs → client-assertion-CderPEmR.mjs} +12 -3
- package/dist/client-resource.d.mts +1 -1
- package/dist/client-resource.mjs +2 -2
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/index.d.mts +50 -6
- package/dist/index.mjs +467 -237
- package/dist/{oauth-C8aTlaAC.d.mts → oauth-CU79t-eG.d.mts} +102 -5
- package/dist/{oauth-Dh4YXCXY.d.mts → oauth-DJcZ8MMZ.d.mts} +152 -23
- package/dist/{utils-CIbcUsZ5.mjs → utils-Cx_XnD9i.mjs} +35 -3
- package/dist/{version-BGWhjYBb.mjs → version-CZxZ64qJ.mjs} +1 -1
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { n as isPrivateHostname } from "./client-assertion-
|
|
1
|
+
import { n as isPrivateHostname } from "./client-assertion-CderPEmR.mjs";
|
|
2
2
|
import { n as mcpHandler } from "./mcp-CYnz-MXn.mjs";
|
|
3
|
-
import { _ as
|
|
4
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
3
|
+
import { _ as storeClientSecret, a as getClient, b as validateClientCredentials, c as getStoredToken, d as normalizeTimestampValue, f as parseClientMetadata, g as searchParamsToQuery, h as resolveSubjectIdentifier, i as extractClientCredentials, l as isPKCERequired, m as resolveSessionAuthTime, n as deleteFromPrompt, o as getJwtPlugin, p as parsePrompt, r as destructureCredentials, t as decryptStoredClientSecret, u as mergeDiscoveryMetadata, v as storeToken, x as verifyOAuthQueryParams, y as toClientDiscoveryArray } from "./utils-Cx_XnD9i.mjs";
|
|
4
|
+
import { t as PACKAGE_VERSION } from "./version-CZxZ64qJ.mjs";
|
|
5
5
|
import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
6
6
|
import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
|
|
7
7
|
import { APIError as APIError$1 } from "better-call";
|
|
8
8
|
import { ASSERTION_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
|
|
9
9
|
import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
|
|
10
|
+
import { isLoopbackHost, isLoopbackIP } from "@better-auth/core/utils/host";
|
|
10
11
|
import { generateRandomString, makeSignature } from "better-auth/crypto";
|
|
11
12
|
import { defineRequestState } from "@better-auth/core/context";
|
|
12
13
|
import { logger } from "@better-auth/core/env";
|
|
@@ -14,8 +15,8 @@ import { BetterAuthError } from "@better-auth/core/error";
|
|
|
14
15
|
import { parseSetCookieHeader } from "better-auth/cookies";
|
|
15
16
|
import { mergeSchema } from "better-auth/db";
|
|
16
17
|
import * as z from "zod";
|
|
17
|
-
import { signJWT, toExpJWT } from "better-auth/plugins";
|
|
18
|
-
import { SignJWT, compactVerify, createLocalJWKSet, decodeJwt } from "jose";
|
|
18
|
+
import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
|
|
19
|
+
import { SignJWT, base64url, compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader } from "jose";
|
|
19
20
|
//#region src/consent.ts
|
|
20
21
|
async function consentEndpoint(ctx, opts) {
|
|
21
22
|
const _query = (await oAuthState.get())?.query;
|
|
@@ -156,6 +157,76 @@ async function postLogin(ctx, opts) {
|
|
|
156
157
|
};
|
|
157
158
|
}
|
|
158
159
|
//#endregion
|
|
160
|
+
//#region src/types/zod.ts
|
|
161
|
+
const DANGEROUS_SCHEMES = [
|
|
162
|
+
"javascript:",
|
|
163
|
+
"data:",
|
|
164
|
+
"vbscript:"
|
|
165
|
+
];
|
|
166
|
+
/**
|
|
167
|
+
* Runtime schema for OAuthAuthorizationQuery.
|
|
168
|
+
* Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
|
|
169
|
+
*/
|
|
170
|
+
const oauthAuthorizationQuerySchema = z.object({
|
|
171
|
+
response_type: z.literal("code").optional(),
|
|
172
|
+
request_uri: z.string().optional(),
|
|
173
|
+
redirect_uri: z.string(),
|
|
174
|
+
scope: z.string().optional(),
|
|
175
|
+
state: z.string().optional(),
|
|
176
|
+
client_id: z.string(),
|
|
177
|
+
prompt: z.string().optional(),
|
|
178
|
+
display: z.string().optional(),
|
|
179
|
+
ui_locales: z.string().optional(),
|
|
180
|
+
max_age: z.coerce.number().optional(),
|
|
181
|
+
acr_values: z.string().optional(),
|
|
182
|
+
login_hint: z.string().optional(),
|
|
183
|
+
id_token_hint: z.string().optional(),
|
|
184
|
+
code_challenge: z.string().optional(),
|
|
185
|
+
code_challenge_method: z.literal("S256").optional(),
|
|
186
|
+
nonce: z.string().optional()
|
|
187
|
+
}).passthrough();
|
|
188
|
+
/**
|
|
189
|
+
* Runtime schema for the authorization code verification value.
|
|
190
|
+
* Validates structure on deserialization from the JSON blob stored in the DB.
|
|
191
|
+
* Uses passthrough so future fields (e.g. from authorization challenge) don't break parsing.
|
|
192
|
+
*/
|
|
193
|
+
const verificationValueSchema = z.object({
|
|
194
|
+
type: z.literal("authorization_code"),
|
|
195
|
+
query: oauthAuthorizationQuerySchema,
|
|
196
|
+
sessionId: z.string(),
|
|
197
|
+
userId: z.string(),
|
|
198
|
+
referenceId: z.string().optional(),
|
|
199
|
+
authTime: z.number().optional()
|
|
200
|
+
}).passthrough();
|
|
201
|
+
/**
|
|
202
|
+
* Reusable URL validation for OAuth redirect URIs.
|
|
203
|
+
* - Blocks dangerous schemes (javascript:, data:, vbscript:)
|
|
204
|
+
* - For http/https: requires HTTPS (HTTP allowed only for loopback hosts: 127.0.0.0/8, [::1], *.localhost per RFC 6761)
|
|
205
|
+
* - Allows custom schemes for mobile apps (e.g., myapp://callback)
|
|
206
|
+
*/
|
|
207
|
+
const SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
|
208
|
+
if (!URL.canParse(val)) {
|
|
209
|
+
ctx.addIssue({
|
|
210
|
+
code: "custom",
|
|
211
|
+
message: "URL must be parseable",
|
|
212
|
+
fatal: true
|
|
213
|
+
});
|
|
214
|
+
return z.NEVER;
|
|
215
|
+
}
|
|
216
|
+
const u = new URL(val);
|
|
217
|
+
if (DANGEROUS_SCHEMES.includes(u.protocol)) {
|
|
218
|
+
ctx.addIssue({
|
|
219
|
+
code: "custom",
|
|
220
|
+
message: "URL cannot use javascript:, data:, or vbscript: scheme"
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (u.protocol === "http:" && !isLoopbackHost(u.host)) ctx.addIssue({
|
|
225
|
+
code: "custom",
|
|
226
|
+
message: "Redirect URI must use HTTPS (HTTP allowed only for loopback hosts)"
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
//#endregion
|
|
159
230
|
//#region src/userinfo.ts
|
|
160
231
|
/**
|
|
161
232
|
* Provides shared /userinfo and id_token claims functionality
|
|
@@ -184,11 +255,7 @@ function userNormalClaims(user, scopes) {
|
|
|
184
255
|
* Handles the /oauth2/userinfo endpoint
|
|
185
256
|
*/
|
|
186
257
|
async function userInfoEndpoint(ctx, opts) {
|
|
187
|
-
|
|
188
|
-
error_description: "request not found",
|
|
189
|
-
error: "invalid_request"
|
|
190
|
-
});
|
|
191
|
-
const authorization = ctx.request.headers.get("authorization");
|
|
258
|
+
const authorization = ctx.headers?.get("authorization");
|
|
192
259
|
const token = typeof authorization === "string" && authorization?.startsWith("Bearer ") ? authorization?.replace("Bearer ", "") : authorization;
|
|
193
260
|
if (!token?.length) throw new APIError("UNAUTHORIZED", {
|
|
194
261
|
error_description: "authorization header not found",
|
|
@@ -234,8 +301,8 @@ async function userInfoEndpoint(ctx, opts) {
|
|
|
234
301
|
* the grant types
|
|
235
302
|
*/
|
|
236
303
|
async function tokenEndpoint(ctx, opts) {
|
|
237
|
-
const grantType = ctx.body
|
|
238
|
-
if (opts.grantTypes &&
|
|
304
|
+
const grantType = ctx.body.grant_type;
|
|
305
|
+
if (opts.grantTypes && !opts.grantTypes.includes(grantType)) throw new APIError("BAD_REQUEST", {
|
|
239
306
|
error_description: `unsupported grant_type ${grantType}`,
|
|
240
307
|
error: "unsupported_grant_type"
|
|
241
308
|
});
|
|
@@ -243,14 +310,6 @@ async function tokenEndpoint(ctx, opts) {
|
|
|
243
310
|
case "authorization_code": return handleAuthorizationCodeGrant(ctx, opts);
|
|
244
311
|
case "client_credentials": return handleClientCredentialsGrant(ctx, opts);
|
|
245
312
|
case "refresh_token": return handleRefreshTokenGrant(ctx, opts);
|
|
246
|
-
case void 0: throw new APIError("BAD_REQUEST", {
|
|
247
|
-
error_description: "missing required grant_type",
|
|
248
|
-
error: "unsupported_grant_type"
|
|
249
|
-
});
|
|
250
|
-
default: throw new APIError("BAD_REQUEST", {
|
|
251
|
-
error_description: `unsupported grant_type ${grantType}`,
|
|
252
|
-
error: "unsupported_grant_type"
|
|
253
|
-
});
|
|
254
313
|
}
|
|
255
314
|
}
|
|
256
315
|
async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, overrides) {
|
|
@@ -268,7 +327,7 @@ async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, r
|
|
|
268
327
|
options: jwtPluginOptions,
|
|
269
328
|
payload: {
|
|
270
329
|
...customClaims,
|
|
271
|
-
sub: user
|
|
330
|
+
sub: user?.id,
|
|
272
331
|
aud: typeof audience === "string" ? audience : audience?.length === 1 ? audience.at(0) : audience,
|
|
273
332
|
azp: client.clientId,
|
|
274
333
|
scope: scopes.join(" "),
|
|
@@ -280,10 +339,23 @@ async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, r
|
|
|
280
339
|
});
|
|
281
340
|
}
|
|
282
341
|
/**
|
|
342
|
+
* Computes an OIDC hash (at_hash, c_hash) per OIDC Core §3.1.3.6.
|
|
343
|
+
* Hashes the token, takes the left half, and base64url-encodes it.
|
|
344
|
+
*/
|
|
345
|
+
async function computeOidcHash(token, signingAlg) {
|
|
346
|
+
let hashAlg;
|
|
347
|
+
if (signingAlg === "EdDSA") hashAlg = "SHA-512";
|
|
348
|
+
else if (signingAlg.endsWith("384")) hashAlg = "SHA-384";
|
|
349
|
+
else if (signingAlg.endsWith("512")) hashAlg = "SHA-512";
|
|
350
|
+
else hashAlg = "SHA-256";
|
|
351
|
+
const digest = new Uint8Array(await crypto.subtle.digest(hashAlg, new TextEncoder().encode(token)));
|
|
352
|
+
return base64url.encode(digest.slice(0, digest.length / 2));
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
283
355
|
* Creates a user id token in code_authorization with scope of 'openid'
|
|
284
356
|
* and hybrid/implicit (not yet implemented) flows
|
|
285
357
|
*/
|
|
286
|
-
async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime) {
|
|
358
|
+
async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) {
|
|
287
359
|
const iat = Math.floor(Date.now() / 1e3);
|
|
288
360
|
const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
|
|
289
361
|
const userClaims = userNormalClaims(user, scopes);
|
|
@@ -296,11 +368,15 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
|
|
|
296
368
|
metadata: parseClientMetadata(client.metadata)
|
|
297
369
|
}) : {};
|
|
298
370
|
const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
|
|
371
|
+
const resolvedKey = !opts.disableJwtPlugin && !jwtPluginOptions?.jwt?.sign ? await resolveSigningKey(ctx, jwtPluginOptions) : void 0;
|
|
372
|
+
const signingAlg = opts.disableJwtPlugin ? "HS256" : resolvedKey?.alg ?? jwtPluginOptions?.jwks?.keyPairConfig?.alg;
|
|
373
|
+
const atHash = accessToken ? await computeOidcHash(accessToken, signingAlg) : void 0;
|
|
299
374
|
const payload = {
|
|
300
375
|
...userClaims,
|
|
301
376
|
auth_time: authTimeSec,
|
|
302
377
|
acr,
|
|
303
378
|
...customClaims,
|
|
379
|
+
at_hash: atHash,
|
|
304
380
|
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
305
381
|
sub: resolvedSub,
|
|
306
382
|
aud: client.clientId,
|
|
@@ -310,10 +386,19 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
|
|
|
310
386
|
sid: client.enableEndSession ? sessionId : void 0
|
|
311
387
|
};
|
|
312
388
|
if (opts.disableJwtPlugin && !client.clientSecret) return;
|
|
313
|
-
|
|
389
|
+
const idToken = opts.disableJwtPlugin ? await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(new TextEncoder().encode(await decryptStoredClientSecret(ctx, opts.storeClientSecret, client.clientSecret))) : await signJWT(ctx, {
|
|
314
390
|
options: jwtPluginOptions,
|
|
315
|
-
payload
|
|
391
|
+
payload,
|
|
392
|
+
resolvedKey: resolvedKey ?? void 0
|
|
316
393
|
});
|
|
394
|
+
if (idToken && atHash && jwtPluginOptions?.jwt?.sign) {
|
|
395
|
+
const header = decodeProtectedHeader(idToken);
|
|
396
|
+
if (header.alg !== signingAlg) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
397
|
+
error_description: `ID token signed with "${header.alg}" but at_hash was computed for "${signingAlg}". Ensure jwt.sign uses the algorithm declared in keyPairConfig.alg.`,
|
|
398
|
+
error: "server_error"
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return idToken;
|
|
317
402
|
}
|
|
318
403
|
/**
|
|
319
404
|
* Encodes a refresh token for a client
|
|
@@ -402,39 +487,45 @@ async function checkResource(ctx, opts, scopes) {
|
|
|
402
487
|
}
|
|
403
488
|
return audience?.length === 1 ? audience.at(0) : audience;
|
|
404
489
|
}
|
|
405
|
-
async function createUserTokens(ctx, opts,
|
|
490
|
+
async function createUserTokens(ctx, opts, params) {
|
|
491
|
+
const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, verificationValue } = params;
|
|
406
492
|
const iat = Math.floor(Date.now() / 1e3);
|
|
407
|
-
const defaultExp = iat + (opts.accessTokenExpiresIn ?? 3600);
|
|
493
|
+
const defaultExp = iat + (user ? opts.accessTokenExpiresIn ?? 3600 : opts.m2mAccessTokenExpiresIn ?? 3600);
|
|
408
494
|
const exp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
|
|
409
495
|
return prev < curr ? prev : curr;
|
|
410
496
|
}, defaultExp) : defaultExp;
|
|
411
497
|
const audience = await checkResource(ctx, opts, scopes);
|
|
412
|
-
const isRefreshToken =
|
|
498
|
+
const isRefreshToken = user && (existingRefreshToken?.scopes?.includes("offline_access") || scopes.includes("offline_access"));
|
|
413
499
|
const isJwtAccessToken = audience && !opts.disableJwtPlugin;
|
|
414
|
-
const isIdToken = scopes.includes("openid");
|
|
415
|
-
const
|
|
500
|
+
const isIdToken = user && scopes.includes("openid");
|
|
501
|
+
const customFields = opts.customTokenResponseFields ? await opts.customTokenResponseFields({
|
|
502
|
+
grantType,
|
|
503
|
+
user,
|
|
504
|
+
scopes,
|
|
505
|
+
metadata: parseClientMetadata(client.metadata),
|
|
506
|
+
verificationValue
|
|
507
|
+
}) : void 0;
|
|
508
|
+
const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
|
|
416
509
|
iat,
|
|
417
510
|
exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
|
|
418
511
|
sid: sessionId
|
|
419
|
-
},
|
|
420
|
-
const [accessToken, refreshToken
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}, additional?.refreshToken, authTime) : void 0,
|
|
435
|
-
isIdToken ? createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime) : void 0
|
|
436
|
-
]);
|
|
512
|
+
}, existingRefreshToken, authTime) : void 0;
|
|
513
|
+
const [accessToken, refreshToken] = await Promise.all([isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, {
|
|
514
|
+
iat,
|
|
515
|
+
exp,
|
|
516
|
+
sid: sessionId
|
|
517
|
+
}) : createOpaqueAccessToken(ctx, opts, user, client, scopes, {
|
|
518
|
+
iat,
|
|
519
|
+
exp,
|
|
520
|
+
sid: sessionId
|
|
521
|
+
}, referenceId, earlyRefreshToken?.id), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
|
|
522
|
+
iat,
|
|
523
|
+
exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
|
|
524
|
+
sid: sessionId
|
|
525
|
+
}, existingRefreshToken, authTime) : void 0]);
|
|
526
|
+
const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) : void 0;
|
|
437
527
|
return ctx.json({
|
|
528
|
+
...customFields,
|
|
438
529
|
access_token: accessToken,
|
|
439
530
|
expires_in: exp - iat,
|
|
440
531
|
expires_at: exp,
|
|
@@ -450,7 +541,6 @@ async function createUserTokens(ctx, opts, client, scopes, user, referenceId, se
|
|
|
450
541
|
/** Checks verification value */
|
|
451
542
|
async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri) {
|
|
452
543
|
const verification = await ctx.context.internalAdapter.findVerificationValue(await storeToken(opts.storeTokens, code, "authorization_code"));
|
|
453
|
-
const verificationValue = verification ? JSON.parse(verification?.value) : void 0;
|
|
454
544
|
if (!verification) throw new APIError("UNAUTHORIZED", {
|
|
455
545
|
error_description: "Invalid code",
|
|
456
546
|
error: "invalid_verification"
|
|
@@ -460,22 +550,25 @@ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri)
|
|
|
460
550
|
error_description: "code expired",
|
|
461
551
|
error: "invalid_verification"
|
|
462
552
|
});
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
|
|
553
|
+
let rawValue;
|
|
554
|
+
try {
|
|
555
|
+
rawValue = JSON.parse(verification.value);
|
|
556
|
+
} catch {
|
|
557
|
+
throw new APIError("UNAUTHORIZED", {
|
|
558
|
+
error_description: "malformed verification value",
|
|
559
|
+
error: "invalid_verification"
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
const parsed = verificationValueSchema.safeParse(rawValue);
|
|
563
|
+
if (!parsed.success) throw new APIError("UNAUTHORIZED", {
|
|
564
|
+
error_description: "malformed verification value",
|
|
469
565
|
error: "invalid_verification"
|
|
470
566
|
});
|
|
567
|
+
const verificationValue = parsed.data;
|
|
471
568
|
if (verificationValue.query.client_id !== client_id) throw new APIError("UNAUTHORIZED", {
|
|
472
569
|
error_description: "invalid client_id",
|
|
473
570
|
error: "invalid_client"
|
|
474
571
|
});
|
|
475
|
-
if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
|
|
476
|
-
error_description: "missing user_id on challenge",
|
|
477
|
-
error: "invalid_user"
|
|
478
|
-
});
|
|
479
572
|
if (verificationValue.query?.redirect_uri && verificationValue.query?.redirect_uri !== redirect_uri) throw new APIError("BAD_REQUEST", {
|
|
480
573
|
error_description: "redirect_uri mismatch",
|
|
481
574
|
error: "invalid_request"
|
|
@@ -563,7 +656,17 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
563
656
|
error: "invalid_request"
|
|
564
657
|
});
|
|
565
658
|
const authTime = verificationValue.authTime != null ? normalizeTimestampValue(verificationValue.authTime) : resolveSessionAuthTime(session);
|
|
566
|
-
return createUserTokens(ctx, opts,
|
|
659
|
+
return createUserTokens(ctx, opts, {
|
|
660
|
+
client,
|
|
661
|
+
scopes: verificationValue.query.scope?.split(" ") ?? [],
|
|
662
|
+
user,
|
|
663
|
+
grantType: "authorization_code",
|
|
664
|
+
referenceId: verificationValue.referenceId,
|
|
665
|
+
sessionId: session.id,
|
|
666
|
+
nonce: verificationValue.query?.nonce,
|
|
667
|
+
authTime,
|
|
668
|
+
verificationValue
|
|
669
|
+
});
|
|
567
670
|
}
|
|
568
671
|
/**
|
|
569
672
|
* Grant that allows direct access to an API using the application's credentials
|
|
@@ -601,43 +704,11 @@ async function handleClientCredentialsGrant(ctx, opts) {
|
|
|
601
704
|
});
|
|
602
705
|
}
|
|
603
706
|
if (!requestedScopes) requestedScopes = client.scopes ?? opts.clientCredentialGrantDefaultScopes ?? opts.scopes ?? [];
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
const iat = Math.floor(Date.now() / 1e3);
|
|
607
|
-
const defaultExp = iat + (opts.m2mAccessTokenExpiresIn ?? 3600);
|
|
608
|
-
const exp = opts.scopeExpirations && requestedScopes ? requestedScopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
|
|
609
|
-
return prev < curr ? prev : curr;
|
|
610
|
-
}, defaultExp) : defaultExp;
|
|
611
|
-
const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
|
|
707
|
+
return createUserTokens(ctx, opts, {
|
|
708
|
+
client,
|
|
612
709
|
scopes: requestedScopes,
|
|
613
|
-
|
|
614
|
-
metadata: parseClientMetadata(client.metadata)
|
|
615
|
-
}) : {};
|
|
616
|
-
const accessToken = audience && !opts.disableJwtPlugin ? await signJWT(ctx, {
|
|
617
|
-
options: jwtPluginOptions,
|
|
618
|
-
payload: {
|
|
619
|
-
...customClaims,
|
|
620
|
-
aud: audience,
|
|
621
|
-
azp: client.clientId,
|
|
622
|
-
scope: requestedScopes.join(" "),
|
|
623
|
-
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
624
|
-
iat,
|
|
625
|
-
exp
|
|
626
|
-
}
|
|
627
|
-
}) : await createOpaqueAccessToken(ctx, opts, void 0, client, requestedScopes, {
|
|
628
|
-
iat,
|
|
629
|
-
exp
|
|
710
|
+
grantType: "client_credentials"
|
|
630
711
|
});
|
|
631
|
-
return ctx.json({
|
|
632
|
-
access_token: accessToken,
|
|
633
|
-
expires_in: exp - iat,
|
|
634
|
-
expires_at: exp,
|
|
635
|
-
token_type: "Bearer",
|
|
636
|
-
scope: requestedScopes.join(" ")
|
|
637
|
-
}, { headers: {
|
|
638
|
-
"Cache-Control": "no-store",
|
|
639
|
-
Pragma: "no-cache"
|
|
640
|
-
} });
|
|
641
712
|
}
|
|
642
713
|
/**
|
|
643
714
|
* Obtains new Session Jwt and Refresh Tokens using a refresh token
|
|
@@ -708,7 +779,16 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
708
779
|
error: "invalid_request"
|
|
709
780
|
});
|
|
710
781
|
const authTime = refreshToken.authTime != null ? normalizeTimestampValue(refreshToken.authTime) : void 0;
|
|
711
|
-
return createUserTokens(ctx, opts,
|
|
782
|
+
return createUserTokens(ctx, opts, {
|
|
783
|
+
client,
|
|
784
|
+
scopes: requestedScopes ?? scopes,
|
|
785
|
+
user,
|
|
786
|
+
grantType: "refresh_token",
|
|
787
|
+
referenceId: refreshToken.referenceId,
|
|
788
|
+
sessionId: refreshToken.sessionId,
|
|
789
|
+
refreshToken,
|
|
790
|
+
authTime
|
|
791
|
+
});
|
|
712
792
|
}
|
|
713
793
|
//#endregion
|
|
714
794
|
//#region src/introspect.ts
|
|
@@ -920,6 +1000,7 @@ async function resolveIntrospectionSub(opts, payload, client) {
|
|
|
920
1000
|
}
|
|
921
1001
|
async function introspectEndpoint(ctx, opts) {
|
|
922
1002
|
let { token, token_type_hint } = ctx.body;
|
|
1003
|
+
if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
|
|
923
1004
|
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
|
|
924
1005
|
if (!client_id || !client_secret && !preVerifiedClient) throw new APIError$1("UNAUTHORIZED", {
|
|
925
1006
|
error_description: "missing required credentials",
|
|
@@ -1077,6 +1158,109 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
|
|
|
1077
1158
|
}
|
|
1078
1159
|
}
|
|
1079
1160
|
//#endregion
|
|
1161
|
+
//#region src/oauth-endpoint.ts
|
|
1162
|
+
/**
|
|
1163
|
+
* Wraps `createAuthEndpoint` so zod schemas stay the single source of truth
|
|
1164
|
+
* for body/query shape while validation failures serialize as the RFC 6749
|
|
1165
|
+
* §5.2 error envelope `{ error, error_description }`.
|
|
1166
|
+
*
|
|
1167
|
+
* A failing issue is routed by its first path segment via `errorCodesByField`:
|
|
1168
|
+
* - missing required (`invalid_type` + "received undefined") → `.missing`
|
|
1169
|
+
* - unsupported value (`invalid_value`) → `.invalid`
|
|
1170
|
+
* - anything else (wrong type, duplicated params, bad format) → `defaultError`
|
|
1171
|
+
*
|
|
1172
|
+
* For enum fields that need to distinguish missing from unsupported, compose
|
|
1173
|
+
* as `z.string().pipe(z.enum([...]))` so duplicated params fail the outer
|
|
1174
|
+
* `z.string()` as `invalid_type` instead of masquerading as an unsupported
|
|
1175
|
+
* enum value.
|
|
1176
|
+
*/
|
|
1177
|
+
function createOAuthEndpoint(path, options, handler) {
|
|
1178
|
+
const { redirectOnError, onValidationError: userHook, errorCodesByField, defaultError = "invalid_request", ...rest } = options;
|
|
1179
|
+
if (!redirectOnError) return createAuthEndpoint(path, {
|
|
1180
|
+
...rest,
|
|
1181
|
+
onValidationError: async (args) => {
|
|
1182
|
+
if (userHook) await userHook(args);
|
|
1183
|
+
throw new APIError$1("BAD_REQUEST", { ...mapIssuesToOAuthError(args.issues, errorCodesByField, defaultError) });
|
|
1184
|
+
}
|
|
1185
|
+
}, handler);
|
|
1186
|
+
const redirect = redirectOnError;
|
|
1187
|
+
const { body: bodySchema, query: querySchema, ...forwarded } = rest;
|
|
1188
|
+
async function validateSlot(ctx, slot, schema) {
|
|
1189
|
+
if (!schema) return { ok: true };
|
|
1190
|
+
const result = await schema.safeParseAsync(ctx[slot] ?? {});
|
|
1191
|
+
if (result.success) {
|
|
1192
|
+
ctx[slot] = result.data;
|
|
1193
|
+
return { ok: true };
|
|
1194
|
+
}
|
|
1195
|
+
if (userHook) await userHook({
|
|
1196
|
+
message: result.error.message,
|
|
1197
|
+
issues: result.error.issues
|
|
1198
|
+
});
|
|
1199
|
+
return {
|
|
1200
|
+
ok: false,
|
|
1201
|
+
response: redirect({
|
|
1202
|
+
...mapIssuesToOAuthError(result.error.issues, errorCodesByField, defaultError),
|
|
1203
|
+
ctx
|
|
1204
|
+
})
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
return createAuthEndpoint(path, forwarded, async (ctx) => {
|
|
1208
|
+
const body = await validateSlot(ctx, "body", bodySchema);
|
|
1209
|
+
if (!body.ok) return body.response;
|
|
1210
|
+
const query = await validateSlot(ctx, "query", querySchema);
|
|
1211
|
+
if (!query.ok) return query.response;
|
|
1212
|
+
return handler(ctx);
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
function mapIssuesToOAuthError(issues, errorCodesByField, defaultError = "invalid_request") {
|
|
1216
|
+
const issue = issues[0];
|
|
1217
|
+
if (!issue) return {
|
|
1218
|
+
error: defaultError,
|
|
1219
|
+
error_description: "Invalid request."
|
|
1220
|
+
};
|
|
1221
|
+
const first = issue.path?.[0];
|
|
1222
|
+
const fieldKey = typeof first === "string" ? first : void 0;
|
|
1223
|
+
const mapping = fieldKey ? errorCodesByField?.[fieldKey] : void 0;
|
|
1224
|
+
const field = issue.path?.length ? z.core.toDotPath(issue.path) : "";
|
|
1225
|
+
return {
|
|
1226
|
+
error: resolveErrorCode(issue, mapping, defaultError),
|
|
1227
|
+
error_description: describeIssue(issue, field)
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function resolveErrorCode(issue, mapping, defaultError) {
|
|
1231
|
+
if (typeof mapping === "string") return mapping;
|
|
1232
|
+
if (isMissingValueIssue(issue)) return mapping?.missing ?? defaultError;
|
|
1233
|
+
if (issue.code === "invalid_value") return mapping?.invalid ?? defaultError;
|
|
1234
|
+
return defaultError;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Returns `true` for issues that represent an absent required value. Zod v4
|
|
1238
|
+
* strips `input` from published issues, so the signal is the `invalid_type`
|
|
1239
|
+
* code combined with a message suffix of "received undefined". The suffix is
|
|
1240
|
+
* pinned by a regression test so a zod rephrase fails the test instead of
|
|
1241
|
+
* silently reclassifying missing fields.
|
|
1242
|
+
*
|
|
1243
|
+
* Assumes the default zod error map. Consumers that install a localized map
|
|
1244
|
+
* via `z.setErrorMap()` will break this check, collapsing missing-field
|
|
1245
|
+
* failures to `defaultError`.
|
|
1246
|
+
*/
|
|
1247
|
+
function isMissingValueIssue(issue) {
|
|
1248
|
+
return issue.code === "invalid_type" && issue.message.endsWith("received undefined");
|
|
1249
|
+
}
|
|
1250
|
+
function describeIssue(issue, field) {
|
|
1251
|
+
if (!field) return issue.message;
|
|
1252
|
+
if (issue.code === "invalid_type") {
|
|
1253
|
+
if (issue.message.endsWith("received undefined")) return `${field} is required`;
|
|
1254
|
+
if (issue.message.endsWith("received array")) return `${field} must not appear more than once`;
|
|
1255
|
+
return `${field} must be a ${issue.expected ?? "valid value"}`;
|
|
1256
|
+
}
|
|
1257
|
+
if (issue.code === "invalid_value") {
|
|
1258
|
+
const values = issue.values;
|
|
1259
|
+
if (Array.isArray(values) && values.length > 0) return `${field} must be one of: ${values.join(", ")}`;
|
|
1260
|
+
}
|
|
1261
|
+
return `${field}: ${issue.message}`;
|
|
1262
|
+
}
|
|
1263
|
+
//#endregion
|
|
1080
1264
|
//#region src/middleware/index.ts
|
|
1081
1265
|
const publicSessionMiddleware = (opts) => createAuthMiddleware(async (ctx) => {
|
|
1082
1266
|
if (!opts.allowPublicClientPrelogin) throw new APIError("BAD_REQUEST");
|
|
@@ -1085,6 +1269,21 @@ const publicSessionMiddleware = (opts) => createAuthMiddleware(async (ctx) => {
|
|
|
1085
1269
|
});
|
|
1086
1270
|
//#endregion
|
|
1087
1271
|
//#region src/register.ts
|
|
1272
|
+
/**
|
|
1273
|
+
* Resolves the auth method and type for unauthenticated DCR.
|
|
1274
|
+
* Overrides confidential methods to "none" per RFC 7591 Section 3.2.1.
|
|
1275
|
+
* When overriding, clears type "web" since it is only valid for confidential clients.
|
|
1276
|
+
*/
|
|
1277
|
+
function resolveUnauthenticatedAuth(body) {
|
|
1278
|
+
if (body.token_endpoint_auth_method === "none") return {
|
|
1279
|
+
tokenEndpointAuthMethod: "none",
|
|
1280
|
+
type: body.type
|
|
1281
|
+
};
|
|
1282
|
+
return {
|
|
1283
|
+
tokenEndpointAuthMethod: "none",
|
|
1284
|
+
type: body.type === "web" ? void 0 : body.type
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1088
1287
|
async function registerEndpoint(ctx, opts) {
|
|
1089
1288
|
if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
|
|
1090
1289
|
error: "access_denied",
|
|
@@ -1096,12 +1295,16 @@ async function registerEndpoint(ctx, opts) {
|
|
|
1096
1295
|
error: "invalid_token",
|
|
1097
1296
|
error_description: "Authentication required for client registration"
|
|
1098
1297
|
});
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1298
|
+
if (!session) {
|
|
1299
|
+
if (body.grant_types?.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
|
|
1300
|
+
error: "invalid_client_metadata",
|
|
1301
|
+
error_description: "client_credentials grant requires authenticated registration"
|
|
1302
|
+
});
|
|
1303
|
+
const resolved = resolveUnauthenticatedAuth(body);
|
|
1304
|
+
body.token_endpoint_auth_method = resolved.tokenEndpointAuthMethod;
|
|
1305
|
+
body.type = resolved.type;
|
|
1306
|
+
}
|
|
1307
|
+
if (!body.scope) body.scope = (opts.clientRegistrationDefaultScopes ?? opts.scopes)?.join(" ");
|
|
1105
1308
|
return createOAuthClientEndpoint(ctx, opts, { isRegister: true });
|
|
1106
1309
|
}
|
|
1107
1310
|
async function checkOAuthClient(client, opts, settings) {
|
|
@@ -1340,57 +1543,11 @@ function schemaToOAuth(input) {
|
|
|
1340
1543
|
};
|
|
1341
1544
|
}
|
|
1342
1545
|
//#endregion
|
|
1343
|
-
//#region src/types/zod.ts
|
|
1344
|
-
const DANGEROUS_SCHEMES = [
|
|
1345
|
-
"javascript:",
|
|
1346
|
-
"data:",
|
|
1347
|
-
"vbscript:"
|
|
1348
|
-
];
|
|
1349
|
-
function isLocalhost(hostname) {
|
|
1350
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.endsWith(".localhost");
|
|
1351
|
-
}
|
|
1352
|
-
/**
|
|
1353
|
-
* Reusable URL validation for OAuth redirect URIs.
|
|
1354
|
-
* - Blocks dangerous schemes (javascript:, data:, vbscript:)
|
|
1355
|
-
* - For http/https: requires HTTPS (HTTP allowed only for localhost)
|
|
1356
|
-
* - Allows custom schemes for mobile apps (e.g., myapp://callback)
|
|
1357
|
-
*/
|
|
1358
|
-
const SafeUrlSchema = z.url().superRefine((val, ctx) => {
|
|
1359
|
-
if (!URL.canParse(val)) {
|
|
1360
|
-
ctx.addIssue({
|
|
1361
|
-
code: "custom",
|
|
1362
|
-
message: "URL must be parseable",
|
|
1363
|
-
fatal: true
|
|
1364
|
-
});
|
|
1365
|
-
return z.NEVER;
|
|
1366
|
-
}
|
|
1367
|
-
const u = new URL(val);
|
|
1368
|
-
if (DANGEROUS_SCHEMES.includes(u.protocol)) {
|
|
1369
|
-
ctx.addIssue({
|
|
1370
|
-
code: "custom",
|
|
1371
|
-
message: "URL cannot use javascript:, data:, or vbscript: scheme"
|
|
1372
|
-
});
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
if (u.protocol === "http:" || u.protocol === "https:") {
|
|
1376
|
-
if (u.protocol === "http:" && !isLocalhost(u.hostname)) ctx.addIssue({
|
|
1377
|
-
code: "custom",
|
|
1378
|
-
message: "Redirect URI must use HTTPS (HTTP allowed only for localhost)"
|
|
1379
|
-
});
|
|
1380
|
-
}
|
|
1381
|
-
});
|
|
1382
|
-
//#endregion
|
|
1383
1546
|
//#region src/oauthClient/endpoints.ts
|
|
1384
1547
|
async function getClientEndpoint(ctx, opts) {
|
|
1385
1548
|
const session = await getSessionFromCtx(ctx);
|
|
1549
|
+
await assertClientPrivileges(ctx, session, opts, "read");
|
|
1386
1550
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1387
|
-
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1388
|
-
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1389
|
-
headers: ctx.headers,
|
|
1390
|
-
action: "read",
|
|
1391
|
-
session: session.session,
|
|
1392
|
-
user: session.user
|
|
1393
|
-
})) throw new APIError("UNAUTHORIZED");
|
|
1394
1551
|
const client = await getClient(ctx, opts, ctx.query.client_id);
|
|
1395
1552
|
if (!client) throw new APIError("NOT_FOUND", {
|
|
1396
1553
|
error_description: "client not found",
|
|
@@ -1431,14 +1588,8 @@ async function getClientPublicEndpoint(ctx, opts, clientId) {
|
|
|
1431
1588
|
}
|
|
1432
1589
|
async function getClientsEndpoint(ctx, opts) {
|
|
1433
1590
|
const session = await getSessionFromCtx(ctx);
|
|
1591
|
+
await assertClientPrivileges(ctx, session, opts, "list");
|
|
1434
1592
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1435
|
-
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1436
|
-
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1437
|
-
headers: ctx.headers,
|
|
1438
|
-
action: "list",
|
|
1439
|
-
session: session.session,
|
|
1440
|
-
user: session.user
|
|
1441
|
-
})) throw new APIError("UNAUTHORIZED");
|
|
1442
1593
|
const referenceId = await opts.clientReference?.(session);
|
|
1443
1594
|
if (referenceId) return await ctx.context.adapter.findMany({
|
|
1444
1595
|
model: "oauthClient",
|
|
@@ -1472,14 +1623,8 @@ async function getClientsEndpoint(ctx, opts) {
|
|
|
1472
1623
|
}
|
|
1473
1624
|
async function deleteClientEndpoint(ctx, opts) {
|
|
1474
1625
|
const session = await getSessionFromCtx(ctx);
|
|
1626
|
+
await assertClientPrivileges(ctx, session, opts, "delete");
|
|
1475
1627
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1476
|
-
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1477
|
-
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1478
|
-
headers: ctx.headers,
|
|
1479
|
-
action: "delete",
|
|
1480
|
-
session: session.session,
|
|
1481
|
-
user: session.user
|
|
1482
|
-
})) throw new APIError("UNAUTHORIZED");
|
|
1483
1628
|
const clientId = ctx.body.client_id;
|
|
1484
1629
|
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1485
1630
|
error_description: "trusted clients must be updated manually",
|
|
@@ -1505,14 +1650,8 @@ async function deleteClientEndpoint(ctx, opts) {
|
|
|
1505
1650
|
}
|
|
1506
1651
|
async function updateClientEndpoint(ctx, opts) {
|
|
1507
1652
|
const session = await getSessionFromCtx(ctx);
|
|
1653
|
+
await assertClientPrivileges(ctx, session, opts, "update");
|
|
1508
1654
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1509
|
-
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1510
|
-
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1511
|
-
headers: ctx.headers,
|
|
1512
|
-
action: "update",
|
|
1513
|
-
session: session.session,
|
|
1514
|
-
user: session.user
|
|
1515
|
-
})) throw new APIError("UNAUTHORIZED");
|
|
1516
1655
|
const clientId = ctx.body.client_id;
|
|
1517
1656
|
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1518
1657
|
error_description: "trusted clients must be updated manually",
|
|
@@ -1543,6 +1682,7 @@ async function updateClientEndpoint(ctx, opts) {
|
|
|
1543
1682
|
else {
|
|
1544
1683
|
schemaUpdates.jwks = null;
|
|
1545
1684
|
schemaUpdates.jwksUri = null;
|
|
1685
|
+
if (!schemaUpdates.clientSecret) schemaUpdates.clientSecret = await storeClientSecret(ctx, opts, opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z"));
|
|
1546
1686
|
}
|
|
1547
1687
|
const updatedClient = await ctx.context.adapter.update({
|
|
1548
1688
|
model: "oauthClient",
|
|
@@ -1565,14 +1705,8 @@ async function updateClientEndpoint(ctx, opts) {
|
|
|
1565
1705
|
}
|
|
1566
1706
|
async function rotateClientSecretEndpoint(ctx, opts) {
|
|
1567
1707
|
const session = await getSessionFromCtx(ctx);
|
|
1708
|
+
await assertClientPrivileges(ctx, session, opts, "rotate");
|
|
1568
1709
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1569
|
-
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1570
|
-
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1571
|
-
headers: ctx.headers,
|
|
1572
|
-
action: "rotate",
|
|
1573
|
-
session: session.session,
|
|
1574
|
-
user: session.user
|
|
1575
|
-
})) throw new APIError("UNAUTHORIZED");
|
|
1576
1710
|
const clientId = ctx.body.client_id;
|
|
1577
1711
|
if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1578
1712
|
error_description: "trusted clients must be updated manually",
|
|
@@ -1614,6 +1748,16 @@ async function rotateClientSecretEndpoint(ctx, opts) {
|
|
|
1614
1748
|
clientSecret: (opts.prefix?.clientSecret ?? "") + clientSecret
|
|
1615
1749
|
});
|
|
1616
1750
|
}
|
|
1751
|
+
async function assertClientPrivileges(ctx, session, opts, action) {
|
|
1752
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1753
|
+
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1754
|
+
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1755
|
+
headers: ctx.headers,
|
|
1756
|
+
action,
|
|
1757
|
+
session: session.session,
|
|
1758
|
+
user: session.user
|
|
1759
|
+
})) throw new APIError("UNAUTHORIZED");
|
|
1760
|
+
}
|
|
1617
1761
|
//#endregion
|
|
1618
1762
|
//#region src/oauthClient/index.ts
|
|
1619
1763
|
const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/create-client", {
|
|
@@ -1799,6 +1943,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
1799
1943
|
}
|
|
1800
1944
|
}
|
|
1801
1945
|
}, async (ctx) => {
|
|
1946
|
+
await assertClientPrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
|
|
1802
1947
|
return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
|
|
1803
1948
|
});
|
|
1804
1949
|
const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client", {
|
|
@@ -1971,6 +2116,7 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
|
|
|
1971
2116
|
} }
|
|
1972
2117
|
} }
|
|
1973
2118
|
}, async (ctx) => {
|
|
2119
|
+
await assertClientPrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
|
|
1974
2120
|
return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
|
|
1975
2121
|
});
|
|
1976
2122
|
const getOAuthClient = (opts) => createAuthEndpoint("/oauth2/get-client", {
|
|
@@ -2383,6 +2529,7 @@ async function revokeAccessToken(ctx, opts, clientId, token) {
|
|
|
2383
2529
|
}
|
|
2384
2530
|
async function revokeEndpoint(ctx, opts) {
|
|
2385
2531
|
let { token, token_type_hint } = ctx.body;
|
|
2532
|
+
if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
|
|
2386
2533
|
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/revoke`));
|
|
2387
2534
|
if (!client_id) throw new APIError$1("UNAUTHORIZED", {
|
|
2388
2535
|
error_description: "missing required credentials",
|
|
@@ -2800,7 +2947,7 @@ const oauthProvider = (options) => {
|
|
|
2800
2947
|
queryParams.delete("sig");
|
|
2801
2948
|
queryParams.delete("exp");
|
|
2802
2949
|
await oAuthState.set({ query: queryParams.toString() });
|
|
2803
|
-
if (ctx.path === "/sign-in/social"
|
|
2950
|
+
if (ctx.path === "/sign-in/social") {
|
|
2804
2951
|
if (ctx.body.additionalData?.query) return;
|
|
2805
2952
|
if (!ctx.body.additionalData) ctx.body.additionalData = {};
|
|
2806
2953
|
ctx.body.additionalData.query = queryParams.toString();
|
|
@@ -2834,12 +2981,15 @@ const oauthProvider = (options) => {
|
|
|
2834
2981
|
metadata: { SERVER_ONLY: true }
|
|
2835
2982
|
}, async (ctx) => {
|
|
2836
2983
|
if (opts.scopes && opts.scopes.includes("openid")) return oidcServerMetadata(ctx, opts);
|
|
2837
|
-
else return
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2984
|
+
else return {
|
|
2985
|
+
...authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
|
|
2986
|
+
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
2987
|
+
public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
|
|
2988
|
+
grant_types_supported: opts.grantTypes,
|
|
2989
|
+
jwt_disabled: opts.disableJwtPlugin
|
|
2990
|
+
}),
|
|
2991
|
+
...mergeDiscoveryMetadata(opts.clientDiscovery)
|
|
2992
|
+
};
|
|
2843
2993
|
}),
|
|
2844
2994
|
getOpenIdConfig: createAuthEndpoint("/.well-known/openid-configuration", {
|
|
2845
2995
|
method: "GET",
|
|
@@ -2848,19 +2998,19 @@ const oauthProvider = (options) => {
|
|
|
2848
2998
|
if (opts.scopes && !opts.scopes.includes("openid")) throw new APIError("NOT_FOUND");
|
|
2849
2999
|
return oidcServerMetadata(ctx, opts);
|
|
2850
3000
|
}),
|
|
2851
|
-
oauth2Authorize:
|
|
3001
|
+
oauth2Authorize: createOAuthEndpoint("/oauth2/authorize", {
|
|
2852
3002
|
method: "GET",
|
|
2853
3003
|
query: z.object({
|
|
2854
|
-
response_type: z.enum(["code"]).optional(),
|
|
3004
|
+
response_type: z.string().pipe(z.enum(["code"])).optional(),
|
|
2855
3005
|
client_id: z.string(),
|
|
2856
3006
|
redirect_uri: SafeUrlSchema.optional(),
|
|
2857
3007
|
scope: z.string().optional(),
|
|
2858
3008
|
state: z.string().optional(),
|
|
2859
3009
|
request_uri: z.string().optional(),
|
|
2860
3010
|
code_challenge: z.string().optional(),
|
|
2861
|
-
code_challenge_method: z.enum(["S256"]).optional(),
|
|
3011
|
+
code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
|
|
2862
3012
|
nonce: z.string().optional(),
|
|
2863
|
-
prompt: z.enum([
|
|
3013
|
+
prompt: z.string().pipe(z.enum([
|
|
2864
3014
|
"none",
|
|
2865
3015
|
"consent",
|
|
2866
3016
|
"login",
|
|
@@ -2868,8 +3018,10 @@ const oauthProvider = (options) => {
|
|
|
2868
3018
|
"select_account",
|
|
2869
3019
|
"login consent",
|
|
2870
3020
|
"select_account consent"
|
|
2871
|
-
]).optional()
|
|
3021
|
+
])).optional()
|
|
2872
3022
|
}),
|
|
3023
|
+
redirectOnError: authorizeRedirectOnError(opts),
|
|
3024
|
+
errorCodesByField: { response_type: { invalid: "unsupported_response_type" } },
|
|
2873
3025
|
metadata: { openapi: {
|
|
2874
3026
|
description: "Authorize an OAuth2 request",
|
|
2875
3027
|
parameters: [
|
|
@@ -3028,14 +3180,14 @@ const oauthProvider = (options) => {
|
|
|
3028
3180
|
}, async (ctx) => {
|
|
3029
3181
|
return continueEndpoint(ctx, opts);
|
|
3030
3182
|
}),
|
|
3031
|
-
oauth2Token:
|
|
3183
|
+
oauth2Token: createOAuthEndpoint("/oauth2/token", {
|
|
3032
3184
|
method: "POST",
|
|
3033
3185
|
body: z.object({
|
|
3034
|
-
grant_type: z.enum([
|
|
3186
|
+
grant_type: z.string().pipe(z.enum([
|
|
3035
3187
|
"authorization_code",
|
|
3036
3188
|
"client_credentials",
|
|
3037
3189
|
"refresh_token"
|
|
3038
|
-
]),
|
|
3190
|
+
])),
|
|
3039
3191
|
client_id: z.string().optional(),
|
|
3040
3192
|
client_secret: z.string().optional(),
|
|
3041
3193
|
client_assertion: z.string().optional(),
|
|
@@ -3047,6 +3199,10 @@ const oauthProvider = (options) => {
|
|
|
3047
3199
|
resource: z.string().optional(),
|
|
3048
3200
|
scope: z.string().optional()
|
|
3049
3201
|
}),
|
|
3202
|
+
errorCodesByField: { grant_type: {
|
|
3203
|
+
missing: "invalid_request",
|
|
3204
|
+
invalid: "unsupported_grant_type"
|
|
3205
|
+
} },
|
|
3050
3206
|
metadata: {
|
|
3051
3207
|
allowedMediaTypes: ["application/x-www-form-urlencoded"],
|
|
3052
3208
|
openapi: {
|
|
@@ -3159,7 +3315,7 @@ const oauthProvider = (options) => {
|
|
|
3159
3315
|
}, async (ctx) => {
|
|
3160
3316
|
return tokenEndpoint(ctx, opts);
|
|
3161
3317
|
}),
|
|
3162
|
-
oauth2Introspect:
|
|
3318
|
+
oauth2Introspect: createOAuthEndpoint("/oauth2/introspect", {
|
|
3163
3319
|
method: "POST",
|
|
3164
3320
|
body: z.object({
|
|
3165
3321
|
client_id: z.string().optional(),
|
|
@@ -3167,7 +3323,7 @@ const oauthProvider = (options) => {
|
|
|
3167
3323
|
client_assertion: z.string().optional(),
|
|
3168
3324
|
client_assertion_type: z.string().optional(),
|
|
3169
3325
|
token: z.string(),
|
|
3170
|
-
token_type_hint: z.
|
|
3326
|
+
token_type_hint: z.string().optional()
|
|
3171
3327
|
}),
|
|
3172
3328
|
metadata: {
|
|
3173
3329
|
allowedMediaTypes: ["application/x-www-form-urlencoded"],
|
|
@@ -3192,8 +3348,7 @@ const oauthProvider = (options) => {
|
|
|
3192
3348
|
},
|
|
3193
3349
|
token_type_hint: {
|
|
3194
3350
|
type: "string",
|
|
3195
|
-
|
|
3196
|
-
description: "Hint about the type of the token submitted for introspection"
|
|
3351
|
+
description: "Hint about the token type. Recognized values: `access_token`, `refresh_token`."
|
|
3197
3352
|
},
|
|
3198
3353
|
resource: {
|
|
3199
3354
|
type: "string",
|
|
@@ -3279,7 +3434,7 @@ const oauthProvider = (options) => {
|
|
|
3279
3434
|
}, async (ctx) => {
|
|
3280
3435
|
return introspectEndpoint(ctx, opts);
|
|
3281
3436
|
}),
|
|
3282
|
-
oauth2Revoke:
|
|
3437
|
+
oauth2Revoke: createOAuthEndpoint("/oauth2/revoke", {
|
|
3283
3438
|
method: "POST",
|
|
3284
3439
|
body: z.object({
|
|
3285
3440
|
client_id: z.string().optional(),
|
|
@@ -3287,7 +3442,7 @@ const oauthProvider = (options) => {
|
|
|
3287
3442
|
client_assertion: z.string().optional(),
|
|
3288
3443
|
client_assertion_type: z.string().optional(),
|
|
3289
3444
|
token: z.string(),
|
|
3290
|
-
token_type_hint: z.
|
|
3445
|
+
token_type_hint: z.string().optional()
|
|
3291
3446
|
}),
|
|
3292
3447
|
metadata: {
|
|
3293
3448
|
allowedMediaTypes: ["application/x-www-form-urlencoded"],
|
|
@@ -3312,8 +3467,7 @@ const oauthProvider = (options) => {
|
|
|
3312
3467
|
},
|
|
3313
3468
|
token_type_hint: {
|
|
3314
3469
|
type: "string",
|
|
3315
|
-
|
|
3316
|
-
description: "Hint about the type of the token submitted for revocation"
|
|
3470
|
+
description: "Hint about the token type. Recognized values: `access_token`, `refresh_token`."
|
|
3317
3471
|
}
|
|
3318
3472
|
},
|
|
3319
3473
|
required: ["token"]
|
|
@@ -3434,7 +3588,7 @@ const oauthProvider = (options) => {
|
|
|
3434
3588
|
}, async (ctx) => {
|
|
3435
3589
|
return userInfoEndpoint(ctx, opts);
|
|
3436
3590
|
}),
|
|
3437
|
-
oauth2EndSession:
|
|
3591
|
+
oauth2EndSession: createOAuthEndpoint("/oauth2/end-session", {
|
|
3438
3592
|
method: "GET",
|
|
3439
3593
|
query: z.object({
|
|
3440
3594
|
id_token_hint: z.string(),
|
|
@@ -3465,7 +3619,7 @@ const oauthProvider = (options) => {
|
|
|
3465
3619
|
}, async (ctx) => {
|
|
3466
3620
|
return rpInitiatedLogoutEndpoint(ctx, opts);
|
|
3467
3621
|
}),
|
|
3468
|
-
registerOAuthClient:
|
|
3622
|
+
registerOAuthClient: createOAuthEndpoint("/oauth2/register", {
|
|
3469
3623
|
method: "POST",
|
|
3470
3624
|
body: z.object({
|
|
3471
3625
|
redirect_uris: z.array(SafeUrlSchema).min(1).min(1),
|
|
@@ -3502,6 +3656,12 @@ const oauthProvider = (options) => {
|
|
|
3502
3656
|
subject_type: z.enum(["public", "pairwise"]).optional(),
|
|
3503
3657
|
skip_consent: z.never({ error: "skip_consent cannot be set during dynamic client registration" }).optional()
|
|
3504
3658
|
}),
|
|
3659
|
+
errorCodesByField: {
|
|
3660
|
+
redirect_uris: "invalid_redirect_uri",
|
|
3661
|
+
post_logout_redirect_uris: "invalid_redirect_uri",
|
|
3662
|
+
software_statement: "invalid_software_statement"
|
|
3663
|
+
},
|
|
3664
|
+
defaultError: "invalid_client_metadata",
|
|
3505
3665
|
metadata: { openapi: {
|
|
3506
3666
|
description: "Register an OAuth2 application",
|
|
3507
3667
|
responses: { "200": {
|
|
@@ -3694,17 +3854,37 @@ const oauthProvider = (options) => {
|
|
|
3694
3854
|
//#endregion
|
|
3695
3855
|
//#region src/authorize.ts
|
|
3696
3856
|
/**
|
|
3697
|
-
* Formats an error url
|
|
3857
|
+
* Formats an error url. Per OIDC Core 1.0 §5 / RFC 6749 §4.2.2.1, errors on
|
|
3858
|
+
* implicit and hybrid flows are delivered in the URL fragment, not the query.
|
|
3859
|
+
* Callers on the code flow (default) omit `mode` and get query delivery.
|
|
3698
3860
|
*/
|
|
3699
|
-
function formatErrorURL(url, error, description, state, iss) {
|
|
3861
|
+
function formatErrorURL(url, error, description, state, iss, mode = "query") {
|
|
3700
3862
|
const searchParams = new URLSearchParams({
|
|
3701
3863
|
error,
|
|
3702
3864
|
error_description: description
|
|
3703
3865
|
});
|
|
3704
3866
|
state && searchParams.append("state", state);
|
|
3705
3867
|
iss && searchParams.append("iss", iss);
|
|
3868
|
+
if (mode === "fragment") return `${url}#${searchParams.toString()}`;
|
|
3706
3869
|
return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
|
|
3707
3870
|
}
|
|
3871
|
+
/**
|
|
3872
|
+
* Selects the response mode for an error redirect to the RP. OIDC Core 1.0 §5
|
|
3873
|
+
* defines defaults based on response_type: `code` → query, types containing
|
|
3874
|
+
* `token` / `id_token` → fragment. An explicit `response_mode` overrides.
|
|
3875
|
+
*
|
|
3876
|
+
* When `response_type` is duplicated (array) or absent, we can't trust the
|
|
3877
|
+
* caller's intent, so we default to query — the safer channel for
|
|
3878
|
+
* unrecognized shapes.
|
|
3879
|
+
*/
|
|
3880
|
+
function deriveResponseMode(raw) {
|
|
3881
|
+
const responseMode = typeof raw.response_mode === "string" ? raw.response_mode : void 0;
|
|
3882
|
+
if (responseMode === "fragment") return "fragment";
|
|
3883
|
+
if (responseMode === "query") return "query";
|
|
3884
|
+
const responseType = typeof raw.response_type === "string" ? raw.response_type : void 0;
|
|
3885
|
+
if (responseType && /\b(token|id_token)\b/.test(responseType)) return "fragment";
|
|
3886
|
+
return "query";
|
|
3887
|
+
}
|
|
3708
3888
|
const handleRedirect = (ctx, uri) => {
|
|
3709
3889
|
const fromFetch = isBrowserFetchRequest(ctx.request?.headers);
|
|
3710
3890
|
const acceptJson = ctx.headers?.get("accept")?.includes("application/json");
|
|
@@ -3728,8 +3908,7 @@ function redirectWithPromptNoneError(ctx, opts, query, error, description) {
|
|
|
3728
3908
|
function validateIssuerUrl(issuer) {
|
|
3729
3909
|
try {
|
|
3730
3910
|
const url = new URL(issuer);
|
|
3731
|
-
|
|
3732
|
-
if (url.protocol !== "https:" && !isLocalhost) url.protocol = "https:";
|
|
3911
|
+
if (url.protocol !== "https:" && !isLoopbackHost(url.host)) url.protocol = "https:";
|
|
3733
3912
|
url.search = "";
|
|
3734
3913
|
url.hash = "";
|
|
3735
3914
|
return url.toString().replace(/\/$/, "");
|
|
@@ -3757,6 +3936,64 @@ function getIssuer(ctx, opts) {
|
|
|
3757
3936
|
function getErrorURL(ctx, error, description) {
|
|
3758
3937
|
return formatErrorURL(ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`, error, description);
|
|
3759
3938
|
}
|
|
3939
|
+
/**
|
|
3940
|
+
* Finds the matching entry in a client's registered redirect_uris for a
|
|
3941
|
+
* requested redirect_uri. Honors RFC 8252 §7.3 loopback port variance for
|
|
3942
|
+
* the full 127.0.0.0/8 range and [::1], matching on scheme+host+path+query
|
|
3943
|
+
* and ignoring port. DNS names like "localhost" are excluded per §8.3.
|
|
3944
|
+
*/
|
|
3945
|
+
function findRegisteredRedirectUri(registered, requested) {
|
|
3946
|
+
if (!registered || !requested) return void 0;
|
|
3947
|
+
let req;
|
|
3948
|
+
try {
|
|
3949
|
+
req = new URL(requested);
|
|
3950
|
+
} catch {}
|
|
3951
|
+
return registered.find((url) => {
|
|
3952
|
+
if (url === requested) return true;
|
|
3953
|
+
if (!req) return false;
|
|
3954
|
+
try {
|
|
3955
|
+
const reg = new URL(url);
|
|
3956
|
+
return isLoopbackIP(reg.hostname) && reg.hostname === req.hostname && reg.pathname === req.pathname && reg.protocol === req.protocol && reg.search === req.search;
|
|
3957
|
+
} catch {
|
|
3958
|
+
return false;
|
|
3959
|
+
}
|
|
3960
|
+
});
|
|
3961
|
+
}
|
|
3962
|
+
/**
|
|
3963
|
+
* Loads the client, verifies it's enabled, and returns the requested
|
|
3964
|
+
* redirect_uri when it matches a registered entry. Returns null whenever the
|
|
3965
|
+
* RP cannot be safely reached, so callers can fall back to the server error
|
|
3966
|
+
* page (avoiding open-redirect risk on validation failures).
|
|
3967
|
+
*/
|
|
3968
|
+
async function resolveTrustedRedirectUri(ctx, opts, clientId, redirectUri) {
|
|
3969
|
+
if (!clientId || !redirectUri) return null;
|
|
3970
|
+
let client;
|
|
3971
|
+
try {
|
|
3972
|
+
client = await getClient(ctx, opts, clientId);
|
|
3973
|
+
} catch {
|
|
3974
|
+
return null;
|
|
3975
|
+
}
|
|
3976
|
+
if (!client || client.disabled) return null;
|
|
3977
|
+
return findRegisteredRedirectUri(client.redirectUris, redirectUri) ? redirectUri : null;
|
|
3978
|
+
}
|
|
3979
|
+
/**
|
|
3980
|
+
* `redirectOnError` callback for `/oauth2/authorize`. Per RFC 6749 §4.1.2.1,
|
|
3981
|
+
* authorize errors MUST be delivered to the client's `redirect_uri` with
|
|
3982
|
+
* `error`, `error_description`, `state`, and (RFC 9207) `iss`. The clause
|
|
3983
|
+
* carves out one case: a missing/invalid `redirect_uri` or `client_id` MUST
|
|
3984
|
+
* NOT redirect to the requested URI. We implement the carve-out via
|
|
3985
|
+
* `resolveTrustedRedirectUri`, falling back to the server error page.
|
|
3986
|
+
*
|
|
3987
|
+
* Channel (query vs fragment) follows OIDC Core §5 via `deriveResponseMode`.
|
|
3988
|
+
*/
|
|
3989
|
+
function authorizeRedirectOnError(opts) {
|
|
3990
|
+
return async ({ error, error_description, ctx }) => {
|
|
3991
|
+
const raw = ctx.query ?? {};
|
|
3992
|
+
const trusted = await resolveTrustedRedirectUri(ctx, opts, typeof raw.client_id === "string" ? raw.client_id : void 0, typeof raw.redirect_uri === "string" ? raw.redirect_uri : void 0);
|
|
3993
|
+
if (trusted) return handleRedirect(ctx, formatErrorURL(trusted, error, error_description, typeof raw.state === "string" ? raw.state : void 0, getIssuer(ctx, opts), deriveResponseMode(raw)));
|
|
3994
|
+
return handleRedirect(ctx, getErrorURL(ctx, error, error_description));
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3760
3997
|
async function authorizeEndpoint(ctx, opts, settings) {
|
|
3761
3998
|
if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError$1("NOT_FOUND");
|
|
3762
3999
|
if (!ctx.request) throw new APIError$1("UNAUTHORIZED", {
|
|
@@ -3787,15 +4024,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
3787
4024
|
const client = await getClient(ctx, opts, query.client_id);
|
|
3788
4025
|
if (!client) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "client_id is required"));
|
|
3789
4026
|
if (client.disabled) return handleRedirect(ctx, getErrorURL(ctx, "client_disabled", "client is disabled"));
|
|
3790
|
-
if (!client.redirectUris
|
|
3791
|
-
if (url === query.redirect_uri) return true;
|
|
3792
|
-
try {
|
|
3793
|
-
const registered = new URL(url);
|
|
3794
|
-
const requested = new URL(query.redirect_uri);
|
|
3795
|
-
if ((registered.hostname === "127.0.0.1" || registered.hostname === "[::1]") && registered.hostname === requested.hostname && registered.pathname === requested.pathname && registered.protocol === requested.protocol && registered.search === requested.search) return true;
|
|
3796
|
-
} catch {}
|
|
3797
|
-
return false;
|
|
3798
|
-
}) || !query.redirect_uri) return handleRedirect(ctx, getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
|
|
4027
|
+
if (!findRegisteredRedirectUri(client.redirectUris, query.redirect_uri) || !query.redirect_uri) return handleRedirect(ctx, getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
|
|
3799
4028
|
let requestedScopes = query.scope?.split(" ").filter((s) => s);
|
|
3800
4029
|
if (requestedScopes) {
|
|
3801
4030
|
const validScopes = new Set(client.scopes ?? opts.scopes);
|
|
@@ -4004,7 +4233,7 @@ function oidcServerMetadata(ctx, opts) {
|
|
|
4004
4233
|
return {
|
|
4005
4234
|
...authServerMetadata(ctx, jwtPluginOptions, {
|
|
4006
4235
|
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
4007
|
-
public_client_supported: opts.allowUnauthenticatedClientRegistration,
|
|
4236
|
+
public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
|
|
4008
4237
|
grant_types_supported: opts.grantTypes,
|
|
4009
4238
|
jwt_disabled: opts.disableJwtPlugin
|
|
4010
4239
|
}),
|
|
@@ -4020,9 +4249,20 @@ function oidcServerMetadata(ctx, opts) {
|
|
|
4020
4249
|
"create",
|
|
4021
4250
|
"select_account",
|
|
4022
4251
|
"none"
|
|
4023
|
-
]
|
|
4252
|
+
],
|
|
4253
|
+
...mergeDiscoveryMetadata(opts.clientDiscovery)
|
|
4024
4254
|
};
|
|
4025
4255
|
}
|
|
4256
|
+
const METADATA_CACHE_CONTROL = "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400";
|
|
4257
|
+
function metadataResponse(body, extraHeaders) {
|
|
4258
|
+
const headers = new Headers(extraHeaders);
|
|
4259
|
+
if (!headers.has("Cache-Control")) headers.set("Cache-Control", METADATA_CACHE_CONTROL);
|
|
4260
|
+
headers.set("Content-Type", "application/json");
|
|
4261
|
+
return new Response(JSON.stringify(body), {
|
|
4262
|
+
status: 200,
|
|
4263
|
+
headers
|
|
4264
|
+
});
|
|
4265
|
+
}
|
|
4026
4266
|
/**
|
|
4027
4267
|
* Provides an exportable `/.well-known/oauth-authorization-server`.
|
|
4028
4268
|
*
|
|
@@ -4032,16 +4272,11 @@ function oidcServerMetadata(ctx, opts) {
|
|
|
4032
4272
|
* @external
|
|
4033
4273
|
*/
|
|
4034
4274
|
const oauthProviderAuthServerMetadata = (auth, opts) => {
|
|
4035
|
-
return async (
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
"Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
|
4041
|
-
...opts?.headers,
|
|
4042
|
-
"Content-Type": "application/json"
|
|
4043
|
-
}
|
|
4044
|
-
});
|
|
4275
|
+
return async (request) => {
|
|
4276
|
+
return metadataResponse(await auth.api.getOAuthServerConfig({
|
|
4277
|
+
request,
|
|
4278
|
+
asResponse: false
|
|
4279
|
+
}), opts?.headers);
|
|
4045
4280
|
};
|
|
4046
4281
|
};
|
|
4047
4282
|
/**
|
|
@@ -4053,17 +4288,12 @@ const oauthProviderAuthServerMetadata = (auth, opts) => {
|
|
|
4053
4288
|
* @external
|
|
4054
4289
|
*/
|
|
4055
4290
|
const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
|
|
4056
|
-
return async (
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
"Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
|
|
4062
|
-
...opts?.headers,
|
|
4063
|
-
"Content-Type": "application/json"
|
|
4064
|
-
}
|
|
4065
|
-
});
|
|
4291
|
+
return async (request) => {
|
|
4292
|
+
return metadataResponse(await auth.api.getOpenIdConfig({
|
|
4293
|
+
request,
|
|
4294
|
+
asResponse: false
|
|
4295
|
+
}), opts?.headers);
|
|
4066
4296
|
};
|
|
4067
4297
|
};
|
|
4068
4298
|
//#endregion
|
|
4069
|
-
export { authServerMetadata, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
|
|
4299
|
+
export { authServerMetadata, checkOAuthClient, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata };
|