@better-auth/oauth-provider 1.7.0-beta.2 → 1.7.0-beta.4
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-CderPEmR.mjs → client-assertion-DLMKVgoj.mjs} +4 -4
- package/dist/client-resource.d.mts +13 -6
- 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 +8 -7
- package/dist/index.mjs +397 -172
- package/dist/{oauth-DJcZ8MMZ.d.mts → oauth-Vt3lTNHX.d.mts} +79 -20
- package/dist/{oauth-CU79t-eG.d.mts → oauth-q7dn10NU.d.mts} +51 -10
- package/dist/{utils-Cx_XnD9i.mjs → utils-DKBWQ8fe.mjs} +69 -26
- package/dist/{version-CZxZ64qJ.mjs → version-nFnRm-a3.mjs} +1 -1
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { n as isPrivateHostname } from "./client-assertion-
|
|
1
|
+
import { n as isPrivateHostname } from "./client-assertion-DLMKVgoj.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 { C as toAudienceClaim, D as verifyOAuthQueryParams, E as validateClientCredentials, S as storeToken, T as toResourceList, _ as resolveSessionAuthTime, a as getClient, b as signedQueryIssuedAtParam, c as getSignedQueryIssuedAt, d as mergeDiscoveryMetadata, f as normalizeTimestampValue, g as removePromptFromQuery, h as postLoginClearedParam, i as extractClientCredentials, l as getStoredToken, m as parsePrompt, n as decryptStoredClientSecret, o as getJwtPlugin, p as parseClientMetadata, r as destructureCredentials, t as checkResource, u as isPKCERequired, v as resolveSubjectIdentifier, w as toClientDiscoveryArray, x as storeClientSecret, y as searchParamsToQuery } from "./utils-DKBWQ8fe.mjs";
|
|
4
|
+
import { t as PACKAGE_VERSION } from "./version-nFnRm-a3.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
|
-
import {
|
|
8
|
+
import { PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
|
|
9
9
|
import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
|
|
10
10
|
import { isLoopbackHost, isLoopbackIP } from "@better-auth/core/utils/host";
|
|
11
11
|
import { generateRandomString, makeSignature } from "better-auth/crypto";
|
|
@@ -17,9 +17,11 @@ import { mergeSchema } from "better-auth/db";
|
|
|
17
17
|
import * as z from "zod";
|
|
18
18
|
import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
|
|
19
19
|
import { SignJWT, base64url, compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader } from "jose";
|
|
20
|
+
import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
|
|
20
21
|
//#region src/consent.ts
|
|
21
22
|
async function consentEndpoint(ctx, opts) {
|
|
22
|
-
const
|
|
23
|
+
const oauthRequest = await oAuthState.get();
|
|
24
|
+
const _query = oauthRequest?.query;
|
|
23
25
|
if (!_query) throw new APIError("BAD_REQUEST", {
|
|
24
26
|
error_description: "missing oauth query",
|
|
25
27
|
error: "invalid_request"
|
|
@@ -43,6 +45,17 @@ async function consentEndpoint(ctx, opts) {
|
|
|
43
45
|
url: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0, getIssuer(ctx, opts))
|
|
44
46
|
};
|
|
45
47
|
const session = await getSessionFromCtx(ctx);
|
|
48
|
+
const hasLoginPrompt = parsePrompt(query.get("prompt") ?? "").has("login");
|
|
49
|
+
const hasSatisfiedLoginPrompt = hasLoginPrompt && sessionSatisfiesLoginPrompt(session?.session.createdAt, oauthRequest?.signedQueryIssuedAt);
|
|
50
|
+
if (hasLoginPrompt && !hasSatisfiedLoginPrompt) {
|
|
51
|
+
ctx?.headers?.set("accept", "application/json");
|
|
52
|
+
ctx.query = searchParamsToQuery(query);
|
|
53
|
+
const { url } = await authorizeEndpoint(ctx, opts);
|
|
54
|
+
return {
|
|
55
|
+
redirect: true,
|
|
56
|
+
url
|
|
57
|
+
};
|
|
58
|
+
}
|
|
46
59
|
const referenceId = await opts.postLogin?.consentReferenceId?.({
|
|
47
60
|
user: session?.user,
|
|
48
61
|
session: session?.session,
|
|
@@ -66,12 +79,14 @@ async function consentEndpoint(ctx, opts) {
|
|
|
66
79
|
]
|
|
67
80
|
});
|
|
68
81
|
const iat = Math.floor(Date.now() / 1e3);
|
|
82
|
+
const resource = query.getAll("resource");
|
|
69
83
|
const consent = {
|
|
70
84
|
clientId,
|
|
71
85
|
userId: session?.user.id,
|
|
72
86
|
scopes: requestedScopes ?? originalRequestedScopes,
|
|
73
87
|
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
74
88
|
updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
89
|
+
resources: resource.length ? resource : void 0,
|
|
75
90
|
referenceId
|
|
76
91
|
};
|
|
77
92
|
foundConsent?.id ? await ctx.context.adapter.update({
|
|
@@ -81,6 +96,7 @@ async function consentEndpoint(ctx, opts) {
|
|
|
81
96
|
value: foundConsent.id
|
|
82
97
|
}],
|
|
83
98
|
update: {
|
|
99
|
+
resources: consent.resources,
|
|
84
100
|
scopes: consent.scopes,
|
|
85
101
|
updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
86
102
|
}
|
|
@@ -93,14 +109,21 @@ async function consentEndpoint(ctx, opts) {
|
|
|
93
109
|
});
|
|
94
110
|
if (requestedScopes) query.set("scope", consent.scopes.join(" "));
|
|
95
111
|
ctx?.headers?.set("accept", "application/json");
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
let authorizationQuery = removePromptFromQuery(query, "consent");
|
|
113
|
+
if (hasSatisfiedLoginPrompt) authorizationQuery = removePromptFromQuery(authorizationQuery, "login");
|
|
114
|
+
ctx.query = searchParamsToQuery(authorizationQuery);
|
|
115
|
+
const { url } = await authorizeEndpoint(ctx, opts, { postLogin: oauthRequest?.postLoginClearedForSession !== void 0 && oauthRequest.postLoginClearedForSession === session?.session.id });
|
|
99
116
|
return {
|
|
100
117
|
redirect: true,
|
|
101
118
|
url
|
|
102
119
|
};
|
|
103
120
|
}
|
|
121
|
+
function sessionSatisfiesLoginPrompt(sessionCreatedAt, signedQueryIssuedAt) {
|
|
122
|
+
if (!signedQueryIssuedAt) return false;
|
|
123
|
+
const normalized = normalizeTimestampValue(sessionCreatedAt);
|
|
124
|
+
if (!normalized) return false;
|
|
125
|
+
return normalized.getTime() >= signedQueryIssuedAt.getTime();
|
|
126
|
+
}
|
|
104
127
|
//#endregion
|
|
105
128
|
//#region src/continue.ts
|
|
106
129
|
async function continueEndpoint(ctx, opts) {
|
|
@@ -119,7 +142,7 @@ async function selected(ctx, opts) {
|
|
|
119
142
|
error: "invalid_request"
|
|
120
143
|
});
|
|
121
144
|
ctx.headers?.set("accept", "application/json");
|
|
122
|
-
ctx.query =
|
|
145
|
+
ctx.query = searchParamsToQuery(removePromptFromQuery(new URLSearchParams(_query), "select_account"));
|
|
123
146
|
const { url } = await authorizeEndpoint(ctx, opts);
|
|
124
147
|
return {
|
|
125
148
|
redirect: true,
|
|
@@ -134,7 +157,7 @@ async function created(ctx, opts) {
|
|
|
134
157
|
});
|
|
135
158
|
const query = new URLSearchParams(_query);
|
|
136
159
|
ctx.headers?.set("accept", "application/json");
|
|
137
|
-
ctx.query =
|
|
160
|
+
ctx.query = searchParamsToQuery(removePromptFromQuery(query, "create"));
|
|
138
161
|
const { url } = await authorizeEndpoint(ctx, opts);
|
|
139
162
|
return {
|
|
140
163
|
redirect: true,
|
|
@@ -158,11 +181,6 @@ async function postLogin(ctx, opts) {
|
|
|
158
181
|
}
|
|
159
182
|
//#endregion
|
|
160
183
|
//#region src/types/zod.ts
|
|
161
|
-
const DANGEROUS_SCHEMES = [
|
|
162
|
-
"javascript:",
|
|
163
|
-
"data:",
|
|
164
|
-
"vbscript:"
|
|
165
|
-
];
|
|
166
184
|
/**
|
|
167
185
|
* Runtime schema for OAuthAuthorizationQuery.
|
|
168
186
|
* Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
|
|
@@ -183,7 +201,8 @@ const oauthAuthorizationQuerySchema = z.object({
|
|
|
183
201
|
id_token_hint: z.string().optional(),
|
|
184
202
|
code_challenge: z.string().optional(),
|
|
185
203
|
code_challenge_method: z.literal("S256").optional(),
|
|
186
|
-
nonce: z.string().optional()
|
|
204
|
+
nonce: z.string().optional(),
|
|
205
|
+
resource: z.union([z.string(), z.array(z.string())]).optional()
|
|
187
206
|
}).passthrough();
|
|
188
207
|
/**
|
|
189
208
|
* Runtime schema for the authorization code verification value.
|
|
@@ -196,34 +215,40 @@ const verificationValueSchema = z.object({
|
|
|
196
215
|
sessionId: z.string(),
|
|
197
216
|
userId: z.string(),
|
|
198
217
|
referenceId: z.string().optional(),
|
|
199
|
-
authTime: z.number().optional()
|
|
218
|
+
authTime: z.number().optional(),
|
|
219
|
+
resource: z.array(z.string()).optional()
|
|
200
220
|
}).passthrough();
|
|
221
|
+
const DANGEROUS_SCHEMES = [
|
|
222
|
+
"javascript:",
|
|
223
|
+
"data:",
|
|
224
|
+
"vbscript:"
|
|
225
|
+
];
|
|
201
226
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
227
|
+
* Validates an RFC 8707 resource indicator. The value must be an absolute URI
|
|
228
|
+
* with no fragment (RFC 8707 §2). Unlike a redirect URI it is not restricted to
|
|
229
|
+
* HTTPS, because a resource server identifier may use any absolute URI scheme;
|
|
230
|
+
* the configured `validAudiences` allowlist is the authoritative control over
|
|
231
|
+
* which resources a token may target.
|
|
206
232
|
*/
|
|
207
|
-
const
|
|
233
|
+
const ResourceUriSchema = z.string().superRefine((val, ctx) => {
|
|
208
234
|
if (!URL.canParse(val)) {
|
|
209
235
|
ctx.addIssue({
|
|
210
236
|
code: "custom",
|
|
211
|
-
message: "
|
|
237
|
+
message: "resource must be an absolute URI",
|
|
212
238
|
fatal: true
|
|
213
239
|
});
|
|
214
240
|
return z.NEVER;
|
|
215
241
|
}
|
|
216
|
-
|
|
217
|
-
if (DANGEROUS_SCHEMES.includes(u.protocol)) {
|
|
242
|
+
if (val.includes("#")) {
|
|
218
243
|
ctx.addIssue({
|
|
219
244
|
code: "custom",
|
|
220
|
-
message: "
|
|
245
|
+
message: "resource must not contain a fragment"
|
|
221
246
|
});
|
|
222
247
|
return;
|
|
223
248
|
}
|
|
224
|
-
if (
|
|
249
|
+
if (DANGEROUS_SCHEMES.includes(new URL(val).protocol)) ctx.addIssue({
|
|
225
250
|
code: "custom",
|
|
226
|
-
message: "
|
|
251
|
+
message: "resource cannot use javascript:, data:, or vbscript: scheme"
|
|
227
252
|
});
|
|
228
253
|
});
|
|
229
254
|
//#endregion
|
|
@@ -312,13 +337,13 @@ async function tokenEndpoint(ctx, opts) {
|
|
|
312
337
|
case "refresh_token": return handleRefreshTokenGrant(ctx, opts);
|
|
313
338
|
}
|
|
314
339
|
}
|
|
315
|
-
async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, overrides) {
|
|
340
|
+
async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, resources, referenceId, overrides) {
|
|
316
341
|
const iat = overrides?.iat ?? Math.floor(Date.now() / 1e3);
|
|
317
342
|
const exp = overrides?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
|
|
318
343
|
const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
|
|
319
344
|
user,
|
|
320
345
|
scopes,
|
|
321
|
-
|
|
346
|
+
resources,
|
|
322
347
|
referenceId,
|
|
323
348
|
metadata: parseClientMetadata(client.metadata)
|
|
324
349
|
}) : {};
|
|
@@ -328,7 +353,7 @@ async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, r
|
|
|
328
353
|
payload: {
|
|
329
354
|
...customClaims,
|
|
330
355
|
sub: user?.id,
|
|
331
|
-
aud:
|
|
356
|
+
aud: toAudienceClaim(audience),
|
|
332
357
|
azp: client.clientId,
|
|
333
358
|
scope: scopes.join(" "),
|
|
334
359
|
sid: overrides?.sid,
|
|
@@ -419,7 +444,7 @@ async function decodeRefreshToken(opts, token) {
|
|
|
419
444
|
});
|
|
420
445
|
return opts.formatRefreshToken?.decrypt ? opts.formatRefreshToken?.decrypt(token) : { token };
|
|
421
446
|
}
|
|
422
|
-
async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, referenceId, refreshId) {
|
|
447
|
+
async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, refreshId) {
|
|
423
448
|
const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
|
|
424
449
|
const exp = payload?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
|
|
425
450
|
const token = opts.generateOpaqueAccessToken ? await opts.generateOpaqueAccessToken() : generateRandomString(32, "A-Z", "a-z");
|
|
@@ -431,6 +456,7 @@ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload,
|
|
|
431
456
|
sessionId: payload?.sid,
|
|
432
457
|
userId: user?.id,
|
|
433
458
|
referenceId,
|
|
459
|
+
resources,
|
|
434
460
|
refreshId,
|
|
435
461
|
scopes,
|
|
436
462
|
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
@@ -439,54 +465,100 @@ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload,
|
|
|
439
465
|
});
|
|
440
466
|
return (opts.prefix?.opaqueAccessToken ?? "") + token;
|
|
441
467
|
}
|
|
442
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Tear down the entire refresh-token family for a (client, user) pair, plus
|
|
470
|
+
* any access tokens that reference those refresh rows, per RFC 9700 §4.14.
|
|
471
|
+
* Access tokens are deleted first so the parent rows' foreign-key children
|
|
472
|
+
* do not block the refresh-row delete.
|
|
473
|
+
*
|
|
474
|
+
* TODO(invalidate-family-race): the two `deleteMany` calls are not atomic
|
|
475
|
+
* with respect to each other. Between them, a concurrent rotation in a
|
|
476
|
+
* different worker can `create` a fresh refresh row (and, immediately after,
|
|
477
|
+
* an access-token row referencing it) for the same (client, user) pair,
|
|
478
|
+
* leaving the family partially rebuilt and the new refresh row orphaned of
|
|
479
|
+
* any deletion. Closing this window requires the same transactional adapter
|
|
480
|
+
* contract tracked under FIXME(strict-family-invalidation) in
|
|
481
|
+
* `createRefreshToken`.
|
|
482
|
+
*
|
|
483
|
+
* @internal
|
|
484
|
+
*/
|
|
485
|
+
async function invalidateRefreshFamily(ctx, clientId, userId) {
|
|
486
|
+
const refreshTokens = await ctx.context.adapter.findMany({
|
|
487
|
+
model: "oauthRefreshToken",
|
|
488
|
+
where: [{
|
|
489
|
+
field: "clientId",
|
|
490
|
+
value: clientId
|
|
491
|
+
}, {
|
|
492
|
+
field: "userId",
|
|
493
|
+
value: userId
|
|
494
|
+
}]
|
|
495
|
+
});
|
|
496
|
+
if (refreshTokens.length) await ctx.context.adapter.deleteMany({
|
|
497
|
+
model: "oauthAccessToken",
|
|
498
|
+
where: [{
|
|
499
|
+
field: "refreshId",
|
|
500
|
+
operator: "in",
|
|
501
|
+
value: refreshTokens.map((r) => r.id)
|
|
502
|
+
}]
|
|
503
|
+
});
|
|
504
|
+
await ctx.context.adapter.deleteMany({
|
|
505
|
+
model: "oauthRefreshToken",
|
|
506
|
+
where: [{
|
|
507
|
+
field: "clientId",
|
|
508
|
+
value: clientId
|
|
509
|
+
}, {
|
|
510
|
+
field: "userId",
|
|
511
|
+
value: userId
|
|
512
|
+
}]
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, payload, originalRefresh, authTime, resources) {
|
|
443
516
|
const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
|
|
444
517
|
const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
|
|
445
518
|
const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
|
|
446
519
|
const sessionId = payload?.sid;
|
|
447
|
-
|
|
520
|
+
const newRow = {
|
|
521
|
+
token: await storeToken(opts.storeTokens, token, "refresh_token"),
|
|
522
|
+
clientId: client.clientId,
|
|
523
|
+
sessionId,
|
|
524
|
+
userId: user.id,
|
|
525
|
+
referenceId,
|
|
526
|
+
authTime,
|
|
527
|
+
scopes,
|
|
528
|
+
resources,
|
|
529
|
+
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
530
|
+
expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
|
|
531
|
+
};
|
|
532
|
+
if (!originalRefresh?.id) return {
|
|
533
|
+
id: (await ctx.context.adapter.create({
|
|
534
|
+
model: "oauthRefreshToken",
|
|
535
|
+
data: newRow
|
|
536
|
+
})).id,
|
|
537
|
+
token: await encodeRefreshToken(opts, token, sessionId)
|
|
538
|
+
};
|
|
539
|
+
if (!await ctx.context.adapter.update({
|
|
448
540
|
model: "oauthRefreshToken",
|
|
449
541
|
where: [{
|
|
450
542
|
field: "id",
|
|
451
543
|
value: originalRefresh.id
|
|
544
|
+
}, {
|
|
545
|
+
field: "revoked",
|
|
546
|
+
operator: "eq",
|
|
547
|
+
value: null
|
|
452
548
|
}],
|
|
453
549
|
update: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
|
|
550
|
+
})) throw new APIError("BAD_REQUEST", {
|
|
551
|
+
error_description: "invalid refresh token",
|
|
552
|
+
error: "invalid_grant"
|
|
454
553
|
});
|
|
455
554
|
return {
|
|
456
555
|
id: (await ctx.context.adapter.create({
|
|
457
556
|
model: "oauthRefreshToken",
|
|
458
|
-
data:
|
|
459
|
-
token: await storeToken(opts.storeTokens, token, "refresh_token"),
|
|
460
|
-
clientId: client.clientId,
|
|
461
|
-
sessionId,
|
|
462
|
-
userId: user.id,
|
|
463
|
-
referenceId,
|
|
464
|
-
authTime,
|
|
465
|
-
scopes,
|
|
466
|
-
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
467
|
-
expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
|
|
468
|
-
}
|
|
557
|
+
data: newRow
|
|
469
558
|
})).id,
|
|
470
559
|
token: await encodeRefreshToken(opts, token, sessionId)
|
|
471
560
|
};
|
|
472
561
|
}
|
|
473
|
-
/**
|
|
474
|
-
* Checks the resource parameter, if provided,
|
|
475
|
-
* and returns a valid audience based on the request
|
|
476
|
-
*/
|
|
477
|
-
async function checkResource(ctx, opts, scopes) {
|
|
478
|
-
const resource = ctx.body.resource;
|
|
479
|
-
const audience = typeof resource === "string" ? [resource] : resource ? [...resource] : void 0;
|
|
480
|
-
if (audience) {
|
|
481
|
-
if (scopes.includes("openid")) audience.push(`${ctx.context.baseURL}/oauth2/userinfo`);
|
|
482
|
-
const validAudiences = new Set([...opts.validAudiences ?? [ctx.context.baseURL], scopes?.includes("openid") ? `${ctx.context.baseURL}/oauth2/userinfo` : void 0].flat().filter((v) => v?.length));
|
|
483
|
-
for (const aud of audience) if (!validAudiences.has(aud)) throw new APIError("BAD_REQUEST", {
|
|
484
|
-
error_description: "requested resource invalid",
|
|
485
|
-
error: "invalid_request"
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
return audience?.length === 1 ? audience.at(0) : audience;
|
|
489
|
-
}
|
|
490
562
|
async function createUserTokens(ctx, opts, params) {
|
|
491
563
|
const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, verificationValue } = params;
|
|
492
564
|
const iat = Math.floor(Date.now() / 1e3);
|
|
@@ -494,7 +566,12 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
494
566
|
const exp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
|
|
495
567
|
return prev < curr ? prev : curr;
|
|
496
568
|
}, defaultExp) : defaultExp;
|
|
497
|
-
const
|
|
569
|
+
const resourceResult = await checkResource(ctx, opts, params?.resources, scopes);
|
|
570
|
+
if (!resourceResult.success) throw new APIError("BAD_REQUEST", {
|
|
571
|
+
error_description: "requested resource invalid",
|
|
572
|
+
error: "invalid_target"
|
|
573
|
+
});
|
|
574
|
+
const audience = resourceResult.audience;
|
|
498
575
|
const isRefreshToken = user && (existingRefreshToken?.scopes?.includes("offline_access") || scopes.includes("offline_access"));
|
|
499
576
|
const isJwtAccessToken = audience && !opts.disableJwtPlugin;
|
|
500
577
|
const isIdToken = user && scopes.includes("openid");
|
|
@@ -505,12 +582,13 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
505
582
|
metadata: parseClientMetadata(client.metadata),
|
|
506
583
|
verificationValue
|
|
507
584
|
}) : void 0;
|
|
585
|
+
const refreshResources = params?.refreshToken?.resources ?? params?.originalResources ?? params?.resources;
|
|
508
586
|
const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
|
|
509
587
|
iat,
|
|
510
588
|
exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
|
|
511
589
|
sid: sessionId
|
|
512
|
-
}, existingRefreshToken, authTime) : void 0;
|
|
513
|
-
const [accessToken, refreshToken] = await Promise.all([isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, {
|
|
590
|
+
}, existingRefreshToken, authTime, refreshResources) : void 0;
|
|
591
|
+
const [accessToken, refreshToken] = await Promise.all([isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, params?.resources, referenceId, {
|
|
514
592
|
iat,
|
|
515
593
|
exp,
|
|
516
594
|
sid: sessionId
|
|
@@ -518,11 +596,11 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
518
596
|
iat,
|
|
519
597
|
exp,
|
|
520
598
|
sid: sessionId
|
|
521
|
-
}, referenceId, earlyRefreshToken?.id), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
|
|
599
|
+
}, params?.resources, referenceId, earlyRefreshToken?.id), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
|
|
522
600
|
iat,
|
|
523
601
|
exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
|
|
524
602
|
sid: sessionId
|
|
525
|
-
}, existingRefreshToken, authTime) : void 0]);
|
|
603
|
+
}, existingRefreshToken, authTime, refreshResources) : void 0]);
|
|
526
604
|
const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) : void 0;
|
|
527
605
|
return ctx.json({
|
|
528
606
|
...customFields,
|
|
@@ -539,16 +617,11 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
539
617
|
} });
|
|
540
618
|
}
|
|
541
619
|
/** Checks verification value */
|
|
542
|
-
async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri) {
|
|
543
|
-
const verification = await ctx.context.internalAdapter.
|
|
620
|
+
async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resource) {
|
|
621
|
+
const verification = await ctx.context.internalAdapter.consumeVerificationValue(await storeToken(opts.storeTokens, code, "authorization_code"));
|
|
544
622
|
if (!verification) throw new APIError("UNAUTHORIZED", {
|
|
545
|
-
error_description: "
|
|
546
|
-
error: "
|
|
547
|
-
});
|
|
548
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(await storeToken(opts.storeTokens, code, "authorization_code"));
|
|
549
|
-
if (!verification.expiresAt || verification.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("UNAUTHORIZED", {
|
|
550
|
-
error_description: "code expired",
|
|
551
|
-
error: "invalid_verification"
|
|
623
|
+
error_description: "invalid code",
|
|
624
|
+
error: "invalid_grant"
|
|
552
625
|
});
|
|
553
626
|
let rawValue;
|
|
554
627
|
try {
|
|
@@ -556,13 +629,13 @@ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri)
|
|
|
556
629
|
} catch {
|
|
557
630
|
throw new APIError("UNAUTHORIZED", {
|
|
558
631
|
error_description: "malformed verification value",
|
|
559
|
-
error: "
|
|
632
|
+
error: "invalid_grant"
|
|
560
633
|
});
|
|
561
634
|
}
|
|
562
635
|
const parsed = verificationValueSchema.safeParse(rawValue);
|
|
563
636
|
if (!parsed.success) throw new APIError("UNAUTHORIZED", {
|
|
564
637
|
error_description: "malformed verification value",
|
|
565
|
-
error: "
|
|
638
|
+
error: "invalid_grant"
|
|
566
639
|
});
|
|
567
640
|
const verificationValue = parsed.data;
|
|
568
641
|
if (verificationValue.query.client_id !== client_id) throw new APIError("UNAUTHORIZED", {
|
|
@@ -573,14 +646,29 @@ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri)
|
|
|
573
646
|
error_description: "redirect_uri mismatch",
|
|
574
647
|
error: "invalid_request"
|
|
575
648
|
});
|
|
576
|
-
|
|
649
|
+
const storedResources = toResourceList(verificationValue.resource) ?? toResourceList(verificationValue.query.resource);
|
|
650
|
+
const effectiveResources = resource ?? storedResources;
|
|
651
|
+
if (resource && storedResources) {
|
|
652
|
+
const requestedSet = new Set(resource);
|
|
653
|
+
const authorizedSet = new Set(storedResources);
|
|
654
|
+
for (const r of requestedSet) if (!authorizedSet.has(r)) throw new APIError("BAD_REQUEST", {
|
|
655
|
+
error_description: "requested resource not authorized",
|
|
656
|
+
error: "invalid_target"
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
verificationValue,
|
|
661
|
+
effectiveResources,
|
|
662
|
+
authorizedResources: storedResources
|
|
663
|
+
};
|
|
577
664
|
}
|
|
578
665
|
/**
|
|
579
666
|
* Obtains new Session Jwt and Refresh Tokens using a code
|
|
580
667
|
*/
|
|
581
668
|
async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
582
669
|
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
|
|
583
|
-
const { code, code_verifier, redirect_uri } = ctx.body;
|
|
670
|
+
const { code, code_verifier, redirect_uri, resource } = ctx.body;
|
|
671
|
+
const resources = toResourceList(resource);
|
|
584
672
|
if (!client_id) throw new APIError("BAD_REQUEST", {
|
|
585
673
|
error_description: "client_id is required",
|
|
586
674
|
error: "invalid_request"
|
|
@@ -600,7 +688,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
600
688
|
error: "invalid_request"
|
|
601
689
|
});
|
|
602
690
|
/** Get and check Verification Value */
|
|
603
|
-
const verificationValue = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri);
|
|
691
|
+
const { verificationValue, effectiveResources, authorizedResources } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
|
|
604
692
|
const scopes = verificationValue.query.scope?.split(" ");
|
|
605
693
|
if (!scopes) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
606
694
|
error_description: "verification scope unset",
|
|
@@ -665,7 +753,9 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
665
753
|
sessionId: session.id,
|
|
666
754
|
nonce: verificationValue.query?.nonce,
|
|
667
755
|
authTime,
|
|
668
|
-
verificationValue
|
|
756
|
+
verificationValue,
|
|
757
|
+
resources: effectiveResources,
|
|
758
|
+
originalResources: authorizedResources
|
|
669
759
|
});
|
|
670
760
|
}
|
|
671
761
|
/**
|
|
@@ -676,7 +766,8 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
676
766
|
*/
|
|
677
767
|
async function handleClientCredentialsGrant(ctx, opts) {
|
|
678
768
|
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
|
|
679
|
-
const { scope } = ctx.body;
|
|
769
|
+
const { scope, resource } = ctx.body;
|
|
770
|
+
const resources = toResourceList(resource);
|
|
680
771
|
if (!client_id) throw new APIError("BAD_REQUEST", {
|
|
681
772
|
error_description: "Missing required client_id",
|
|
682
773
|
error: "invalid_grant"
|
|
@@ -707,7 +798,8 @@ async function handleClientCredentialsGrant(ctx, opts) {
|
|
|
707
798
|
return createUserTokens(ctx, opts, {
|
|
708
799
|
client,
|
|
709
800
|
scopes: requestedScopes,
|
|
710
|
-
grantType: "client_credentials"
|
|
801
|
+
grantType: "client_credentials",
|
|
802
|
+
resources
|
|
711
803
|
});
|
|
712
804
|
}
|
|
713
805
|
/**
|
|
@@ -718,7 +810,8 @@ async function handleClientCredentialsGrant(ctx, opts) {
|
|
|
718
810
|
*/
|
|
719
811
|
async function handleRefreshTokenGrant(ctx, opts) {
|
|
720
812
|
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
|
|
721
|
-
const { refresh_token, scope } = ctx.body;
|
|
813
|
+
const { refresh_token, scope, resource } = ctx.body;
|
|
814
|
+
const resources = toResourceList(resource);
|
|
722
815
|
if (!client_id) throw new APIError("BAD_REQUEST", {
|
|
723
816
|
error_description: "Missing required client_id",
|
|
724
817
|
error: "invalid_grant"
|
|
@@ -748,21 +841,16 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
748
841
|
error: "invalid_grant"
|
|
749
842
|
});
|
|
750
843
|
if (refreshToken.revoked) {
|
|
751
|
-
await ctx.
|
|
752
|
-
model: "oauthRefreshToken",
|
|
753
|
-
where: [{
|
|
754
|
-
field: "clientId",
|
|
755
|
-
value: client_id
|
|
756
|
-
}, {
|
|
757
|
-
field: "userId",
|
|
758
|
-
value: refreshToken.userId
|
|
759
|
-
}]
|
|
760
|
-
});
|
|
844
|
+
await invalidateRefreshFamily(ctx, client_id, refreshToken.userId);
|
|
761
845
|
throw new APIError("BAD_REQUEST", {
|
|
762
846
|
error_description: "invalid refresh token",
|
|
763
847
|
error: "invalid_grant"
|
|
764
848
|
});
|
|
765
849
|
}
|
|
850
|
+
if (resources && refreshToken.resources && !resources.every((v) => refreshToken.resources?.includes(v))) throw new APIError("BAD_REQUEST", {
|
|
851
|
+
error_description: "requested resource invalid",
|
|
852
|
+
error: "invalid_target"
|
|
853
|
+
});
|
|
766
854
|
const scopes = refreshToken?.scopes;
|
|
767
855
|
const requestedScopes = scope?.split(" ");
|
|
768
856
|
if (requestedScopes) {
|
|
@@ -787,6 +875,7 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
787
875
|
referenceId: refreshToken.referenceId,
|
|
788
876
|
sessionId: refreshToken.sessionId,
|
|
789
877
|
refreshToken,
|
|
878
|
+
resources: resources ?? refreshToken.resources,
|
|
790
879
|
authTime
|
|
791
880
|
});
|
|
792
881
|
}
|
|
@@ -894,10 +983,17 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
894
983
|
}
|
|
895
984
|
let user;
|
|
896
985
|
if (accessToken.userId) user = await ctx.context.internalAdapter.findUserById(accessToken?.userId);
|
|
986
|
+
const resources = Array.isArray(accessToken.resources) ? accessToken.resources : void 0;
|
|
987
|
+
const audience = resources ? [...resources] : void 0;
|
|
988
|
+
if (audience?.length && accessToken.scopes?.includes("openid")) {
|
|
989
|
+
const userInfoEndpoint = `${ctx.context.baseURL}/oauth2/userinfo`;
|
|
990
|
+
if (!audience.includes(userInfoEndpoint)) audience.push(userInfoEndpoint);
|
|
991
|
+
}
|
|
897
992
|
const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
|
|
898
993
|
user,
|
|
899
994
|
scopes: accessToken.scopes,
|
|
900
995
|
referenceId: accessToken?.referenceId,
|
|
996
|
+
resources,
|
|
901
997
|
metadata: parseClientMetadata(client?.metadata)
|
|
902
998
|
}) : {};
|
|
903
999
|
const jwtPluginOptions = (opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options;
|
|
@@ -905,6 +1001,7 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
905
1001
|
...customClaims,
|
|
906
1002
|
active: true,
|
|
907
1003
|
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
1004
|
+
aud: toAudienceClaim(audience),
|
|
908
1005
|
client_id: accessToken.clientId,
|
|
909
1006
|
sub: user?.id,
|
|
910
1007
|
sid: sessionId,
|
|
@@ -939,7 +1036,7 @@ async function validateRefreshToken(ctx, opts, token, clientId) {
|
|
|
939
1036
|
model: "session",
|
|
940
1037
|
where: [{
|
|
941
1038
|
field: "id",
|
|
942
|
-
value:
|
|
1039
|
+
value: sessionId
|
|
943
1040
|
}]
|
|
944
1041
|
});
|
|
945
1042
|
if (!session || session.expiresAt < /* @__PURE__ */ new Date()) sessionId = void 0;
|
|
@@ -1268,6 +1365,28 @@ const publicSessionMiddleware = (opts) => createAuthMiddleware(async (ctx) => {
|
|
|
1268
1365
|
if (!await verifyOAuthQueryParams(query, ctx.context.secret)) throw new APIError("UNAUTHORIZED", { error: "invalid_signature" });
|
|
1269
1366
|
});
|
|
1270
1367
|
//#endregion
|
|
1368
|
+
//#region src/oauthClient/privileges.ts
|
|
1369
|
+
/**
|
|
1370
|
+
* Authorizes a client action against the configured `clientPrivileges` hook.
|
|
1371
|
+
*
|
|
1372
|
+
* This is the single authorization helper for every OAuth client mutation. The
|
|
1373
|
+
* create path enforces it at the shared creation chokepoint so that no
|
|
1374
|
+
* registration route can reach client persistence without it.
|
|
1375
|
+
*
|
|
1376
|
+
* @throws APIError UNAUTHORIZED when there is no session or the hook denies the action.
|
|
1377
|
+
* @throws APIError BAD_REQUEST when the request carries no headers.
|
|
1378
|
+
*/
|
|
1379
|
+
async function assertClientPrivileges(ctx, session, opts, action) {
|
|
1380
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1381
|
+
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1382
|
+
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
1383
|
+
headers: ctx.headers,
|
|
1384
|
+
action,
|
|
1385
|
+
session: session.session,
|
|
1386
|
+
user: session.user
|
|
1387
|
+
})) throw new APIError("UNAUTHORIZED");
|
|
1388
|
+
}
|
|
1389
|
+
//#endregion
|
|
1271
1390
|
//#region src/register.ts
|
|
1272
1391
|
/**
|
|
1273
1392
|
* Resolves the auth method and type for unauthenticated DCR.
|
|
@@ -1403,6 +1522,9 @@ async function checkOAuthClient(client, opts, settings) {
|
|
|
1403
1522
|
async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
1404
1523
|
const body = ctx.body;
|
|
1405
1524
|
const session = await getSessionFromCtx(ctx);
|
|
1525
|
+
if (settings.isRegister) {
|
|
1526
|
+
if (session) await assertClientPrivileges(ctx, session, opts, "create");
|
|
1527
|
+
} else await assertClientPrivileges(ctx, session, opts, "create");
|
|
1406
1528
|
const isPublic = body.token_endpoint_auth_method === "none";
|
|
1407
1529
|
const isPrivateKeyJwt = body.token_endpoint_auth_method === "private_key_jwt";
|
|
1408
1530
|
await checkOAuthClient(ctx.body, opts, {
|
|
@@ -1748,16 +1870,6 @@ async function rotateClientSecretEndpoint(ctx, opts) {
|
|
|
1748
1870
|
clientSecret: (opts.prefix?.clientSecret ?? "") + clientSecret
|
|
1749
1871
|
});
|
|
1750
1872
|
}
|
|
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
|
-
}
|
|
1761
1873
|
//#endregion
|
|
1762
1874
|
//#region src/oauthClient/index.ts
|
|
1763
1875
|
const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/create-client", {
|
|
@@ -1781,7 +1893,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
1781
1893
|
"client_secret_post",
|
|
1782
1894
|
"private_key_jwt"
|
|
1783
1895
|
]).default("client_secret_basic").optional(),
|
|
1784
|
-
jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
|
|
1896
|
+
jwks: z.union([z.array(z.record(z.string(), z.unknown())).min(1), z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) })]).optional(),
|
|
1785
1897
|
jwks_uri: z.string().optional(),
|
|
1786
1898
|
grant_types: z.array(z.enum([
|
|
1787
1899
|
"authorization_code",
|
|
@@ -1943,7 +2055,6 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
1943
2055
|
}
|
|
1944
2056
|
}
|
|
1945
2057
|
}, async (ctx) => {
|
|
1946
|
-
await assertClientPrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
|
|
1947
2058
|
return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
|
|
1948
2059
|
});
|
|
1949
2060
|
const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client", {
|
|
@@ -1968,7 +2079,7 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
|
|
|
1968
2079
|
"client_secret_post",
|
|
1969
2080
|
"private_key_jwt"
|
|
1970
2081
|
]).default("client_secret_basic").optional(),
|
|
1971
|
-
jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
|
|
2082
|
+
jwks: z.union([z.array(z.record(z.string(), z.unknown())).min(1), z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) })]).optional(),
|
|
1972
2083
|
jwks_uri: z.string().optional(),
|
|
1973
2084
|
grant_types: z.array(z.enum([
|
|
1974
2085
|
"authorization_code",
|
|
@@ -2116,7 +2227,6 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
|
|
|
2116
2227
|
} }
|
|
2117
2228
|
} }
|
|
2118
2229
|
}, async (ctx) => {
|
|
2119
|
-
await assertClientPrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
|
|
2120
2230
|
return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
|
|
2121
2231
|
});
|
|
2122
2232
|
const getOAuthClient = (opts) => createAuthEndpoint("/oauth2/get-client", {
|
|
@@ -2320,12 +2430,12 @@ async function updateConsentEndpoint(ctx, opts) {
|
|
|
2320
2430
|
error_description: "no consent",
|
|
2321
2431
|
error: "not_found"
|
|
2322
2432
|
});
|
|
2433
|
+
if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
|
|
2323
2434
|
const client = await getClient(ctx, opts, consent.clientId);
|
|
2324
|
-
if (!
|
|
2325
|
-
error_description: "
|
|
2435
|
+
if (!client) throw new APIError("NOT_FOUND", {
|
|
2436
|
+
error_description: "client not found",
|
|
2326
2437
|
error: "not_found"
|
|
2327
2438
|
});
|
|
2328
|
-
if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
|
|
2329
2439
|
const allowedScopes = client?.scopes ?? opts.scopes ?? [];
|
|
2330
2440
|
const updates = ctx.body.update;
|
|
2331
2441
|
const scopes = updates.scopes;
|
|
@@ -2473,16 +2583,7 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
|
|
|
2473
2583
|
error: "invalid_request"
|
|
2474
2584
|
});
|
|
2475
2585
|
if (refreshToken.revoked) {
|
|
2476
|
-
await ctx.
|
|
2477
|
-
model: "oauthRefreshToken",
|
|
2478
|
-
where: [{
|
|
2479
|
-
field: "clientId",
|
|
2480
|
-
value: clientId
|
|
2481
|
-
}, {
|
|
2482
|
-
field: "userId",
|
|
2483
|
-
value: refreshToken.userId
|
|
2484
|
-
}]
|
|
2485
|
-
});
|
|
2586
|
+
await invalidateRefreshFamily(ctx, clientId, refreshToken.userId);
|
|
2486
2587
|
throw new APIError$1("BAD_REQUEST", {
|
|
2487
2588
|
error_description: "refresh token revoked",
|
|
2488
2589
|
error: "invalid_request"
|
|
@@ -2490,20 +2591,31 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
|
|
|
2490
2591
|
}
|
|
2491
2592
|
if (!refreshToken.clientId || refreshToken.clientId !== clientId) return null;
|
|
2492
2593
|
const iat = Math.floor(Date.now() / 1e3);
|
|
2493
|
-
await
|
|
2494
|
-
model: "oauthAccessToken",
|
|
2495
|
-
where: [{
|
|
2496
|
-
field: "refreshId",
|
|
2497
|
-
value: refreshToken.id
|
|
2498
|
-
}]
|
|
2499
|
-
}), ctx.context.adapter.update({
|
|
2594
|
+
if (!await ctx.context.adapter.update({
|
|
2500
2595
|
model: "oauthRefreshToken",
|
|
2501
2596
|
where: [{
|
|
2502
2597
|
field: "id",
|
|
2503
2598
|
value: refreshToken.id
|
|
2599
|
+
}, {
|
|
2600
|
+
field: "revoked",
|
|
2601
|
+
operator: "eq",
|
|
2602
|
+
value: null
|
|
2504
2603
|
}],
|
|
2505
2604
|
update: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
|
|
2506
|
-
})
|
|
2605
|
+
})) {
|
|
2606
|
+
await invalidateRefreshFamily(ctx, clientId, refreshToken.userId);
|
|
2607
|
+
throw new APIError$1("BAD_REQUEST", {
|
|
2608
|
+
error_description: "refresh token revoked",
|
|
2609
|
+
error: "invalid_request"
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
await ctx.context.adapter.deleteMany({
|
|
2613
|
+
model: "oauthAccessToken",
|
|
2614
|
+
where: [{
|
|
2615
|
+
field: "refreshId",
|
|
2616
|
+
value: refreshToken.id
|
|
2617
|
+
}]
|
|
2618
|
+
});
|
|
2507
2619
|
}
|
|
2508
2620
|
/**
|
|
2509
2621
|
* We don't know the access token format so we try to validate it
|
|
@@ -2617,7 +2729,8 @@ const schema = {
|
|
|
2617
2729
|
references: {
|
|
2618
2730
|
model: "user",
|
|
2619
2731
|
field: "id"
|
|
2620
|
-
}
|
|
2732
|
+
},
|
|
2733
|
+
index: true
|
|
2621
2734
|
},
|
|
2622
2735
|
createdAt: {
|
|
2623
2736
|
type: "date",
|
|
@@ -2716,7 +2829,8 @@ const schema = {
|
|
|
2716
2829
|
oauthRefreshToken: { fields: {
|
|
2717
2830
|
token: {
|
|
2718
2831
|
type: "string",
|
|
2719
|
-
required: true
|
|
2832
|
+
required: true,
|
|
2833
|
+
unique: true
|
|
2720
2834
|
},
|
|
2721
2835
|
clientId: {
|
|
2722
2836
|
type: "string",
|
|
@@ -2724,7 +2838,8 @@ const schema = {
|
|
|
2724
2838
|
references: {
|
|
2725
2839
|
model: "oauthClient",
|
|
2726
2840
|
field: "clientId"
|
|
2727
|
-
}
|
|
2841
|
+
},
|
|
2842
|
+
index: true
|
|
2728
2843
|
},
|
|
2729
2844
|
sessionId: {
|
|
2730
2845
|
type: "string",
|
|
@@ -2733,7 +2848,8 @@ const schema = {
|
|
|
2733
2848
|
model: "session",
|
|
2734
2849
|
field: "id",
|
|
2735
2850
|
onDelete: "set null"
|
|
2736
|
-
}
|
|
2851
|
+
},
|
|
2852
|
+
index: true
|
|
2737
2853
|
},
|
|
2738
2854
|
userId: {
|
|
2739
2855
|
type: "string",
|
|
@@ -2741,12 +2857,17 @@ const schema = {
|
|
|
2741
2857
|
references: {
|
|
2742
2858
|
model: "user",
|
|
2743
2859
|
field: "id"
|
|
2744
|
-
}
|
|
2860
|
+
},
|
|
2861
|
+
index: true
|
|
2745
2862
|
},
|
|
2746
2863
|
referenceId: {
|
|
2747
2864
|
type: "string",
|
|
2748
2865
|
required: false
|
|
2749
2866
|
},
|
|
2867
|
+
resources: {
|
|
2868
|
+
type: "string[]",
|
|
2869
|
+
required: false
|
|
2870
|
+
},
|
|
2750
2871
|
expiresAt: { type: "date" },
|
|
2751
2872
|
createdAt: { type: "date" },
|
|
2752
2873
|
revoked: {
|
|
@@ -2775,7 +2896,8 @@ const schema = {
|
|
|
2775
2896
|
references: {
|
|
2776
2897
|
model: "oauthClient",
|
|
2777
2898
|
field: "clientId"
|
|
2778
|
-
}
|
|
2899
|
+
},
|
|
2900
|
+
index: true
|
|
2779
2901
|
},
|
|
2780
2902
|
sessionId: {
|
|
2781
2903
|
type: "string",
|
|
@@ -2784,7 +2906,8 @@ const schema = {
|
|
|
2784
2906
|
model: "session",
|
|
2785
2907
|
field: "id",
|
|
2786
2908
|
onDelete: "set null"
|
|
2787
|
-
}
|
|
2909
|
+
},
|
|
2910
|
+
index: true
|
|
2788
2911
|
},
|
|
2789
2912
|
userId: {
|
|
2790
2913
|
type: "string",
|
|
@@ -2792,19 +2915,25 @@ const schema = {
|
|
|
2792
2915
|
references: {
|
|
2793
2916
|
model: "user",
|
|
2794
2917
|
field: "id"
|
|
2795
|
-
}
|
|
2918
|
+
},
|
|
2919
|
+
index: true
|
|
2796
2920
|
},
|
|
2797
2921
|
referenceId: {
|
|
2798
2922
|
type: "string",
|
|
2799
2923
|
required: false
|
|
2800
2924
|
},
|
|
2925
|
+
resources: {
|
|
2926
|
+
type: "string[]",
|
|
2927
|
+
required: false
|
|
2928
|
+
},
|
|
2801
2929
|
refreshId: {
|
|
2802
2930
|
type: "string",
|
|
2803
2931
|
required: false,
|
|
2804
2932
|
references: {
|
|
2805
2933
|
model: "oauthRefreshToken",
|
|
2806
2934
|
field: "id"
|
|
2807
|
-
}
|
|
2935
|
+
},
|
|
2936
|
+
index: true
|
|
2808
2937
|
},
|
|
2809
2938
|
expiresAt: { type: "date" },
|
|
2810
2939
|
createdAt: { type: "date" },
|
|
@@ -2823,7 +2952,8 @@ const schema = {
|
|
|
2823
2952
|
references: {
|
|
2824
2953
|
model: "oauthClient",
|
|
2825
2954
|
field: "clientId"
|
|
2826
|
-
}
|
|
2955
|
+
},
|
|
2956
|
+
index: true
|
|
2827
2957
|
},
|
|
2828
2958
|
userId: {
|
|
2829
2959
|
type: "string",
|
|
@@ -2831,12 +2961,17 @@ const schema = {
|
|
|
2831
2961
|
references: {
|
|
2832
2962
|
model: "user",
|
|
2833
2963
|
field: "id"
|
|
2834
|
-
}
|
|
2964
|
+
},
|
|
2965
|
+
index: true
|
|
2835
2966
|
},
|
|
2836
2967
|
referenceId: {
|
|
2837
2968
|
type: "string",
|
|
2838
2969
|
required: false
|
|
2839
2970
|
},
|
|
2971
|
+
resources: {
|
|
2972
|
+
type: "string[]",
|
|
2973
|
+
required: false
|
|
2974
|
+
},
|
|
2840
2975
|
scopes: {
|
|
2841
2976
|
type: "string[]",
|
|
2842
2977
|
required: true
|
|
@@ -2914,10 +3049,55 @@ const oauthProvider = (options) => {
|
|
|
2914
3049
|
if (opts.grantTypes && opts.grantTypes.includes("refresh_token") && !opts.grantTypes.includes("authorization_code")) throw new BetterAuthError("refresh_token grant requires authorization_code grant");
|
|
2915
3050
|
if (opts.disableJwtPlugin && (opts.storeClientSecret === "hashed" || typeof opts.storeClientSecret === "object" && "hash" in opts.storeClientSecret)) throw new BetterAuthError("unable to store hashed secrets because id tokens will be signed with secret");
|
|
2916
3051
|
if (!opts.disableJwtPlugin && (opts.storeClientSecret === "encrypted" || typeof opts.storeClientSecret === "object" && ("encrypt" in opts.storeClientSecret || "decrypt" in opts.storeClientSecret))) throw new BetterAuthError("encryption method not recommended, please use 'hashed' or the 'hash' function");
|
|
3052
|
+
const handleIssuerMetadataRequest = async (request, ctx) => {
|
|
3053
|
+
const requestPathname = new URL(request.url).pathname;
|
|
3054
|
+
const requestPath = ctx.options.advanced?.skipTrailingSlashes ? requestPathname.replace(/\/+$/, "") || "/" : requestPathname;
|
|
3055
|
+
const issuer = opts.disableJwtPlugin ? ctx.baseURL : getJwtPlugin(ctx)?.options?.jwt?.issuer ?? ctx.baseURL;
|
|
3056
|
+
let issuerPath = "/";
|
|
3057
|
+
try {
|
|
3058
|
+
issuerPath = new URL(issuer).pathname.replace(/\/$/, "") || "";
|
|
3059
|
+
} catch {
|
|
3060
|
+
issuerPath = new URL(ctx.baseURL).pathname.replace(/\/$/, "") || "";
|
|
3061
|
+
}
|
|
3062
|
+
const endpointCtx = { context: ctx };
|
|
3063
|
+
const authServerMetadataPaths = new Set([`/.well-known/oauth-authorization-server${issuerPath}`, `${issuerPath}/.well-known/oauth-authorization-server`]);
|
|
3064
|
+
const openIdConfigPath = `${issuerPath}/.well-known/openid-configuration`;
|
|
3065
|
+
const isAuthServerMetadataRequest = authServerMetadataPaths.has(requestPath);
|
|
3066
|
+
const isOpenIdConfigRequest = opts.scopes?.includes("openid") && requestPath === openIdConfigPath;
|
|
3067
|
+
const createMetadataResponse = (metadata) => {
|
|
3068
|
+
const response = metadataResponse(metadata);
|
|
3069
|
+
if (request.method === "HEAD") return new Response(null, {
|
|
3070
|
+
status: response.status,
|
|
3071
|
+
headers: response.headers
|
|
3072
|
+
});
|
|
3073
|
+
return response;
|
|
3074
|
+
};
|
|
3075
|
+
if (isAuthServerMetadataRequest || isOpenIdConfigRequest) {
|
|
3076
|
+
if (request.method !== "GET" && request.method !== "HEAD") return { response: new Response(null, {
|
|
3077
|
+
status: 405,
|
|
3078
|
+
headers: { Allow: "GET, HEAD" }
|
|
3079
|
+
}) };
|
|
3080
|
+
}
|
|
3081
|
+
if (isAuthServerMetadataRequest) {
|
|
3082
|
+
if (opts.scopes?.includes("openid")) return { response: createMetadataResponse(oidcServerMetadata(endpointCtx, opts)) };
|
|
3083
|
+
return { response: createMetadataResponse({
|
|
3084
|
+
...authServerMetadata(endpointCtx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx)?.options, {
|
|
3085
|
+
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
3086
|
+
dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
|
|
3087
|
+
public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
|
|
3088
|
+
grant_types_supported: opts.grantTypes,
|
|
3089
|
+
jwt_disabled: opts.disableJwtPlugin
|
|
3090
|
+
}),
|
|
3091
|
+
...mergeDiscoveryMetadata(opts.clientDiscovery)
|
|
3092
|
+
}) };
|
|
3093
|
+
}
|
|
3094
|
+
if (isOpenIdConfigRequest) return { response: createMetadataResponse(oidcServerMetadata(endpointCtx, opts)) };
|
|
3095
|
+
};
|
|
2917
3096
|
return {
|
|
2918
3097
|
id: "oauth-provider",
|
|
2919
3098
|
version: PACKAGE_VERSION,
|
|
2920
3099
|
options: opts,
|
|
3100
|
+
onRequest: handleIssuerMetadataRequest,
|
|
2921
3101
|
init: (ctx) => {
|
|
2922
3102
|
if (ctx.options.secondaryStorage && ctx.options.session?.storeSessionInDatabase !== true) throw new BetterAuthError("OAuth Provider requires `session.storeSessionInDatabase: true` when using secondaryStorage");
|
|
2923
3103
|
if (!opts.disableJwtPlugin) {
|
|
@@ -2943,10 +3123,18 @@ const oauthProvider = (options) => {
|
|
|
2943
3123
|
handler: createAuthMiddleware(async (ctx) => {
|
|
2944
3124
|
const query = ctx.body.oauth_query;
|
|
2945
3125
|
if (!await verifyOAuthQueryParams(query, ctx.context.secret)) throw new APIError("BAD_REQUEST", { error: "invalid_signature" });
|
|
3126
|
+
const signedQueryIssuedAt = getSignedQueryIssuedAt(query);
|
|
2946
3127
|
const queryParams = new URLSearchParams(query);
|
|
3128
|
+
const postLoginClearedForSession = queryParams.get("ba_pl") ?? void 0;
|
|
2947
3129
|
queryParams.delete("sig");
|
|
2948
3130
|
queryParams.delete("exp");
|
|
2949
|
-
|
|
3131
|
+
queryParams.delete(signedQueryIssuedAtParam);
|
|
3132
|
+
queryParams.delete(postLoginClearedParam);
|
|
3133
|
+
await oAuthState.set({
|
|
3134
|
+
query: queryParams.toString(),
|
|
3135
|
+
signedQueryIssuedAt: signedQueryIssuedAt ?? void 0,
|
|
3136
|
+
postLoginClearedForSession
|
|
3137
|
+
});
|
|
2950
3138
|
if (ctx.path === "/sign-in/social") {
|
|
2951
3139
|
if (ctx.body.additionalData?.query) return;
|
|
2952
3140
|
if (!ctx.body.additionalData) ctx.body.additionalData = {};
|
|
@@ -2970,7 +3158,7 @@ const oauthProvider = (options) => {
|
|
|
2970
3158
|
const secFetchMode = ctx.request?.headers?.get("sec-fetch-mode")?.toLowerCase();
|
|
2971
3159
|
const acceptHeader = ctx.request?.headers?.get("accept")?.toLowerCase() ?? "";
|
|
2972
3160
|
if (!(secFetchMode === "navigate" || !secFetchMode && (acceptHeader.includes("text/html") || acceptHeader.includes("application/xhtml+xml")))) ctx.headers?.set("accept", "application/json");
|
|
2973
|
-
ctx.query =
|
|
3161
|
+
ctx.query = searchParamsToQuery(removePromptFromQuery(query, "login"));
|
|
2974
3162
|
return await authorizeEndpoint(ctx, opts);
|
|
2975
3163
|
})
|
|
2976
3164
|
}]
|
|
@@ -2984,6 +3172,7 @@ const oauthProvider = (options) => {
|
|
|
2984
3172
|
else return {
|
|
2985
3173
|
...authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
|
|
2986
3174
|
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
3175
|
+
dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
|
|
2987
3176
|
public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
|
|
2988
3177
|
grant_types_supported: opts.grantTypes,
|
|
2989
3178
|
jwt_disabled: opts.disableJwtPlugin
|
|
@@ -3010,6 +3199,7 @@ const oauthProvider = (options) => {
|
|
|
3010
3199
|
code_challenge: z.string().optional(),
|
|
3011
3200
|
code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
|
|
3012
3201
|
nonce: z.string().optional(),
|
|
3202
|
+
resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional(),
|
|
3013
3203
|
prompt: z.string().pipe(z.enum([
|
|
3014
3204
|
"none",
|
|
3015
3205
|
"consent",
|
|
@@ -3021,7 +3211,10 @@ const oauthProvider = (options) => {
|
|
|
3021
3211
|
])).optional()
|
|
3022
3212
|
}),
|
|
3023
3213
|
redirectOnError: authorizeRedirectOnError(opts),
|
|
3024
|
-
errorCodesByField: {
|
|
3214
|
+
errorCodesByField: {
|
|
3215
|
+
response_type: { invalid: "unsupported_response_type" },
|
|
3216
|
+
resource: { invalid: "invalid_target" }
|
|
3217
|
+
},
|
|
3025
3218
|
metadata: { openapi: {
|
|
3026
3219
|
description: "Authorize an OAuth2 request",
|
|
3027
3220
|
parameters: [
|
|
@@ -3091,6 +3284,16 @@ const oauthProvider = (options) => {
|
|
|
3091
3284
|
schema: { type: "string" },
|
|
3092
3285
|
description: "OpenID Connect nonce"
|
|
3093
3286
|
},
|
|
3287
|
+
{
|
|
3288
|
+
name: "resource",
|
|
3289
|
+
in: "query",
|
|
3290
|
+
required: false,
|
|
3291
|
+
schema: {
|
|
3292
|
+
type: "array",
|
|
3293
|
+
items: { type: "string" }
|
|
3294
|
+
},
|
|
3295
|
+
description: "Requested token resource(s) (ie audience) to obtain a JWT formatted access token. May be supplied multiple times as repeated 'resource' query parameters (RFC 8707) or as an array of strings."
|
|
3296
|
+
},
|
|
3094
3297
|
{
|
|
3095
3298
|
name: "prompt",
|
|
3096
3299
|
in: "query",
|
|
@@ -3196,13 +3399,16 @@ const oauthProvider = (options) => {
|
|
|
3196
3399
|
code_verifier: z.string().optional(),
|
|
3197
3400
|
redirect_uri: SafeUrlSchema.optional(),
|
|
3198
3401
|
refresh_token: z.string().optional(),
|
|
3199
|
-
resource: z.
|
|
3402
|
+
resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional(),
|
|
3200
3403
|
scope: z.string().optional()
|
|
3201
3404
|
}),
|
|
3202
|
-
errorCodesByField: {
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3405
|
+
errorCodesByField: {
|
|
3406
|
+
grant_type: {
|
|
3407
|
+
missing: "invalid_request",
|
|
3408
|
+
invalid: "unsupported_grant_type"
|
|
3409
|
+
},
|
|
3410
|
+
resource: { invalid: "invalid_target" }
|
|
3411
|
+
},
|
|
3206
3412
|
metadata: {
|
|
3207
3413
|
allowedMediaTypes: ["application/x-www-form-urlencoded"],
|
|
3208
3414
|
openapi: {
|
|
@@ -3247,8 +3453,15 @@ const oauthProvider = (options) => {
|
|
|
3247
3453
|
description: "Refresh token (for refresh_token grant)"
|
|
3248
3454
|
},
|
|
3249
3455
|
resource: {
|
|
3250
|
-
|
|
3251
|
-
|
|
3456
|
+
oneOf: [{
|
|
3457
|
+
type: "string",
|
|
3458
|
+
description: "Single resource (URL)"
|
|
3459
|
+
}, {
|
|
3460
|
+
type: "array",
|
|
3461
|
+
items: { type: "string" },
|
|
3462
|
+
description: "Multiple resources (URLs)"
|
|
3463
|
+
}],
|
|
3464
|
+
description: "Requested token resource(s) (ie audience) to obtain a JWT formatted access token"
|
|
3252
3465
|
},
|
|
3253
3466
|
scope: {
|
|
3254
3467
|
type: "string",
|
|
@@ -3349,10 +3562,6 @@ const oauthProvider = (options) => {
|
|
|
3349
3562
|
token_type_hint: {
|
|
3350
3563
|
type: "string",
|
|
3351
3564
|
description: "Hint about the token type. Recognized values: `access_token`, `refresh_token`."
|
|
3352
|
-
},
|
|
3353
|
-
resource: {
|
|
3354
|
-
type: "string",
|
|
3355
|
-
description: "Introspects a token for a specific resource."
|
|
3356
3565
|
}
|
|
3357
3566
|
},
|
|
3358
3567
|
required: ["token"]
|
|
@@ -3640,7 +3849,7 @@ const oauthProvider = (options) => {
|
|
|
3640
3849
|
"client_secret_post",
|
|
3641
3850
|
"private_key_jwt"
|
|
3642
3851
|
]).default("client_secret_basic").optional(),
|
|
3643
|
-
jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
|
|
3852
|
+
jwks: z.union([z.array(z.record(z.string(), z.unknown())).min(1), z.object({ keys: z.array(z.record(z.string(), z.unknown())).min(1) })]).optional(),
|
|
3644
3853
|
jwks_uri: z.string().optional(),
|
|
3645
3854
|
grant_types: z.array(z.enum([
|
|
3646
3855
|
"authorization_code",
|
|
@@ -4045,6 +4254,9 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
4045
4254
|
if (!query.code_challenge || !query.code_challenge_method) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", "code_challenge and code_challenge_method must both be provided", query.state, getIssuer(ctx, opts)));
|
|
4046
4255
|
if (!["S256"].includes(query.code_challenge_method)) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method, only S256 is supported", query.state, getIssuer(ctx, opts)));
|
|
4047
4256
|
}
|
|
4257
|
+
const resource = query.resource;
|
|
4258
|
+
if (!(await checkResource(ctx, opts, resource, requestedScopes)).success) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_target", "requested resource invalid", query.state, getIssuer(ctx, opts)));
|
|
4259
|
+
const requestedResources = toResourceList(resource) ?? [];
|
|
4048
4260
|
const session = await getSessionFromCtx(ctx);
|
|
4049
4261
|
if (!session || promptSet?.has("login") || promptSet?.has("create")) {
|
|
4050
4262
|
if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "login_required", "authentication required");
|
|
@@ -4071,7 +4283,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
4071
4283
|
});
|
|
4072
4284
|
if (signupRedirect) {
|
|
4073
4285
|
if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "interaction_required", "End-User interaction is required");
|
|
4074
|
-
return redirectWithPromptCode(ctx, opts, "create", typeof signupRedirect === "string" ? signupRedirect : void 0);
|
|
4286
|
+
return redirectWithPromptCode(ctx, opts, "create", { page: typeof signupRedirect === "string" ? signupRedirect : void 0 });
|
|
4075
4287
|
}
|
|
4076
4288
|
}
|
|
4077
4289
|
if (!settings?.postLogin && opts.postLogin) {
|
|
@@ -4085,7 +4297,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
4085
4297
|
return redirectWithPromptCode(ctx, opts, "post_login");
|
|
4086
4298
|
}
|
|
4087
4299
|
}
|
|
4088
|
-
if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent");
|
|
4300
|
+
if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
|
|
4089
4301
|
const referenceId = await opts.postLogin?.consentReferenceId?.({
|
|
4090
4302
|
user: session.user,
|
|
4091
4303
|
session: session.session,
|
|
@@ -4097,7 +4309,8 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
4097
4309
|
userId: session.user.id,
|
|
4098
4310
|
sessionId: session.session.id,
|
|
4099
4311
|
authTime: new Date(session.session.createdAt).getTime(),
|
|
4100
|
-
referenceId
|
|
4312
|
+
referenceId,
|
|
4313
|
+
resource: requestedResources
|
|
4101
4314
|
});
|
|
4102
4315
|
const consent = await ctx.context.adapter.findOne({
|
|
4103
4316
|
model: "oauthConsent",
|
|
@@ -4118,7 +4331,12 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
4118
4331
|
});
|
|
4119
4332
|
if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val))) {
|
|
4120
4333
|
if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "consent_required", "End-User consent is required");
|
|
4121
|
-
return redirectWithPromptCode(ctx, opts, "consent");
|
|
4334
|
+
return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
|
|
4335
|
+
}
|
|
4336
|
+
const consentedResources = consent?.resources ?? [];
|
|
4337
|
+
if (requestedResources.some((requestedResource) => !consentedResources.includes(requestedResource))) {
|
|
4338
|
+
if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "consent_required", "End-User consent is required");
|
|
4339
|
+
return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
|
|
4122
4340
|
}
|
|
4123
4341
|
return redirectWithAuthorizationCode(ctx, opts, {
|
|
4124
4342
|
query,
|
|
@@ -4126,7 +4344,8 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
4126
4344
|
userId: session.user.id,
|
|
4127
4345
|
sessionId: session.session.id,
|
|
4128
4346
|
authTime: new Date(session.session.createdAt).getTime(),
|
|
4129
|
-
referenceId
|
|
4347
|
+
referenceId,
|
|
4348
|
+
resource: requestedResources
|
|
4130
4349
|
});
|
|
4131
4350
|
}
|
|
4132
4351
|
function serializeAuthorizationQuery(query) {
|
|
@@ -4152,7 +4371,8 @@ async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
|
|
|
4152
4371
|
userId: verificationValue.userId,
|
|
4153
4372
|
sessionId: verificationValue?.sessionId,
|
|
4154
4373
|
referenceId: verificationValue.referenceId,
|
|
4155
|
-
authTime: verificationValue.authTime
|
|
4374
|
+
authTime: verificationValue.authTime,
|
|
4375
|
+
resource: verificationValue.resource
|
|
4156
4376
|
})
|
|
4157
4377
|
};
|
|
4158
4378
|
await ctx.context.internalAdapter.createVerificationValue({
|
|
@@ -4165,8 +4385,8 @@ async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
|
|
|
4165
4385
|
redirectUriWithCode.searchParams.set("iss", getIssuer(ctx, opts));
|
|
4166
4386
|
return handleRedirect(ctx, redirectUriWithCode.toString());
|
|
4167
4387
|
}
|
|
4168
|
-
async function redirectWithPromptCode(ctx, opts, type,
|
|
4169
|
-
const queryParams = await signParams(ctx, opts);
|
|
4388
|
+
async function redirectWithPromptCode(ctx, opts, type, options) {
|
|
4389
|
+
const queryParams = await signParams(ctx, opts, { postLoginClearedForSession: type === "consent" && opts.postLogin ? options?.sessionId : void 0 });
|
|
4170
4390
|
let path = opts.loginPage;
|
|
4171
4391
|
if (type === "select_account") path = opts.selectAccount?.page ?? opts.loginPage;
|
|
4172
4392
|
else if (type === "post_login") {
|
|
@@ -4174,12 +4394,16 @@ async function redirectWithPromptCode(ctx, opts, type, page) {
|
|
|
4174
4394
|
path = opts.postLogin?.page;
|
|
4175
4395
|
} else if (type === "consent") path = opts.consentPage;
|
|
4176
4396
|
else if (type === "create") path = opts.signup?.page ?? opts.loginPage;
|
|
4177
|
-
return handleRedirect(ctx, `${page ?? path}?${queryParams}`);
|
|
4397
|
+
return handleRedirect(ctx, `${options?.page ?? path}?${queryParams}`);
|
|
4178
4398
|
}
|
|
4179
|
-
async function signParams(ctx, opts) {
|
|
4180
|
-
const
|
|
4399
|
+
async function signParams(ctx, opts, flags) {
|
|
4400
|
+
const issuedAt = Date.now();
|
|
4401
|
+
const exp = Math.floor(issuedAt / 1e3) + (opts.codeExpiresIn ?? 600);
|
|
4181
4402
|
const params = serializeAuthorizationQuery(ctx.query);
|
|
4182
4403
|
params.set("exp", String(exp));
|
|
4404
|
+
params.set(signedQueryIssuedAtParam, String(issuedAt));
|
|
4405
|
+
params.delete(postLoginClearedParam);
|
|
4406
|
+
if (flags?.postLoginClearedForSession) params.set(postLoginClearedParam, flags.postLoginClearedForSession);
|
|
4183
4407
|
const signature = await makeSignature(params.toString(), ctx.context.secret);
|
|
4184
4408
|
params.append("sig", signature);
|
|
4185
4409
|
return params.toString();
|
|
@@ -4194,7 +4418,7 @@ function authServerMetadata(ctx, opts, overrides) {
|
|
|
4194
4418
|
authorization_endpoint: `${baseURL}/oauth2/authorize`,
|
|
4195
4419
|
token_endpoint: `${baseURL}/oauth2/token`,
|
|
4196
4420
|
jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
|
|
4197
|
-
registration_endpoint: `${baseURL}/oauth2/register
|
|
4421
|
+
registration_endpoint: overrides?.dynamic_client_registration_supported ? `${baseURL}/oauth2/register` : void 0,
|
|
4198
4422
|
introspection_endpoint: `${baseURL}/oauth2/introspect`,
|
|
4199
4423
|
revocation_endpoint: `${baseURL}/oauth2/revoke`,
|
|
4200
4424
|
response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
|
|
@@ -4210,19 +4434,19 @@ function authServerMetadata(ctx, opts, overrides) {
|
|
|
4210
4434
|
"client_secret_post",
|
|
4211
4435
|
"private_key_jwt"
|
|
4212
4436
|
],
|
|
4213
|
-
token_endpoint_auth_signing_alg_values_supported: [...
|
|
4437
|
+
token_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
|
|
4214
4438
|
introspection_endpoint_auth_methods_supported: [
|
|
4215
4439
|
"client_secret_basic",
|
|
4216
4440
|
"client_secret_post",
|
|
4217
4441
|
"private_key_jwt"
|
|
4218
4442
|
],
|
|
4219
|
-
introspection_endpoint_auth_signing_alg_values_supported: [...
|
|
4443
|
+
introspection_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
|
|
4220
4444
|
revocation_endpoint_auth_methods_supported: [
|
|
4221
4445
|
"client_secret_basic",
|
|
4222
4446
|
"client_secret_post",
|
|
4223
4447
|
"private_key_jwt"
|
|
4224
4448
|
],
|
|
4225
|
-
revocation_endpoint_auth_signing_alg_values_supported: [...
|
|
4449
|
+
revocation_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
|
|
4226
4450
|
code_challenge_methods_supported: ["S256"],
|
|
4227
4451
|
authorization_response_iss_parameter_supported: true
|
|
4228
4452
|
};
|
|
@@ -4233,6 +4457,7 @@ function oidcServerMetadata(ctx, opts) {
|
|
|
4233
4457
|
return {
|
|
4234
4458
|
...authServerMetadata(ctx, jwtPluginOptions, {
|
|
4235
4459
|
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
4460
|
+
dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
|
|
4236
4461
|
public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
|
|
4237
4462
|
grant_types_supported: opts.grantTypes,
|
|
4238
4463
|
jwt_disabled: opts.disableJwtPlugin
|