@better-auth/oauth-provider 1.7.0-beta.5 → 1.7.0-beta.7
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-DmT1B6_6.mjs → client-assertion-CctbJywV.mjs} +88 -64
- package/dist/client-resource.d.mts +17 -2
- package/dist/client-resource.mjs +45 -25
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +3 -13
- package/dist/index.d.mts +100 -17
- package/dist/index.mjs +1239 -1699
- package/dist/introspect-BXNvkz8S.mjs +2119 -0
- package/dist/{oauth-BXrYl5x6.d.mts → oauth-CPWY2Few.d.mts} +836 -33
- package/dist/{oauth-DU6NeviY.d.mts → oauth-CqOygaZd.d.mts} +265 -148
- package/dist/resource-challenge-B-cqv4ur.mjs +63 -0
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/signed-query-CFv2jNMT.mjs +44 -0
- package/dist/{utils-D2dLqo7f.mjs → utils-Baq6atYN.mjs} +310 -68
- package/dist/{version-B1ZiRmxj.mjs → version-DkFgXWfN.mjs} +1 -1
- package/package.json +7 -8
- package/dist/mcp-CYnz-MXn.mjs +0 -56
package/dist/index.mjs
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { n as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { APIError as APIError$1 } from "better-call";
|
|
8
|
-
import { PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
|
|
1
|
+
import { D as applyOAuthProviderMetadataExtensions, E as verifyOAuthQueryParams, F as getSupportedGrantTypes, L as isExtensionTokenEndpointAuthMethod, M as getClientDiscoveries, P as getSupportedAuthMethods, R as validateOAuthProviderExtensions, S as storeToken, T as validateClientCredentials, _ as removePromptFromQuery, a as getClient, b as searchParamsToQuery, c as getStoredToken, d as mergeDiscoveryMetadata, g as removeMaxAgeFromQuery, h as parsePrompt, i as extractClientCredentials, j as extendOAuthProvider, l as isPKCERequired, m as parseClientMetadata, n as decryptStoredClientSecret, o as getJwtPlugin, p as parseBearerToken, r as destructureCredentials, t as clientAllowsGrant, u as isSessionFreshForSignedQuery, w as toResourceList, x as storeClientSecret, y as resolveSubjectIdentifier } from "./utils-Baq6atYN.mjs";
|
|
2
|
+
import { a as setSignedOAuthQueryParameterNames, i as postLoginClearedParam, n as canonicalizeOAuthQueryParams, o as signedQueryIssuedAtParam, r as getSignedQueryIssuedAt } from "./signed-query-CFv2jNMT.mjs";
|
|
3
|
+
import { _ as invalidateResourceCache, a as invalidateRefreshFamily, b as resolveResourcePolicy, c as ResourceUriSchema, d as clientRegistrationRequestSchema, f as JWS_ALGORITHMS, g as getResource, h as extractRepeatedResourceFromForm, i as getOAuthProviderApi, l as SafeUrlSchema, m as buildClientResourceLinkId, o as tokenEndpoint, p as assertIdentifierValid, r as decodeRefreshToken, s as userInfoEndpoint, t as introspectEndpoint, u as authorizationQuerySchema, v as isAudienceClaimAllowed, x as seedResources, y as logEnforcePerClientResourcesResolution } from "./introspect-BXNvkz8S.mjs";
|
|
4
|
+
import { n as consumeClientAssertion, r as isPrivateHostname } from "./client-assertion-CctbJywV.mjs";
|
|
5
|
+
import { t as PACKAGE_VERSION } from "./version-DkFgXWfN.mjs";
|
|
6
|
+
import { t as raiseResourceServerChallenge } from "./resource-challenge-B-cqv4ur.mjs";
|
|
9
7
|
import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
|
|
10
8
|
import { isLoopbackHost, isLoopbackIP } from "@better-auth/core/utils/host";
|
|
9
|
+
import { APIError, NO_STORE_HEADERS, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, dispatchAuthEndpoint, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
11
10
|
import { generateRandomString, makeSignature } from "better-auth/crypto";
|
|
12
|
-
import {
|
|
11
|
+
import { APIError as APIError$1 } from "better-call";
|
|
12
|
+
import { defineRequestState, runWithTransaction } from "@better-auth/core/context";
|
|
13
13
|
import { logger } from "@better-auth/core/env";
|
|
14
14
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
15
15
|
import { parseSetCookieHeader } from "better-auth/cookies";
|
|
16
16
|
import { mergeSchema } from "better-auth/db";
|
|
17
17
|
import * as z from "zod";
|
|
18
|
+
import { DPOP_SIGNING_ALGORITHMS, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
|
|
19
|
+
import { getJwks, stripAccessTokenAuthorizationScheme } from "better-auth/oauth2";
|
|
20
|
+
import { compactVerify, createLocalJWKSet, decodeJwt, jwtVerify } from "jose";
|
|
18
21
|
import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
|
|
19
|
-
import { SignJWT, base64url, compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader } from "jose";
|
|
20
|
-
import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
|
|
21
22
|
//#region src/consent.ts
|
|
22
23
|
async function consentEndpoint(ctx, opts, authorize) {
|
|
23
24
|
const oauthRequest = await oAuthState.get();
|
|
@@ -159,1019 +160,6 @@ async function postLogin(ctx, authorize) {
|
|
|
159
160
|
return await authorize(ctx, { postLogin: state?.postLoginClearedForSession !== void 0 && state.postLoginClearedForSession === session?.session.id });
|
|
160
161
|
}
|
|
161
162
|
//#endregion
|
|
162
|
-
//#region src/types/zod.ts
|
|
163
|
-
const DANGEROUS_SCHEMES = [
|
|
164
|
-
"javascript:",
|
|
165
|
-
"data:",
|
|
166
|
-
"vbscript:"
|
|
167
|
-
];
|
|
168
|
-
/**
|
|
169
|
-
* Validates an RFC 8707 resource indicator. The value must be an absolute URI
|
|
170
|
-
* with no fragment (RFC 8707 §2). Unlike a redirect URI it is not restricted to
|
|
171
|
-
* HTTPS, because a resource server identifier may use any absolute URI scheme;
|
|
172
|
-
* the configured `validAudiences` allowlist is the authoritative control over
|
|
173
|
-
* which resources a token may target.
|
|
174
|
-
*/
|
|
175
|
-
const ResourceUriSchema = z.string().superRefine((val, ctx) => {
|
|
176
|
-
if (!URL.canParse(val)) {
|
|
177
|
-
ctx.addIssue({
|
|
178
|
-
code: "custom",
|
|
179
|
-
message: "resource must be an absolute URI",
|
|
180
|
-
fatal: true
|
|
181
|
-
});
|
|
182
|
-
return z.NEVER;
|
|
183
|
-
}
|
|
184
|
-
if (val.includes("#")) {
|
|
185
|
-
ctx.addIssue({
|
|
186
|
-
code: "custom",
|
|
187
|
-
message: "resource must not contain a fragment"
|
|
188
|
-
});
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (DANGEROUS_SCHEMES.includes(new URL(val).protocol)) ctx.addIssue({
|
|
192
|
-
code: "custom",
|
|
193
|
-
message: "resource cannot use javascript:, data:, or vbscript: scheme"
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
const authorizationPromptTokenSchema = z.enum([
|
|
197
|
-
"none",
|
|
198
|
-
"consent",
|
|
199
|
-
"login",
|
|
200
|
-
"create",
|
|
201
|
-
"select_account"
|
|
202
|
-
]);
|
|
203
|
-
const authorizationPromptSchema = z.string().superRefine((value, ctx) => {
|
|
204
|
-
const promptTokens = value.split(" ").map((token) => token.trim()).filter(Boolean);
|
|
205
|
-
const promptSet = /* @__PURE__ */ new Set();
|
|
206
|
-
if (!promptTokens.length) {
|
|
207
|
-
ctx.addIssue({
|
|
208
|
-
code: "custom",
|
|
209
|
-
message: "prompt must include at least one value"
|
|
210
|
-
});
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
for (const token of promptTokens) {
|
|
214
|
-
const result = authorizationPromptTokenSchema.safeParse(token);
|
|
215
|
-
if (!result.success) {
|
|
216
|
-
ctx.addIssue({
|
|
217
|
-
code: "custom",
|
|
218
|
-
message: `unsupported prompt value: ${token}`
|
|
219
|
-
});
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
promptSet.add(result.data);
|
|
223
|
-
}
|
|
224
|
-
if (promptSet.has("none") && promptSet.size > 1) ctx.addIssue({
|
|
225
|
-
code: "custom",
|
|
226
|
-
message: "prompt=none cannot be combined with other prompt values"
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
const maxAgeSchema = z.union([z.number(), z.string().trim().min(1)]).transform((value, ctx) => {
|
|
230
|
-
const maxAge = typeof value === "number" ? value : Number(value);
|
|
231
|
-
if (!Number.isInteger(maxAge) || maxAge < 0) {
|
|
232
|
-
ctx.addIssue({
|
|
233
|
-
code: "custom",
|
|
234
|
-
message: "max_age must be a non-negative integer"
|
|
235
|
-
});
|
|
236
|
-
return z.NEVER;
|
|
237
|
-
}
|
|
238
|
-
return maxAge;
|
|
239
|
-
});
|
|
240
|
-
/**
|
|
241
|
-
* Runtime schema for OAuthAuthorizationQuery.
|
|
242
|
-
* Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
|
|
243
|
-
*/
|
|
244
|
-
const authorizationQuerySchema = z.object({
|
|
245
|
-
response_type: z.string().pipe(z.enum(["code"])).optional(),
|
|
246
|
-
request_uri: z.string().optional(),
|
|
247
|
-
redirect_uri: SafeUrlSchema.optional(),
|
|
248
|
-
scope: z.string().optional(),
|
|
249
|
-
state: z.string().optional(),
|
|
250
|
-
client_id: z.string(),
|
|
251
|
-
prompt: authorizationPromptSchema.optional(),
|
|
252
|
-
display: z.string().optional(),
|
|
253
|
-
ui_locales: z.string().optional(),
|
|
254
|
-
max_age: maxAgeSchema.optional(),
|
|
255
|
-
acr_values: z.string().optional(),
|
|
256
|
-
login_hint: z.string().optional(),
|
|
257
|
-
id_token_hint: z.string().optional(),
|
|
258
|
-
code_challenge: z.string().optional(),
|
|
259
|
-
code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
|
|
260
|
-
nonce: z.string().optional(),
|
|
261
|
-
resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional()
|
|
262
|
-
}).passthrough();
|
|
263
|
-
const storedAuthorizationQuerySchema = authorizationQuerySchema.extend({ redirect_uri: SafeUrlSchema });
|
|
264
|
-
/**
|
|
265
|
-
* Runtime schema for the authorization code verification value.
|
|
266
|
-
* Validates structure on deserialization from the JSON blob stored in the DB.
|
|
267
|
-
* Uses passthrough so future fields (e.g. from authorization challenge) don't break parsing.
|
|
268
|
-
*/
|
|
269
|
-
const verificationValueSchema = z.object({
|
|
270
|
-
type: z.literal("authorization_code"),
|
|
271
|
-
query: storedAuthorizationQuerySchema,
|
|
272
|
-
sessionId: z.string(),
|
|
273
|
-
userId: z.string(),
|
|
274
|
-
referenceId: z.string().optional(),
|
|
275
|
-
authTime: z.number().optional(),
|
|
276
|
-
resource: z.array(z.string()).optional()
|
|
277
|
-
}).passthrough();
|
|
278
|
-
//#endregion
|
|
279
|
-
//#region src/userinfo.ts
|
|
280
|
-
/**
|
|
281
|
-
* Provides shared /userinfo and id_token claims functionality
|
|
282
|
-
*
|
|
283
|
-
* @see https://openid.net/specs/openid-connect-core-1_0.html#NormalClaims
|
|
284
|
-
*/
|
|
285
|
-
function userNormalClaims(user, scopes) {
|
|
286
|
-
const name = user.name.split(" ").filter((v) => v !== "");
|
|
287
|
-
const profile = {
|
|
288
|
-
name: user.name ?? void 0,
|
|
289
|
-
picture: user.image ?? void 0,
|
|
290
|
-
given_name: name.length > 1 ? name.slice(0, -1).join(" ") : void 0,
|
|
291
|
-
family_name: name.length > 1 ? name.at(-1) : void 0
|
|
292
|
-
};
|
|
293
|
-
const email = {
|
|
294
|
-
email: user.email ?? void 0,
|
|
295
|
-
email_verified: user.emailVerified ?? false
|
|
296
|
-
};
|
|
297
|
-
return {
|
|
298
|
-
sub: user.id ?? void 0,
|
|
299
|
-
...scopes.includes("profile") ? profile : {},
|
|
300
|
-
...scopes.includes("email") ? email : {}
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Handles the /oauth2/userinfo endpoint
|
|
305
|
-
*/
|
|
306
|
-
async function userInfoEndpoint(ctx, opts) {
|
|
307
|
-
const authorization = ctx.headers?.get("authorization");
|
|
308
|
-
const token = typeof authorization === "string" && authorization?.startsWith("Bearer ") ? authorization?.replace("Bearer ", "") : authorization;
|
|
309
|
-
if (!token?.length) throw new APIError("UNAUTHORIZED", {
|
|
310
|
-
error_description: "authorization header not found",
|
|
311
|
-
error: "invalid_request"
|
|
312
|
-
});
|
|
313
|
-
const jwt = await validateAccessToken(ctx, opts, token);
|
|
314
|
-
if (!jwt.active) throw new APIError("UNAUTHORIZED", {
|
|
315
|
-
error_description: "the access token is invalid or has been revoked",
|
|
316
|
-
error: "invalid_token"
|
|
317
|
-
});
|
|
318
|
-
const scopes = jwt.scope?.split(" ");
|
|
319
|
-
if (!scopes?.includes("openid")) throw new APIError("BAD_REQUEST", {
|
|
320
|
-
error_description: "Missing required scope",
|
|
321
|
-
error: "invalid_scope"
|
|
322
|
-
});
|
|
323
|
-
if (!jwt.sub) throw new APIError("BAD_REQUEST", {
|
|
324
|
-
error_description: "user not found",
|
|
325
|
-
error: "invalid_request"
|
|
326
|
-
});
|
|
327
|
-
const user = await ctx.context.internalAdapter.findUserById(jwt.sub);
|
|
328
|
-
if (!user) throw new APIError("BAD_REQUEST", {
|
|
329
|
-
error_description: "user not found",
|
|
330
|
-
error: "invalid_request"
|
|
331
|
-
});
|
|
332
|
-
const baseUserClaims = userNormalClaims(user, scopes ?? []);
|
|
333
|
-
if (opts.pairwiseSecret) {
|
|
334
|
-
const clientId = jwt.client_id ?? jwt.azp;
|
|
335
|
-
if (clientId) {
|
|
336
|
-
const client = await getClient(ctx, opts, clientId);
|
|
337
|
-
if (client) baseUserClaims.sub = await resolveSubjectIdentifier(user.id, client, opts);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
const additionalInfoUserClaims = opts.customUserInfoClaims && scopes?.length ? await opts.customUserInfoClaims({
|
|
341
|
-
user,
|
|
342
|
-
scopes,
|
|
343
|
-
jwt
|
|
344
|
-
}) : {};
|
|
345
|
-
return {
|
|
346
|
-
...baseUserClaims,
|
|
347
|
-
...additionalInfoUserClaims
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
//#endregion
|
|
351
|
-
//#region src/token.ts
|
|
352
|
-
/**
|
|
353
|
-
* Handles the /oauth2/token endpoint by delegating
|
|
354
|
-
* the grant types
|
|
355
|
-
*/
|
|
356
|
-
async function tokenEndpoint(ctx, opts) {
|
|
357
|
-
const grantType = ctx.body.grant_type;
|
|
358
|
-
if (opts.grantTypes && !opts.grantTypes.includes(grantType)) throw new APIError("BAD_REQUEST", {
|
|
359
|
-
error_description: `unsupported grant_type ${grantType}`,
|
|
360
|
-
error: "unsupported_grant_type"
|
|
361
|
-
});
|
|
362
|
-
switch (grantType) {
|
|
363
|
-
case "authorization_code": return handleAuthorizationCodeGrant(ctx, opts);
|
|
364
|
-
case "client_credentials": return handleClientCredentialsGrant(ctx, opts);
|
|
365
|
-
case "refresh_token": return handleRefreshTokenGrant(ctx, opts);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, resources, referenceId, overrides) {
|
|
369
|
-
const iat = overrides?.iat ?? Math.floor(Date.now() / 1e3);
|
|
370
|
-
const exp = overrides?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
|
|
371
|
-
const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
|
|
372
|
-
user,
|
|
373
|
-
scopes,
|
|
374
|
-
resources,
|
|
375
|
-
referenceId,
|
|
376
|
-
metadata: parseClientMetadata(client.metadata)
|
|
377
|
-
}) : {};
|
|
378
|
-
const jwtPluginOptions = getJwtPlugin(ctx.context).options;
|
|
379
|
-
return signJWT(ctx, {
|
|
380
|
-
options: jwtPluginOptions,
|
|
381
|
-
payload: {
|
|
382
|
-
...customClaims,
|
|
383
|
-
sub: user?.id,
|
|
384
|
-
aud: toAudienceClaim(audience),
|
|
385
|
-
azp: client.clientId,
|
|
386
|
-
scope: scopes.join(" "),
|
|
387
|
-
sid: overrides?.sid,
|
|
388
|
-
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
389
|
-
iat,
|
|
390
|
-
exp
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* Computes an OIDC hash (at_hash, c_hash) per OIDC Core §3.1.3.6.
|
|
396
|
-
* Hashes the token, takes the left half, and base64url-encodes it.
|
|
397
|
-
*/
|
|
398
|
-
async function computeOidcHash(token, signingAlg) {
|
|
399
|
-
let hashAlg;
|
|
400
|
-
if (signingAlg === "EdDSA") hashAlg = "SHA-512";
|
|
401
|
-
else if (signingAlg.endsWith("384")) hashAlg = "SHA-384";
|
|
402
|
-
else if (signingAlg.endsWith("512")) hashAlg = "SHA-512";
|
|
403
|
-
else hashAlg = "SHA-256";
|
|
404
|
-
const digest = new Uint8Array(await crypto.subtle.digest(hashAlg, new TextEncoder().encode(token)));
|
|
405
|
-
return base64url.encode(digest.slice(0, digest.length / 2));
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* Creates a user id token in code_authorization with scope of 'openid'
|
|
409
|
-
* and hybrid/implicit (not yet implemented) flows
|
|
410
|
-
*/
|
|
411
|
-
async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) {
|
|
412
|
-
const iat = Math.floor(Date.now() / 1e3);
|
|
413
|
-
const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
|
|
414
|
-
const userClaims = userNormalClaims(user, scopes);
|
|
415
|
-
const resolvedSub = await resolveSubjectIdentifier(user.id, client, opts);
|
|
416
|
-
const authTimeSec = authTime != null ? Math.floor(authTime.getTime() / 1e3) : void 0;
|
|
417
|
-
const acr = "urn:mace:incommon:iap:bronze";
|
|
418
|
-
const customClaims = opts.customIdTokenClaims ? await opts.customIdTokenClaims({
|
|
419
|
-
user,
|
|
420
|
-
scopes,
|
|
421
|
-
metadata: parseClientMetadata(client.metadata)
|
|
422
|
-
}) : {};
|
|
423
|
-
const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
|
|
424
|
-
const resolvedKey = !opts.disableJwtPlugin && !jwtPluginOptions?.jwt?.sign ? await resolveSigningKey(ctx, jwtPluginOptions) : void 0;
|
|
425
|
-
const signingAlg = opts.disableJwtPlugin ? "HS256" : resolvedKey?.alg ?? jwtPluginOptions?.jwks?.keyPairConfig?.alg;
|
|
426
|
-
const atHash = accessToken ? await computeOidcHash(accessToken, signingAlg) : void 0;
|
|
427
|
-
const emitSid = Boolean(client.enableEndSession || client.backchannelLogoutUri);
|
|
428
|
-
const payload = {
|
|
429
|
-
...userClaims,
|
|
430
|
-
auth_time: authTimeSec,
|
|
431
|
-
acr,
|
|
432
|
-
...customClaims,
|
|
433
|
-
at_hash: atHash,
|
|
434
|
-
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
435
|
-
sub: resolvedSub,
|
|
436
|
-
aud: client.clientId,
|
|
437
|
-
nonce,
|
|
438
|
-
iat,
|
|
439
|
-
exp,
|
|
440
|
-
sid: emitSid ? sessionId : void 0
|
|
441
|
-
};
|
|
442
|
-
if (opts.disableJwtPlugin && !client.clientSecret) return;
|
|
443
|
-
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, {
|
|
444
|
-
options: jwtPluginOptions,
|
|
445
|
-
payload,
|
|
446
|
-
resolvedKey: resolvedKey ?? void 0
|
|
447
|
-
});
|
|
448
|
-
if (idToken && atHash && jwtPluginOptions?.jwt?.sign) {
|
|
449
|
-
const header = decodeProtectedHeader(idToken);
|
|
450
|
-
if (header.alg !== signingAlg) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
451
|
-
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.`,
|
|
452
|
-
error: "server_error"
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
return idToken;
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Encodes a refresh token for a client
|
|
459
|
-
*/
|
|
460
|
-
async function encodeRefreshToken(opts, token, sessionId) {
|
|
461
|
-
return (opts.prefix?.refreshToken ?? "") + (opts.formatRefreshToken?.encrypt ? opts.formatRefreshToken.encrypt(token, sessionId) : token);
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Decodes a refresh token for a client
|
|
465
|
-
*
|
|
466
|
-
* @internal
|
|
467
|
-
*/
|
|
468
|
-
async function decodeRefreshToken(opts, token) {
|
|
469
|
-
if (opts.prefix?.refreshToken) if (token.startsWith(opts.prefix.refreshToken)) token = token.replace(opts.prefix.refreshToken, "");
|
|
470
|
-
else throw new APIError("BAD_REQUEST", {
|
|
471
|
-
error_description: "refresh token not found",
|
|
472
|
-
error: "invalid_token"
|
|
473
|
-
});
|
|
474
|
-
return opts.formatRefreshToken?.decrypt ? opts.formatRefreshToken?.decrypt(token) : { token };
|
|
475
|
-
}
|
|
476
|
-
async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, refreshId) {
|
|
477
|
-
const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
|
|
478
|
-
const exp = payload?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
|
|
479
|
-
const token = opts.generateOpaqueAccessToken ? await opts.generateOpaqueAccessToken() : generateRandomString(32, "A-Z", "a-z");
|
|
480
|
-
await ctx.context.adapter.create({
|
|
481
|
-
model: "oauthAccessToken",
|
|
482
|
-
data: {
|
|
483
|
-
token: await storeToken(opts.storeTokens, token, "access_token"),
|
|
484
|
-
clientId: client.clientId,
|
|
485
|
-
sessionId: payload?.sid,
|
|
486
|
-
userId: user?.id,
|
|
487
|
-
referenceId,
|
|
488
|
-
resources,
|
|
489
|
-
refreshId,
|
|
490
|
-
scopes,
|
|
491
|
-
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
492
|
-
expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
return (opts.prefix?.opaqueAccessToken ?? "") + token;
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Tear down the entire refresh-token family for a (client, user) pair, plus
|
|
499
|
-
* any access tokens that reference those refresh rows, per RFC 9700 §4.14.
|
|
500
|
-
* Access tokens are deleted first so the parent rows' foreign-key children
|
|
501
|
-
* do not block the refresh-row delete.
|
|
502
|
-
*
|
|
503
|
-
* TODO(invalidate-family-race): the two `deleteMany` calls are not atomic
|
|
504
|
-
* with respect to each other. Between them, a concurrent rotation in a
|
|
505
|
-
* different worker can `create` a fresh refresh row (and, immediately after,
|
|
506
|
-
* an access-token row referencing it) for the same (client, user) pair,
|
|
507
|
-
* leaving the family partially rebuilt and the new refresh row orphaned of
|
|
508
|
-
* any deletion. Closing this window requires the same transactional adapter
|
|
509
|
-
* contract tracked under FIXME(strict-family-invalidation) in
|
|
510
|
-
* `createRefreshToken`.
|
|
511
|
-
*
|
|
512
|
-
* @internal
|
|
513
|
-
*/
|
|
514
|
-
async function invalidateRefreshFamily(ctx, clientId, userId) {
|
|
515
|
-
const refreshTokens = await ctx.context.adapter.findMany({
|
|
516
|
-
model: "oauthRefreshToken",
|
|
517
|
-
where: [{
|
|
518
|
-
field: "clientId",
|
|
519
|
-
value: clientId
|
|
520
|
-
}, {
|
|
521
|
-
field: "userId",
|
|
522
|
-
value: userId
|
|
523
|
-
}]
|
|
524
|
-
});
|
|
525
|
-
if (refreshTokens.length) await ctx.context.adapter.deleteMany({
|
|
526
|
-
model: "oauthAccessToken",
|
|
527
|
-
where: [{
|
|
528
|
-
field: "refreshId",
|
|
529
|
-
operator: "in",
|
|
530
|
-
value: refreshTokens.map((r) => r.id)
|
|
531
|
-
}]
|
|
532
|
-
});
|
|
533
|
-
await ctx.context.adapter.deleteMany({
|
|
534
|
-
model: "oauthRefreshToken",
|
|
535
|
-
where: [{
|
|
536
|
-
field: "clientId",
|
|
537
|
-
value: clientId
|
|
538
|
-
}, {
|
|
539
|
-
field: "userId",
|
|
540
|
-
value: userId
|
|
541
|
-
}]
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, payload, originalRefresh, authTime, resources) {
|
|
545
|
-
const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
|
|
546
|
-
const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
|
|
547
|
-
const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
|
|
548
|
-
const sessionId = payload?.sid;
|
|
549
|
-
const newRow = {
|
|
550
|
-
token: await storeToken(opts.storeTokens, token, "refresh_token"),
|
|
551
|
-
clientId: client.clientId,
|
|
552
|
-
sessionId,
|
|
553
|
-
userId: user.id,
|
|
554
|
-
referenceId,
|
|
555
|
-
authTime,
|
|
556
|
-
scopes,
|
|
557
|
-
resources,
|
|
558
|
-
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
559
|
-
expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
|
|
560
|
-
};
|
|
561
|
-
if (!originalRefresh?.id) return {
|
|
562
|
-
id: (await ctx.context.adapter.create({
|
|
563
|
-
model: "oauthRefreshToken",
|
|
564
|
-
data: newRow
|
|
565
|
-
})).id,
|
|
566
|
-
token: await encodeRefreshToken(opts, token, sessionId)
|
|
567
|
-
};
|
|
568
|
-
if (!await ctx.context.adapter.update({
|
|
569
|
-
model: "oauthRefreshToken",
|
|
570
|
-
where: [{
|
|
571
|
-
field: "id",
|
|
572
|
-
value: originalRefresh.id
|
|
573
|
-
}, {
|
|
574
|
-
field: "revoked",
|
|
575
|
-
operator: "eq",
|
|
576
|
-
value: null
|
|
577
|
-
}],
|
|
578
|
-
update: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
|
|
579
|
-
})) throw new APIError("BAD_REQUEST", {
|
|
580
|
-
error_description: "invalid refresh token",
|
|
581
|
-
error: "invalid_grant"
|
|
582
|
-
});
|
|
583
|
-
return {
|
|
584
|
-
id: (await ctx.context.adapter.create({
|
|
585
|
-
model: "oauthRefreshToken",
|
|
586
|
-
data: newRow
|
|
587
|
-
})).id,
|
|
588
|
-
token: await encodeRefreshToken(opts, token, sessionId)
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
async function createUserTokens(ctx, opts, params) {
|
|
592
|
-
const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, verificationValue } = params;
|
|
593
|
-
const iat = Math.floor(Date.now() / 1e3);
|
|
594
|
-
const defaultExp = iat + (user ? opts.accessTokenExpiresIn ?? 3600 : opts.m2mAccessTokenExpiresIn ?? 3600);
|
|
595
|
-
const exp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
|
|
596
|
-
return prev < curr ? prev : curr;
|
|
597
|
-
}, defaultExp) : defaultExp;
|
|
598
|
-
const resourceResult = await checkResource(ctx, opts, params?.resources, scopes);
|
|
599
|
-
if (!resourceResult.success) throw new APIError("BAD_REQUEST", {
|
|
600
|
-
error_description: "requested resource invalid",
|
|
601
|
-
error: "invalid_target"
|
|
602
|
-
});
|
|
603
|
-
const audience = resourceResult.audience;
|
|
604
|
-
const isRefreshToken = user && clientAllowsGrant(client, "refresh_token") && (existingRefreshToken?.scopes?.includes("offline_access") || scopes.includes("offline_access"));
|
|
605
|
-
const isJwtAccessToken = audience && !opts.disableJwtPlugin;
|
|
606
|
-
const isIdToken = user && scopes.includes("openid");
|
|
607
|
-
const customFields = opts.customTokenResponseFields ? await opts.customTokenResponseFields({
|
|
608
|
-
grantType,
|
|
609
|
-
user,
|
|
610
|
-
scopes,
|
|
611
|
-
metadata: parseClientMetadata(client.metadata),
|
|
612
|
-
verificationValue
|
|
613
|
-
}) : void 0;
|
|
614
|
-
const refreshResources = params?.refreshToken?.resources ?? params?.originalResources ?? params?.resources;
|
|
615
|
-
const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
|
|
616
|
-
iat,
|
|
617
|
-
exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
|
|
618
|
-
sid: sessionId
|
|
619
|
-
}, existingRefreshToken, authTime, refreshResources) : void 0;
|
|
620
|
-
const [accessToken, refreshToken] = await Promise.all([isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, params?.resources, referenceId, {
|
|
621
|
-
iat,
|
|
622
|
-
exp,
|
|
623
|
-
sid: sessionId
|
|
624
|
-
}) : createOpaqueAccessToken(ctx, opts, user, client, scopes, {
|
|
625
|
-
iat,
|
|
626
|
-
exp,
|
|
627
|
-
sid: sessionId
|
|
628
|
-
}, params?.resources, referenceId, earlyRefreshToken?.id), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
|
|
629
|
-
iat,
|
|
630
|
-
exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
|
|
631
|
-
sid: sessionId
|
|
632
|
-
}, existingRefreshToken, authTime, refreshResources) : void 0]);
|
|
633
|
-
const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) : void 0;
|
|
634
|
-
return ctx.json({
|
|
635
|
-
...customFields,
|
|
636
|
-
access_token: accessToken,
|
|
637
|
-
expires_in: exp - iat,
|
|
638
|
-
expires_at: exp,
|
|
639
|
-
token_type: "Bearer",
|
|
640
|
-
refresh_token: refreshToken?.token,
|
|
641
|
-
scope: scopes.join(" "),
|
|
642
|
-
id_token: idToken
|
|
643
|
-
}, { headers: {
|
|
644
|
-
"Cache-Control": "no-store",
|
|
645
|
-
Pragma: "no-cache"
|
|
646
|
-
} });
|
|
647
|
-
}
|
|
648
|
-
/** Checks verification value */
|
|
649
|
-
async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resource) {
|
|
650
|
-
const verification = await ctx.context.internalAdapter.consumeVerificationValue(await storeToken(opts.storeTokens, code, "authorization_code"));
|
|
651
|
-
if (!verification) throw new APIError("UNAUTHORIZED", {
|
|
652
|
-
error_description: "invalid code",
|
|
653
|
-
error: "invalid_grant"
|
|
654
|
-
});
|
|
655
|
-
let rawValue;
|
|
656
|
-
try {
|
|
657
|
-
rawValue = JSON.parse(verification.value);
|
|
658
|
-
} catch {
|
|
659
|
-
throw new APIError("UNAUTHORIZED", {
|
|
660
|
-
error_description: "malformed verification value",
|
|
661
|
-
error: "invalid_grant"
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
const parsed = verificationValueSchema.safeParse(rawValue);
|
|
665
|
-
if (!parsed.success) throw new APIError("UNAUTHORIZED", {
|
|
666
|
-
error_description: "malformed verification value",
|
|
667
|
-
error: "invalid_grant"
|
|
668
|
-
});
|
|
669
|
-
const verificationValue = parsed.data;
|
|
670
|
-
if (verificationValue.query.client_id !== client_id) throw new APIError("UNAUTHORIZED", {
|
|
671
|
-
error_description: "invalid client_id",
|
|
672
|
-
error: "invalid_client"
|
|
673
|
-
});
|
|
674
|
-
if (verificationValue.query?.redirect_uri && verificationValue.query?.redirect_uri !== redirect_uri) throw new APIError("BAD_REQUEST", {
|
|
675
|
-
error_description: "redirect_uri mismatch",
|
|
676
|
-
error: "invalid_request"
|
|
677
|
-
});
|
|
678
|
-
const storedResources = toResourceList(verificationValue.resource) ?? toResourceList(verificationValue.query.resource);
|
|
679
|
-
const effectiveResources = resource ?? storedResources;
|
|
680
|
-
if (resource && storedResources) {
|
|
681
|
-
const requestedSet = new Set(resource);
|
|
682
|
-
const authorizedSet = new Set(storedResources);
|
|
683
|
-
for (const r of requestedSet) if (!authorizedSet.has(r)) throw new APIError("BAD_REQUEST", {
|
|
684
|
-
error_description: "requested resource not authorized",
|
|
685
|
-
error: "invalid_target"
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
return {
|
|
689
|
-
verificationValue,
|
|
690
|
-
effectiveResources,
|
|
691
|
-
authorizedResources: storedResources
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Obtains new Session Jwt and Refresh Tokens using a code
|
|
696
|
-
*/
|
|
697
|
-
async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
698
|
-
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
|
|
699
|
-
const { code, code_verifier, redirect_uri, resource } = ctx.body;
|
|
700
|
-
const resources = toResourceList(resource);
|
|
701
|
-
if (!client_id) throw new APIError("BAD_REQUEST", {
|
|
702
|
-
error_description: "client_id is required",
|
|
703
|
-
error: "invalid_request"
|
|
704
|
-
});
|
|
705
|
-
if (!code) throw new APIError("BAD_REQUEST", {
|
|
706
|
-
error_description: "code is required",
|
|
707
|
-
error: "invalid_request"
|
|
708
|
-
});
|
|
709
|
-
if (!redirect_uri) throw new APIError("BAD_REQUEST", {
|
|
710
|
-
error_description: "redirect_uri is required",
|
|
711
|
-
error: "invalid_request"
|
|
712
|
-
});
|
|
713
|
-
const isAuthCodeWithSecret = client_id && client_secret;
|
|
714
|
-
const isAuthCodeWithPkce = client_id && code && code_verifier;
|
|
715
|
-
if (!isAuthCodeWithSecret && !isAuthCodeWithPkce && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
|
|
716
|
-
error_description: "Either code_verifier or client_secret is required",
|
|
717
|
-
error: "invalid_request"
|
|
718
|
-
});
|
|
719
|
-
/** Get and check Verification Value */
|
|
720
|
-
const { verificationValue, effectiveResources, authorizedResources } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
|
|
721
|
-
const scopes = verificationValue.query.scope?.split(" ");
|
|
722
|
-
if (!scopes) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
723
|
-
error_description: "verification scope unset",
|
|
724
|
-
error: "invalid_scope"
|
|
725
|
-
});
|
|
726
|
-
/** Verify Client */
|
|
727
|
-
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerifiedClient, "authorization_code");
|
|
728
|
-
if (isPKCERequired(client, (verificationValue.query?.scope)?.split(" ") || [])) {
|
|
729
|
-
if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
|
|
730
|
-
error_description: "PKCE is required for this client",
|
|
731
|
-
error: "invalid_request"
|
|
732
|
-
});
|
|
733
|
-
} else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret || preVerifiedClient)) throw new APIError("BAD_REQUEST", {
|
|
734
|
-
error_description: "Either PKCE (code_verifier) or client authentication (client_secret or client_assertion) is required",
|
|
735
|
-
error: "invalid_request"
|
|
736
|
-
});
|
|
737
|
-
/** Check PKCE challenge if verifier is provided */
|
|
738
|
-
const pkceUsedInAuth = !!verificationValue.query?.code_challenge;
|
|
739
|
-
const pkceUsedInToken = !!code_verifier;
|
|
740
|
-
if (pkceUsedInAuth || pkceUsedInToken) {
|
|
741
|
-
if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
|
|
742
|
-
error_description: "code_verifier required because PKCE was used in authorization",
|
|
743
|
-
error: "invalid_request"
|
|
744
|
-
});
|
|
745
|
-
if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("UNAUTHORIZED", {
|
|
746
|
-
error_description: "code_verifier provided but PKCE was not used in authorization",
|
|
747
|
-
error: "invalid_request"
|
|
748
|
-
});
|
|
749
|
-
if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("UNAUTHORIZED", {
|
|
750
|
-
error_description: "code verification failed",
|
|
751
|
-
error: "invalid_request"
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
/** Get user */
|
|
755
|
-
if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
|
|
756
|
-
error_description: "missing user, user may have been deleted",
|
|
757
|
-
error: "invalid_user"
|
|
758
|
-
});
|
|
759
|
-
const user = await ctx.context.internalAdapter.findUserById(verificationValue.userId);
|
|
760
|
-
if (!user) throw new APIError("BAD_REQUEST", {
|
|
761
|
-
error_description: "missing user, user may have been deleted",
|
|
762
|
-
error: "invalid_user"
|
|
763
|
-
});
|
|
764
|
-
const session = await ctx.context.adapter.findOne({
|
|
765
|
-
model: "session",
|
|
766
|
-
where: [{
|
|
767
|
-
field: "id",
|
|
768
|
-
value: verificationValue.sessionId
|
|
769
|
-
}]
|
|
770
|
-
});
|
|
771
|
-
if (!session || session.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
|
|
772
|
-
error_description: "session no longer exists",
|
|
773
|
-
error: "invalid_request"
|
|
774
|
-
});
|
|
775
|
-
const authTime = verificationValue.authTime != null ? normalizeTimestampValue(verificationValue.authTime) : resolveSessionAuthTime(session);
|
|
776
|
-
return createUserTokens(ctx, opts, {
|
|
777
|
-
client,
|
|
778
|
-
scopes: verificationValue.query.scope?.split(" ") ?? [],
|
|
779
|
-
user,
|
|
780
|
-
grantType: "authorization_code",
|
|
781
|
-
referenceId: verificationValue.referenceId,
|
|
782
|
-
sessionId: session.id,
|
|
783
|
-
nonce: verificationValue.query?.nonce,
|
|
784
|
-
authTime,
|
|
785
|
-
verificationValue,
|
|
786
|
-
resources: effectiveResources,
|
|
787
|
-
originalResources: authorizedResources
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Grant that allows direct access to an API using the application's credentials
|
|
792
|
-
* This grant is for M2M so the concept of a user id does not exist on the token.
|
|
793
|
-
*
|
|
794
|
-
* MUST follow https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
|
|
795
|
-
*/
|
|
796
|
-
async function handleClientCredentialsGrant(ctx, opts) {
|
|
797
|
-
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
|
|
798
|
-
const { scope, resource } = ctx.body;
|
|
799
|
-
const resources = toResourceList(resource);
|
|
800
|
-
if (!client_id) throw new APIError("BAD_REQUEST", {
|
|
801
|
-
error_description: "Missing required client_id",
|
|
802
|
-
error: "invalid_grant"
|
|
803
|
-
});
|
|
804
|
-
if (!client_secret && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
|
|
805
|
-
error_description: "Missing a required client_secret",
|
|
806
|
-
error: "invalid_grant"
|
|
807
|
-
});
|
|
808
|
-
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient, "client_credentials");
|
|
809
|
-
let requestedScopes = scope?.split(" ");
|
|
810
|
-
if (requestedScopes) {
|
|
811
|
-
const validScopes = new Set(client.scopes ?? opts.scopes);
|
|
812
|
-
const oidcScopes = new Set([
|
|
813
|
-
"openid",
|
|
814
|
-
"profile",
|
|
815
|
-
"email",
|
|
816
|
-
"offline_access"
|
|
817
|
-
]);
|
|
818
|
-
const invalidScopes = requestedScopes.filter((scope) => {
|
|
819
|
-
return !validScopes?.has(scope) || oidcScopes.has(scope);
|
|
820
|
-
});
|
|
821
|
-
if (invalidScopes.length) throw new APIError("BAD_REQUEST", {
|
|
822
|
-
error_description: `The following scopes are invalid: ${invalidScopes.join(", ")}`,
|
|
823
|
-
error: "invalid_scope"
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
if (!requestedScopes) requestedScopes = client.scopes ?? opts.clientCredentialGrantDefaultScopes ?? opts.scopes ?? [];
|
|
827
|
-
return createUserTokens(ctx, opts, {
|
|
828
|
-
client,
|
|
829
|
-
scopes: requestedScopes,
|
|
830
|
-
grantType: "client_credentials",
|
|
831
|
-
resources
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
/**
|
|
835
|
-
* Obtains new Session Jwt and Refresh Tokens using a refresh token
|
|
836
|
-
*
|
|
837
|
-
* Refresh tokens will only allow the same or lesser scopes as the initial authorize request.
|
|
838
|
-
* To add scopes, you must restart the authorize process again.
|
|
839
|
-
*/
|
|
840
|
-
async function handleRefreshTokenGrant(ctx, opts) {
|
|
841
|
-
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
|
|
842
|
-
const { refresh_token, scope, resource } = ctx.body;
|
|
843
|
-
const resources = toResourceList(resource);
|
|
844
|
-
if (!client_id) throw new APIError("BAD_REQUEST", {
|
|
845
|
-
error_description: "Missing required client_id",
|
|
846
|
-
error: "invalid_grant"
|
|
847
|
-
});
|
|
848
|
-
if (!refresh_token) throw new APIError("BAD_REQUEST", {
|
|
849
|
-
error_description: "Missing a required refresh_token for refresh_token grant",
|
|
850
|
-
error: "invalid_grant"
|
|
851
|
-
});
|
|
852
|
-
const decodedRefresh = await decodeRefreshToken(opts, refresh_token);
|
|
853
|
-
const refreshToken = await ctx.context.adapter.findOne({
|
|
854
|
-
model: "oauthRefreshToken",
|
|
855
|
-
where: [{
|
|
856
|
-
field: "token",
|
|
857
|
-
value: await getStoredToken(opts.storeTokens, decodedRefresh.token, "refresh_token")
|
|
858
|
-
}]
|
|
859
|
-
});
|
|
860
|
-
if (!refreshToken) throw new APIError("BAD_REQUEST", {
|
|
861
|
-
error_description: "session not found",
|
|
862
|
-
error: "invalid_grant"
|
|
863
|
-
});
|
|
864
|
-
if (refreshToken.clientId !== client_id) throw new APIError("BAD_REQUEST", {
|
|
865
|
-
error_description: "invalid client_id",
|
|
866
|
-
error: "invalid_client"
|
|
867
|
-
});
|
|
868
|
-
if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
|
|
869
|
-
error_description: "invalid refresh token",
|
|
870
|
-
error: "invalid_grant"
|
|
871
|
-
});
|
|
872
|
-
if (refreshToken.revoked) {
|
|
873
|
-
await invalidateRefreshFamily(ctx, client_id, refreshToken.userId);
|
|
874
|
-
throw new APIError("BAD_REQUEST", {
|
|
875
|
-
error_description: "invalid refresh token",
|
|
876
|
-
error: "invalid_grant"
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
if (resources && refreshToken.resources && !resources.every((v) => refreshToken.resources?.includes(v))) throw new APIError("BAD_REQUEST", {
|
|
880
|
-
error_description: "requested resource invalid",
|
|
881
|
-
error: "invalid_target"
|
|
882
|
-
});
|
|
883
|
-
const scopes = refreshToken?.scopes;
|
|
884
|
-
const requestedScopes = scope?.split(" ");
|
|
885
|
-
if (requestedScopes) {
|
|
886
|
-
const validScopes = new Set(scopes);
|
|
887
|
-
for (const requestedScope of requestedScopes) if (!validScopes.has(requestedScope)) throw new APIError("BAD_REQUEST", {
|
|
888
|
-
error_description: `unable to issue scope ${requestedScope}`,
|
|
889
|
-
error: "invalid_scope"
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes, preVerifiedClient, "refresh_token");
|
|
893
|
-
const user = await ctx.context.internalAdapter.findUserById(refreshToken.userId);
|
|
894
|
-
if (!user) throw new APIError("BAD_REQUEST", {
|
|
895
|
-
error_description: "user not found",
|
|
896
|
-
error: "invalid_request"
|
|
897
|
-
});
|
|
898
|
-
const authTime = refreshToken.authTime != null ? normalizeTimestampValue(refreshToken.authTime) : void 0;
|
|
899
|
-
return createUserTokens(ctx, opts, {
|
|
900
|
-
client,
|
|
901
|
-
scopes: requestedScopes ?? scopes,
|
|
902
|
-
user,
|
|
903
|
-
grantType: "refresh_token",
|
|
904
|
-
referenceId: refreshToken.referenceId,
|
|
905
|
-
sessionId: refreshToken.sessionId,
|
|
906
|
-
refreshToken,
|
|
907
|
-
resources: resources ?? refreshToken.resources,
|
|
908
|
-
authTime
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
//#endregion
|
|
912
|
-
//#region src/introspect.ts
|
|
913
|
-
/**
|
|
914
|
-
* IMPORTANT NOTES:
|
|
915
|
-
* Introspection follows RFC7662
|
|
916
|
-
* https://datatracker.ietf.org/doc/html/rfc7662
|
|
917
|
-
* - APIError: Continue catches (returnable to client)
|
|
918
|
-
* - Error: Should immediately stop catches (internal error)
|
|
919
|
-
*/
|
|
920
|
-
/**
|
|
921
|
-
* Validates a JWT access token against the configured JWKs.
|
|
922
|
-
*
|
|
923
|
-
* @returns RFC7662 introspection format
|
|
924
|
-
*/
|
|
925
|
-
async function validateJwtAccessToken(ctx, opts, token, clientId) {
|
|
926
|
-
const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
|
|
927
|
-
const jwtPluginOptions = jwtPlugin?.options;
|
|
928
|
-
let jwtPayload;
|
|
929
|
-
try {
|
|
930
|
-
jwtPayload = await verifyJwsAccessToken(token, {
|
|
931
|
-
jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
|
|
932
|
-
return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
|
|
933
|
-
},
|
|
934
|
-
verifyOptions: {
|
|
935
|
-
audience: opts.validAudiences ?? ctx.context.baseURL,
|
|
936
|
-
issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL
|
|
937
|
-
}
|
|
938
|
-
});
|
|
939
|
-
} catch (error) {
|
|
940
|
-
if (error instanceof Error) {
|
|
941
|
-
if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
|
|
942
|
-
error_description: "invalid JWT signature",
|
|
943
|
-
error: "invalid_request"
|
|
944
|
-
});
|
|
945
|
-
else if (error.name === "JWTExpired") return { active: false };
|
|
946
|
-
else if (error.name === "JWTInvalid") return { active: false };
|
|
947
|
-
throw error;
|
|
948
|
-
}
|
|
949
|
-
throw new Error(error);
|
|
950
|
-
}
|
|
951
|
-
if (!jwtPayload.azp) return { active: false };
|
|
952
|
-
const client = await getClient(ctx, opts, jwtPayload.azp);
|
|
953
|
-
if (!client || client?.disabled) return { active: false };
|
|
954
|
-
if (clientId && jwtPayload.azp !== clientId) return { active: false };
|
|
955
|
-
const sessionId = jwtPayload.sid;
|
|
956
|
-
if (sessionId) {
|
|
957
|
-
const session = await ctx.context.adapter.findOne({
|
|
958
|
-
model: "session",
|
|
959
|
-
where: [{
|
|
960
|
-
field: "id",
|
|
961
|
-
value: sessionId
|
|
962
|
-
}]
|
|
963
|
-
});
|
|
964
|
-
if (!session || session.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
|
|
965
|
-
}
|
|
966
|
-
jwtPayload.client_id = jwtPayload.azp;
|
|
967
|
-
jwtPayload.active = true;
|
|
968
|
-
return jwtPayload;
|
|
969
|
-
}
|
|
970
|
-
/**
|
|
971
|
-
* Searches for an opaque access token in the database and validates it
|
|
972
|
-
*
|
|
973
|
-
* @returns RFC7662 introspection format
|
|
974
|
-
*/
|
|
975
|
-
async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
976
|
-
let tokenValue = token;
|
|
977
|
-
if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
|
|
978
|
-
else throw new APIError$1("BAD_REQUEST", {
|
|
979
|
-
error_description: "opaque access token not found",
|
|
980
|
-
error: "invalid_request"
|
|
981
|
-
});
|
|
982
|
-
const accessToken = await ctx.context.adapter.findOne({
|
|
983
|
-
model: "oauthAccessToken",
|
|
984
|
-
where: [{
|
|
985
|
-
field: "token",
|
|
986
|
-
value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
|
|
987
|
-
}]
|
|
988
|
-
});
|
|
989
|
-
if (!accessToken) throw new APIError$1("BAD_REQUEST", {
|
|
990
|
-
error_description: "opaque access token not found",
|
|
991
|
-
error: "invalid_token"
|
|
992
|
-
});
|
|
993
|
-
if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
|
|
994
|
-
if (accessToken.revoked) return { active: false };
|
|
995
|
-
let client;
|
|
996
|
-
if (accessToken.clientId) {
|
|
997
|
-
client = await getClient(ctx, opts, accessToken.clientId);
|
|
998
|
-
if (!client || client?.disabled) return { active: false };
|
|
999
|
-
if (clientId && accessToken.clientId !== clientId) return { active: false };
|
|
1000
|
-
}
|
|
1001
|
-
const sessionId = accessToken.sessionId ?? void 0;
|
|
1002
|
-
if (sessionId) {
|
|
1003
|
-
const session = await ctx.context.adapter.findOne({
|
|
1004
|
-
model: "session",
|
|
1005
|
-
where: [{
|
|
1006
|
-
field: "id",
|
|
1007
|
-
value: sessionId
|
|
1008
|
-
}]
|
|
1009
|
-
});
|
|
1010
|
-
if (!session || session.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
|
|
1011
|
-
}
|
|
1012
|
-
let user;
|
|
1013
|
-
if (accessToken.userId) user = await ctx.context.internalAdapter.findUserById(accessToken?.userId);
|
|
1014
|
-
const resources = Array.isArray(accessToken.resources) ? accessToken.resources : void 0;
|
|
1015
|
-
const audience = resources ? [...resources] : void 0;
|
|
1016
|
-
if (audience?.length && accessToken.scopes?.includes("openid")) {
|
|
1017
|
-
const userInfoEndpoint = `${ctx.context.baseURL}/oauth2/userinfo`;
|
|
1018
|
-
if (!audience.includes(userInfoEndpoint)) audience.push(userInfoEndpoint);
|
|
1019
|
-
}
|
|
1020
|
-
const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
|
|
1021
|
-
user,
|
|
1022
|
-
scopes: accessToken.scopes,
|
|
1023
|
-
referenceId: accessToken?.referenceId,
|
|
1024
|
-
resources,
|
|
1025
|
-
metadata: parseClientMetadata(client?.metadata)
|
|
1026
|
-
}) : {};
|
|
1027
|
-
const jwtPluginOptions = (opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options;
|
|
1028
|
-
return {
|
|
1029
|
-
...customClaims,
|
|
1030
|
-
active: true,
|
|
1031
|
-
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
1032
|
-
aud: toAudienceClaim(audience),
|
|
1033
|
-
client_id: accessToken.clientId,
|
|
1034
|
-
sub: user?.id,
|
|
1035
|
-
sid: sessionId,
|
|
1036
|
-
exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
|
|
1037
|
-
iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
|
|
1038
|
-
scope: accessToken.scopes?.join(" ")
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
/**
|
|
1042
|
-
* Validates a refresh token in the session store.
|
|
1043
|
-
*
|
|
1044
|
-
* @returns payload in RFC7662 introspection format
|
|
1045
|
-
*/
|
|
1046
|
-
async function validateRefreshToken(ctx, opts, token, clientId) {
|
|
1047
|
-
const refreshToken = await ctx.context.adapter.findOne({
|
|
1048
|
-
model: "oauthRefreshToken",
|
|
1049
|
-
where: [{
|
|
1050
|
-
field: "token",
|
|
1051
|
-
value: await getStoredToken(opts.storeTokens, token, "refresh_token")
|
|
1052
|
-
}]
|
|
1053
|
-
});
|
|
1054
|
-
if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
|
|
1055
|
-
error_description: "token not found",
|
|
1056
|
-
error: "invalid_token"
|
|
1057
|
-
});
|
|
1058
|
-
if (!refreshToken.clientId || refreshToken.clientId !== clientId) return { active: false };
|
|
1059
|
-
if (!refreshToken.expiresAt || refreshToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
|
|
1060
|
-
if (refreshToken.revoked) return { active: false };
|
|
1061
|
-
let sessionId = refreshToken.sessionId ?? void 0;
|
|
1062
|
-
if (sessionId) {
|
|
1063
|
-
const session = await ctx.context.adapter.findOne({
|
|
1064
|
-
model: "session",
|
|
1065
|
-
where: [{
|
|
1066
|
-
field: "id",
|
|
1067
|
-
value: sessionId
|
|
1068
|
-
}]
|
|
1069
|
-
});
|
|
1070
|
-
if (!session || session.expiresAt < /* @__PURE__ */ new Date()) sessionId = void 0;
|
|
1071
|
-
}
|
|
1072
|
-
let user = void 0;
|
|
1073
|
-
if (refreshToken.userId) user = await ctx.context.internalAdapter.findUserById(refreshToken?.userId) ?? void 0;
|
|
1074
|
-
return {
|
|
1075
|
-
active: true,
|
|
1076
|
-
client_id: clientId,
|
|
1077
|
-
iss: ((opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options)?.jwt?.issuer ?? ctx.context.baseURL,
|
|
1078
|
-
sub: user?.id,
|
|
1079
|
-
sid: sessionId,
|
|
1080
|
-
exp: Math.floor(new Date(refreshToken.expiresAt).getTime() / 1e3),
|
|
1081
|
-
iat: Math.floor(new Date(refreshToken.createdAt).getTime() / 1e3),
|
|
1082
|
-
scope: refreshToken.scopes?.join(" ")
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
/**
|
|
1086
|
-
* We don't know the access token format so we try to validate it
|
|
1087
|
-
* as a JWT first, then as an opaque token.
|
|
1088
|
-
*
|
|
1089
|
-
* @returns RFC7662 introspection format
|
|
1090
|
-
*
|
|
1091
|
-
* @internal
|
|
1092
|
-
*/
|
|
1093
|
-
async function validateAccessToken(ctx, opts, token, clientId) {
|
|
1094
|
-
try {
|
|
1095
|
-
return await validateJwtAccessToken(ctx, opts, token, clientId);
|
|
1096
|
-
} catch (err) {
|
|
1097
|
-
if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
|
|
1098
|
-
else throw new Error(err);
|
|
1099
|
-
}
|
|
1100
|
-
try {
|
|
1101
|
-
return await validateOpaqueAccessToken(ctx, opts, token, clientId);
|
|
1102
|
-
} catch (err) {
|
|
1103
|
-
if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
|
|
1104
|
-
else throw new Error("Unknown error validating access token");
|
|
1105
|
-
}
|
|
1106
|
-
throw new APIError$1("BAD_REQUEST", {
|
|
1107
|
-
error_description: "Invalid access token",
|
|
1108
|
-
error: "invalid_request"
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
/**
|
|
1112
|
-
* Resolves pairwise sub on an introspection payload.
|
|
1113
|
-
* Applied at the presentation layer so internal validation functions
|
|
1114
|
-
* keep real user.id (needed for user lookup in /userinfo).
|
|
1115
|
-
*/
|
|
1116
|
-
async function resolveIntrospectionSub(opts, payload, client) {
|
|
1117
|
-
if (payload.active && payload.sub) {
|
|
1118
|
-
const resolvedSub = await resolveSubjectIdentifier(payload.sub, client, opts);
|
|
1119
|
-
return {
|
|
1120
|
-
...payload,
|
|
1121
|
-
sub: resolvedSub
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
return payload;
|
|
1125
|
-
}
|
|
1126
|
-
async function introspectEndpoint(ctx, opts) {
|
|
1127
|
-
let { token, token_type_hint } = ctx.body;
|
|
1128
|
-
if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
|
|
1129
|
-
const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
|
|
1130
|
-
if (!client_id || !client_secret && !preVerifiedClient) throw new APIError$1("UNAUTHORIZED", {
|
|
1131
|
-
error_description: "missing required credentials",
|
|
1132
|
-
error: "invalid_client"
|
|
1133
|
-
});
|
|
1134
|
-
if (token && typeof token === "string" && token.startsWith("Bearer ")) token = token.replace("Bearer ", "");
|
|
1135
|
-
if (!token?.length) throw new APIError$1("BAD_REQUEST", {
|
|
1136
|
-
error_description: "missing a required token for introspection",
|
|
1137
|
-
error: "invalid_request"
|
|
1138
|
-
});
|
|
1139
|
-
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
|
|
1140
|
-
try {
|
|
1141
|
-
if (token_type_hint === void 0 || token_type_hint === "access_token") try {
|
|
1142
|
-
return resolveIntrospectionSub(opts, await validateAccessToken(ctx, opts, token, client.clientId), client);
|
|
1143
|
-
} catch (error) {
|
|
1144
|
-
if (error instanceof APIError$1) {
|
|
1145
|
-
if (token_type_hint === "access_token") throw error;
|
|
1146
|
-
} else if (error instanceof Error) throw error;
|
|
1147
|
-
else throw new Error(error);
|
|
1148
|
-
}
|
|
1149
|
-
if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
|
|
1150
|
-
return resolveIntrospectionSub(opts, await validateRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId), client);
|
|
1151
|
-
} catch (error) {
|
|
1152
|
-
if (error instanceof APIError$1) {
|
|
1153
|
-
if (token_type_hint === "refresh_token") throw error;
|
|
1154
|
-
} else if (error instanceof Error) throw error;
|
|
1155
|
-
else throw new Error(error);
|
|
1156
|
-
}
|
|
1157
|
-
throw new APIError$1("BAD_REQUEST", {
|
|
1158
|
-
error_description: "token not found",
|
|
1159
|
-
error: "invalid_request"
|
|
1160
|
-
});
|
|
1161
|
-
} catch (error) {
|
|
1162
|
-
if (error instanceof APIError$1) {
|
|
1163
|
-
if (error.name === "BAD_REQUEST") return { active: false };
|
|
1164
|
-
throw error;
|
|
1165
|
-
} else if (error instanceof Error) {
|
|
1166
|
-
logger.error("Introspection error:", error.message, error.stack);
|
|
1167
|
-
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
1168
|
-
} else {
|
|
1169
|
-
logger.error("Introspection error:", error);
|
|
1170
|
-
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
//#endregion
|
|
1175
163
|
//#region src/logout.ts
|
|
1176
164
|
const BACKCHANNEL_LOGOUT_EVENT_URI = "http://schemas.openid.net/event/backchannel-logout";
|
|
1177
165
|
const LOGOUT_TOKEN_JWT_TYP = "logout+jwt";
|
|
@@ -1440,6 +428,155 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
|
|
|
1440
428
|
}
|
|
1441
429
|
}
|
|
1442
430
|
//#endregion
|
|
431
|
+
//#region src/metadata.ts
|
|
432
|
+
function authServerMetadata(ctx, opts, overrides) {
|
|
433
|
+
const baseURL = ctx.context.baseURL;
|
|
434
|
+
const backchannelSupported = !overrides?.jwt_disabled;
|
|
435
|
+
return {
|
|
436
|
+
scopes_supported: overrides?.scopes_supported,
|
|
437
|
+
issuer: validateIssuerUrl(opts?.jwt?.issuer ?? baseURL),
|
|
438
|
+
authorization_endpoint: `${baseURL}/oauth2/authorize`,
|
|
439
|
+
token_endpoint: `${baseURL}/oauth2/token`,
|
|
440
|
+
jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
|
|
441
|
+
registration_endpoint: overrides?.dynamic_client_registration_supported ? `${baseURL}/oauth2/register` : void 0,
|
|
442
|
+
introspection_endpoint: `${baseURL}/oauth2/introspect`,
|
|
443
|
+
revocation_endpoint: `${baseURL}/oauth2/revoke`,
|
|
444
|
+
response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
|
|
445
|
+
response_modes_supported: ["query"],
|
|
446
|
+
grant_types_supported: overrides?.grant_types_supported ?? [
|
|
447
|
+
"authorization_code",
|
|
448
|
+
"client_credentials",
|
|
449
|
+
"refresh_token"
|
|
450
|
+
],
|
|
451
|
+
token_endpoint_auth_methods_supported: overrides?.token_endpoint_auth_methods_supported ?? [
|
|
452
|
+
...overrides?.public_client_supported ? ["none"] : [],
|
|
453
|
+
"client_secret_basic",
|
|
454
|
+
"client_secret_post",
|
|
455
|
+
"private_key_jwt"
|
|
456
|
+
],
|
|
457
|
+
token_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
|
|
458
|
+
introspection_endpoint_auth_methods_supported: overrides?.endpoint_auth_methods_supported ?? [
|
|
459
|
+
"client_secret_basic",
|
|
460
|
+
"client_secret_post",
|
|
461
|
+
"private_key_jwt"
|
|
462
|
+
],
|
|
463
|
+
introspection_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
|
|
464
|
+
revocation_endpoint_auth_methods_supported: overrides?.endpoint_auth_methods_supported ?? [
|
|
465
|
+
"client_secret_basic",
|
|
466
|
+
"client_secret_post",
|
|
467
|
+
"private_key_jwt"
|
|
468
|
+
],
|
|
469
|
+
revocation_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
|
|
470
|
+
code_challenge_methods_supported: ["S256"],
|
|
471
|
+
authorization_response_iss_parameter_supported: true,
|
|
472
|
+
dpop_signing_alg_values_supported: overrides?.dpop_signing_alg_values_supported ?? [...DPOP_SIGNING_ALGORITHMS],
|
|
473
|
+
backchannel_logout_supported: backchannelSupported,
|
|
474
|
+
backchannel_logout_session_supported: backchannelSupported
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Builds the authorization-server metadata shared by the
|
|
479
|
+
* `/.well-known/oauth-authorization-server` and `/.well-known/openid-configuration`
|
|
480
|
+
* responses, plus the inputs both need to finish their own document.
|
|
481
|
+
*/
|
|
482
|
+
function buildAuthServerMetadata(ctx, opts) {
|
|
483
|
+
const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
|
|
484
|
+
const clientDiscoveries = getClientDiscoveries(opts);
|
|
485
|
+
const publicClientSupported = opts.allowUnauthenticatedClientRegistration || clientDiscoveries.length > 0;
|
|
486
|
+
return {
|
|
487
|
+
jwtPluginOptions,
|
|
488
|
+
clientDiscoveries,
|
|
489
|
+
authMetadata: authServerMetadata(ctx, jwtPluginOptions, {
|
|
490
|
+
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
491
|
+
dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
|
|
492
|
+
public_client_supported: publicClientSupported,
|
|
493
|
+
grant_types_supported: getSupportedGrantTypes(opts),
|
|
494
|
+
token_endpoint_auth_methods_supported: getSupportedAuthMethods(opts, { includeNone: publicClientSupported }),
|
|
495
|
+
endpoint_auth_methods_supported: getSupportedAuthMethods(opts),
|
|
496
|
+
jwt_disabled: opts.disableJwtPlugin,
|
|
497
|
+
dpop_signing_alg_values_supported: opts.dpop?.signingAlgorithms
|
|
498
|
+
})
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function oauthAuthorizationServerMetadata(ctx, opts) {
|
|
502
|
+
const { clientDiscoveries, authMetadata } = buildAuthServerMetadata(ctx, opts);
|
|
503
|
+
return applyOAuthProviderMetadataExtensions(ctx, opts, "oauth-authorization-server", {
|
|
504
|
+
...mergeDiscoveryMetadata(clientDiscoveries),
|
|
505
|
+
...authMetadata
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
function oidcServerMetadata(ctx, opts) {
|
|
509
|
+
const baseURL = ctx.context.baseURL;
|
|
510
|
+
const { jwtPluginOptions, clientDiscoveries, authMetadata } = buildAuthServerMetadata(ctx, opts);
|
|
511
|
+
const metadata = {
|
|
512
|
+
...authMetadata,
|
|
513
|
+
claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
|
|
514
|
+
userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
|
|
515
|
+
subject_types_supported: opts.pairwiseSecret ? ["public", "pairwise"] : ["public"],
|
|
516
|
+
id_token_signing_alg_values_supported: (() => {
|
|
517
|
+
if (opts.disableJwtPlugin) return ["HS256"];
|
|
518
|
+
const primary = jwtPluginOptions?.jwks?.keyPairConfig?.alg ?? "EdDSA";
|
|
519
|
+
const extras = jwtPluginOptions?.jwks?.keyPairConfigs?.map((c) => c.alg) ?? [];
|
|
520
|
+
return Array.from(new Set([primary, ...extras]));
|
|
521
|
+
})(),
|
|
522
|
+
end_session_endpoint: `${baseURL}/oauth2/end-session`,
|
|
523
|
+
acr_values_supported: ["urn:mace:incommon:iap:bronze"],
|
|
524
|
+
prompt_values_supported: [
|
|
525
|
+
"login",
|
|
526
|
+
"consent",
|
|
527
|
+
"create",
|
|
528
|
+
"select_account",
|
|
529
|
+
"none"
|
|
530
|
+
]
|
|
531
|
+
};
|
|
532
|
+
return applyOAuthProviderMetadataExtensions(ctx, opts, "openid-configuration", {
|
|
533
|
+
...mergeDiscoveryMetadata(clientDiscoveries),
|
|
534
|
+
...metadata
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
const METADATA_CACHE_CONTROL = "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400";
|
|
538
|
+
function metadataResponse(body, extraHeaders) {
|
|
539
|
+
const headers = new Headers(extraHeaders);
|
|
540
|
+
if (!headers.has("Cache-Control")) headers.set("Cache-Control", METADATA_CACHE_CONTROL);
|
|
541
|
+
headers.set("Content-Type", "application/json");
|
|
542
|
+
return new Response(JSON.stringify(body), {
|
|
543
|
+
status: 200,
|
|
544
|
+
headers
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Provides an exportable `/.well-known/oauth-authorization-server`.
|
|
549
|
+
*
|
|
550
|
+
* Useful when basePath prevents the endpoint from being located at the root
|
|
551
|
+
* and must be provided manually.
|
|
552
|
+
*
|
|
553
|
+
* @external
|
|
554
|
+
*/
|
|
555
|
+
const oauthProviderAuthServerMetadata = (auth, opts) => {
|
|
556
|
+
return async (request) => {
|
|
557
|
+
return metadataResponse(await auth.api.getOAuthServerConfig({
|
|
558
|
+
request,
|
|
559
|
+
asResponse: false
|
|
560
|
+
}), opts?.headers);
|
|
561
|
+
};
|
|
562
|
+
};
|
|
563
|
+
/**
|
|
564
|
+
* Provides an exportable `/.well-known/openid-configuration`.
|
|
565
|
+
*
|
|
566
|
+
* Useful when basePath prevents the endpoint from being located at the root
|
|
567
|
+
* and must be provided manually.
|
|
568
|
+
*
|
|
569
|
+
* @external
|
|
570
|
+
*/
|
|
571
|
+
const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
|
|
572
|
+
return async (request) => {
|
|
573
|
+
return metadataResponse(await auth.api.getOpenIdConfig({
|
|
574
|
+
request,
|
|
575
|
+
asResponse: false
|
|
576
|
+
}), opts?.headers);
|
|
577
|
+
};
|
|
578
|
+
};
|
|
579
|
+
//#endregion
|
|
1443
580
|
//#region src/oauth-endpoint.ts
|
|
1444
581
|
/**
|
|
1445
582
|
* Wraps `createAuthEndpoint` so zod schemas stay the single source of truth
|
|
@@ -1572,6 +709,64 @@ async function assertClientPrivileges(ctx, session, opts, action) {
|
|
|
1572
709
|
})) throw new APIError("UNAUTHORIZED");
|
|
1573
710
|
}
|
|
1574
711
|
//#endregion
|
|
712
|
+
//#region src/utils/initial-access-token.ts
|
|
713
|
+
/**
|
|
714
|
+
* Builds an RFC 6750 §3 Bearer challenge for the registration endpoint. The
|
|
715
|
+
* `error` code maps to the status per RFC 6750 §3.1 (`invalid_request` → 400,
|
|
716
|
+
* `invalid_token` → 401) and is echoed in both the `WWW-Authenticate` challenge
|
|
717
|
+
* and the JSON body.
|
|
718
|
+
*
|
|
719
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-3
|
|
720
|
+
*/
|
|
721
|
+
function registrationBearerError(status, error, errorDescription) {
|
|
722
|
+
return new APIError$1(status, {
|
|
723
|
+
error,
|
|
724
|
+
error_description: errorDescription
|
|
725
|
+
}, {
|
|
726
|
+
"WWW-Authenticate": `Bearer error="${error}"`,
|
|
727
|
+
...NO_STORE_HEADERS
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Resolves an RFC 7591 initial access token from the request and authorizes it
|
|
732
|
+
* against the deployment-supplied validator.
|
|
733
|
+
*
|
|
734
|
+
* Returns the authorization (optionally carrying a `referenceId`) when a valid
|
|
735
|
+
* token is present, or `undefined` when no Bearer credentials were sent so the
|
|
736
|
+
* caller can fall back to session or open registration. Throws an RFC 6750
|
|
737
|
+
* Bearer challenge when the header is malformed, no validator is configured, or
|
|
738
|
+
* the token is rejected.
|
|
739
|
+
*
|
|
740
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7591#section-3
|
|
741
|
+
*/
|
|
742
|
+
async function authorizeInitialAccessToken(ctx, opts, clientMetadata) {
|
|
743
|
+
const headers = ctx.headers;
|
|
744
|
+
let initialAccessToken;
|
|
745
|
+
try {
|
|
746
|
+
initialAccessToken = parseBearerToken(headers?.get("authorization"));
|
|
747
|
+
} catch {
|
|
748
|
+
throw registrationBearerError("BAD_REQUEST", "invalid_request", "Malformed initial access token Authorization header");
|
|
749
|
+
}
|
|
750
|
+
if (!initialAccessToken || !headers) return;
|
|
751
|
+
if (!opts.validateInitialAccessToken) throw registrationBearerError("UNAUTHORIZED", "invalid_token", "Invalid initial access token");
|
|
752
|
+
let authorization;
|
|
753
|
+
try {
|
|
754
|
+
authorization = await opts.validateInitialAccessToken({
|
|
755
|
+
initialAccessToken,
|
|
756
|
+
headers,
|
|
757
|
+
clientMetadata
|
|
758
|
+
});
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if (error instanceof APIError$1) throw error;
|
|
761
|
+
throw new APIError$1("INTERNAL_SERVER_ERROR", {
|
|
762
|
+
error: "server_error",
|
|
763
|
+
error_description: "Initial access token validation failed"
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
if (!authorization) throw registrationBearerError("UNAUTHORIZED", "invalid_token", "Invalid initial access token");
|
|
767
|
+
return authorization;
|
|
768
|
+
}
|
|
769
|
+
//#endregion
|
|
1575
770
|
//#region src/register.ts
|
|
1576
771
|
/**
|
|
1577
772
|
* Resolves the auth method and type for unauthenticated DCR.
|
|
@@ -1588,18 +783,42 @@ function resolveUnauthenticatedAuth(body) {
|
|
|
1588
783
|
type: body.type === "web" ? void 0 : body.type
|
|
1589
784
|
};
|
|
1590
785
|
}
|
|
786
|
+
const DEFAULT_REGISTRATION_GRANT_TYPES = ["authorization_code"];
|
|
787
|
+
function resolveRegistrationGrantTypes(client) {
|
|
788
|
+
const grantTypes = client.grant_types ?? [...DEFAULT_REGISTRATION_GRANT_TYPES];
|
|
789
|
+
if (grantTypes.length > 0) return grantTypes;
|
|
790
|
+
throw new APIError("BAD_REQUEST", {
|
|
791
|
+
error: "invalid_client_metadata",
|
|
792
|
+
error_description: "grant_types must contain at least one grant type"
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
function resolveRegistrationResponseTypes(client, grantTypes) {
|
|
796
|
+
if (client.response_types) return client.response_types;
|
|
797
|
+
return grantTypes.includes("authorization_code") ? ["code"] : void 0;
|
|
798
|
+
}
|
|
799
|
+
function applyOAuthClientRegistrationDefaults(client) {
|
|
800
|
+
const grantTypes = resolveRegistrationGrantTypes(client);
|
|
801
|
+
return {
|
|
802
|
+
...client,
|
|
803
|
+
token_endpoint_auth_method: client.token_endpoint_auth_method ?? "client_secret_basic",
|
|
804
|
+
grant_types: grantTypes,
|
|
805
|
+
response_types: resolveRegistrationResponseTypes(client, grantTypes)
|
|
806
|
+
};
|
|
807
|
+
}
|
|
1591
808
|
async function registerEndpoint(ctx, opts) {
|
|
809
|
+
const body = ctx.body;
|
|
1592
810
|
if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
|
|
1593
811
|
error: "access_denied",
|
|
1594
812
|
error_description: "Client registration is disabled"
|
|
1595
813
|
});
|
|
1596
|
-
const body = ctx.body;
|
|
1597
814
|
const session = await getSessionFromCtx(ctx);
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
815
|
+
const tokenAuthorization = session ? void 0 : await authorizeInitialAccessToken(ctx, opts, body);
|
|
816
|
+
const isTokenAuthorized = Boolean(tokenAuthorization);
|
|
817
|
+
if (!(session || isTokenAuthorized || opts.allowUnauthenticatedClientRegistration)) throw new APIError("UNAUTHORIZED", { error_description: "Authentication required for client registration" }, {
|
|
818
|
+
"WWW-Authenticate": "Bearer",
|
|
819
|
+
...NO_STORE_HEADERS
|
|
1601
820
|
});
|
|
1602
|
-
if (!session) {
|
|
821
|
+
if (!session && !isTokenAuthorized) {
|
|
1603
822
|
if (body.grant_types?.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
|
|
1604
823
|
error: "invalid_client_metadata",
|
|
1605
824
|
error_description: "client_credentials grant requires authenticated registration"
|
|
@@ -1609,47 +828,83 @@ async function registerEndpoint(ctx, opts) {
|
|
|
1609
828
|
body.type = resolved.type;
|
|
1610
829
|
}
|
|
1611
830
|
if (!body.scope) body.scope = (opts.clientRegistrationDefaultScopes ?? opts.scopes)?.join(" ");
|
|
1612
|
-
|
|
831
|
+
const requestedResources = Array.isArray(body.resources) ? [...new Set(body.resources.filter((resource) => typeof resource === "string" && resource.length > 0))] : [];
|
|
832
|
+
if (requestedResources.length > 0) for (const identifier of requestedResources) {
|
|
833
|
+
const row = await getResource(ctx, opts, identifier);
|
|
834
|
+
if (!row) throw new APIError("BAD_REQUEST", {
|
|
835
|
+
error: "invalid_target",
|
|
836
|
+
error_description: `requested resource ${identifier} does not exist`
|
|
837
|
+
});
|
|
838
|
+
if (row.disabled) throw new APIError("BAD_REQUEST", {
|
|
839
|
+
error: "invalid_target",
|
|
840
|
+
error_description: `requested resource ${identifier} is disabled`
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return createOAuthClientEndpoint(ctx, opts, {
|
|
844
|
+
isRegister: true,
|
|
845
|
+
session,
|
|
846
|
+
referenceId: tokenAuthorization?.referenceId,
|
|
847
|
+
resources: requestedResources.length > 0 ? requestedResources : void 0
|
|
848
|
+
});
|
|
1613
849
|
}
|
|
1614
850
|
async function checkOAuthClient(client, opts, settings) {
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1617
|
-
|
|
851
|
+
const clientWithDefaults = applyOAuthClientRegistrationDefaults(client);
|
|
852
|
+
const isPublic = clientWithDefaults.token_endpoint_auth_method === "none";
|
|
853
|
+
const tokenEndpointAuthMethod = clientWithDefaults.token_endpoint_auth_method ?? "client_secret_basic";
|
|
854
|
+
if (!new Set(getSupportedAuthMethods(opts, { includeNone: true })).has(tokenEndpointAuthMethod)) throw new APIError("BAD_REQUEST", {
|
|
855
|
+
error: "invalid_client_metadata",
|
|
856
|
+
error_description: `unsupported token_endpoint_auth_method ${tokenEndpointAuthMethod}`
|
|
857
|
+
});
|
|
858
|
+
if (clientWithDefaults.dpop_bound_access_tokens !== void 0 && typeof clientWithDefaults.dpop_bound_access_tokens !== "boolean") throw new APIError("BAD_REQUEST", {
|
|
859
|
+
error: "invalid_client_metadata",
|
|
860
|
+
error_description: "dpop_bound_access_tokens must be a boolean"
|
|
861
|
+
});
|
|
862
|
+
if (clientWithDefaults.type) {
|
|
863
|
+
if (isPublic && !(clientWithDefaults.type === "native" || clientWithDefaults.type === "user-agent-based")) throw new APIError("BAD_REQUEST", {
|
|
1618
864
|
error: "invalid_client_metadata",
|
|
1619
865
|
error_description: `Type must be 'native' or 'user-agent-based' for public applications`
|
|
1620
866
|
});
|
|
1621
|
-
else if (!isPublic && !(
|
|
867
|
+
else if (!isPublic && !(clientWithDefaults.type === "web")) throw new APIError("BAD_REQUEST", {
|
|
1622
868
|
error: "invalid_client_metadata",
|
|
1623
869
|
error_description: `Type must be 'web' for confidential applications`
|
|
1624
870
|
});
|
|
1625
871
|
}
|
|
1626
|
-
|
|
872
|
+
const grantTypes = clientWithDefaults.grant_types ?? [];
|
|
873
|
+
const responseTypes = clientWithDefaults.response_types;
|
|
874
|
+
if (grantTypes.includes("authorization_code") && (!clientWithDefaults.redirect_uris || clientWithDefaults.redirect_uris.length === 0)) throw new APIError("BAD_REQUEST", {
|
|
1627
875
|
error: "invalid_redirect_uri",
|
|
1628
876
|
error_description: "Redirect URIs are required for authorization_code and implicit grant types"
|
|
1629
877
|
});
|
|
1630
|
-
const
|
|
1631
|
-
const
|
|
1632
|
-
|
|
878
|
+
const supportedGrantTypes = new Set(getSupportedGrantTypes(opts));
|
|
879
|
+
for (const grantType of grantTypes) if (!supportedGrantTypes.has(grantType)) throw new APIError("BAD_REQUEST", {
|
|
880
|
+
error: "invalid_client_metadata",
|
|
881
|
+
error_description: `unsupported grant_type ${grantType}`
|
|
882
|
+
});
|
|
883
|
+
if (grantTypes.includes("authorization_code") && !responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
|
|
1633
884
|
error: "invalid_client_metadata",
|
|
1634
885
|
error_description: "When 'authorization_code' grant type is used, 'code' response type must be included"
|
|
1635
886
|
});
|
|
1636
|
-
if (
|
|
1637
|
-
|
|
887
|
+
if (!grantTypes.includes("authorization_code") && responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
|
|
888
|
+
error: "invalid_client_metadata",
|
|
889
|
+
error_description: "When 'code' response type is used, 'authorization_code' grant type must be included"
|
|
890
|
+
});
|
|
891
|
+
if (clientWithDefaults.subject_type !== void 0) {
|
|
892
|
+
if (clientWithDefaults.subject_type !== "public" && clientWithDefaults.subject_type !== "pairwise") throw new APIError("BAD_REQUEST", {
|
|
1638
893
|
error: "invalid_client_metadata",
|
|
1639
894
|
error_description: `subject_type must be "public" or "pairwise"`
|
|
1640
895
|
});
|
|
1641
|
-
if (
|
|
896
|
+
if (clientWithDefaults.subject_type === "pairwise" && !opts.pairwiseSecret) throw new APIError("BAD_REQUEST", {
|
|
1642
897
|
error: "invalid_client_metadata",
|
|
1643
898
|
error_description: "pairwise subject_type requires server pairwiseSecret configuration"
|
|
1644
899
|
});
|
|
1645
|
-
if (
|
|
1646
|
-
if (new Set(
|
|
900
|
+
if (clientWithDefaults.subject_type === "pairwise" && clientWithDefaults.redirect_uris && clientWithDefaults.redirect_uris.length > 1) {
|
|
901
|
+
if (new Set(clientWithDefaults.redirect_uris.map((uri) => new URL(uri).host)).size > 1) throw new APIError("BAD_REQUEST", {
|
|
1647
902
|
error: "invalid_client_metadata",
|
|
1648
903
|
error_description: "pairwise clients with redirect_uris on different hosts require a sector_identifier_uri, which is not yet supported. All redirect_uris must share the same host."
|
|
1649
904
|
});
|
|
1650
905
|
}
|
|
1651
906
|
}
|
|
1652
|
-
const requestedScopes = (
|
|
907
|
+
const requestedScopes = (clientWithDefaults?.scope)?.split(" ").filter((v) => v.length);
|
|
1653
908
|
const allowedScopes = settings?.isRegister ? opts.clientRegistrationAllowedScopes ?? opts.scopes : opts.scopes;
|
|
1654
909
|
if (allowedScopes) {
|
|
1655
910
|
const validScopes = new Set(allowedScopes);
|
|
@@ -1658,21 +913,22 @@ async function checkOAuthClient(client, opts, settings) {
|
|
|
1658
913
|
error_description: `cannot request scope ${requestedScope}`
|
|
1659
914
|
});
|
|
1660
915
|
}
|
|
1661
|
-
if (settings?.isRegister &&
|
|
916
|
+
if (settings?.isRegister && clientWithDefaults.require_pkce === false) throw new APIError("BAD_REQUEST", {
|
|
1662
917
|
error: "invalid_client_metadata",
|
|
1663
918
|
error_description: `pkce is required for registered clients.`
|
|
1664
919
|
});
|
|
1665
|
-
|
|
1666
|
-
|
|
920
|
+
const usesAssertionKeyMaterial = tokenEndpointAuthMethod === "private_key_jwt" || isExtensionTokenEndpointAuthMethod(opts, tokenEndpointAuthMethod);
|
|
921
|
+
if (clientWithDefaults.jwks || clientWithDefaults.jwks_uri) {
|
|
922
|
+
if (!usesAssertionKeyMaterial) throw new APIError("BAD_REQUEST", {
|
|
1667
923
|
error: "invalid_client_metadata",
|
|
1668
|
-
error_description: "jwks and jwks_uri are
|
|
924
|
+
error_description: "jwks and jwks_uri are only allowed with private_key_jwt or an assertion-based authentication method"
|
|
1669
925
|
});
|
|
1670
|
-
if (
|
|
926
|
+
if (clientWithDefaults.jwks && clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
|
|
1671
927
|
error: "invalid_client_metadata",
|
|
1672
|
-
error_description: "
|
|
928
|
+
error_description: "jwks and jwks_uri are mutually exclusive"
|
|
1673
929
|
});
|
|
1674
|
-
if (
|
|
1675
|
-
const uri = new URL(
|
|
930
|
+
if (clientWithDefaults.jwks_uri) try {
|
|
931
|
+
const uri = new URL(clientWithDefaults.jwks_uri);
|
|
1676
932
|
if (uri.protocol !== "https:") throw new APIError("BAD_REQUEST", {
|
|
1677
933
|
error: "invalid_client_metadata",
|
|
1678
934
|
error_description: "jwks_uri must use HTTPS"
|
|
@@ -1692,25 +948,26 @@ async function checkOAuthClient(client, opts, settings) {
|
|
|
1692
948
|
error_description: "jwks_uri must be a valid URL"
|
|
1693
949
|
});
|
|
1694
950
|
}
|
|
1695
|
-
if (
|
|
1696
|
-
const keys = Array.isArray(
|
|
951
|
+
if (clientWithDefaults.jwks) {
|
|
952
|
+
const keys = Array.isArray(clientWithDefaults.jwks) ? clientWithDefaults.jwks : clientWithDefaults.jwks.keys;
|
|
1697
953
|
if (!Array.isArray(keys) || keys.length === 0) throw new APIError("BAD_REQUEST", {
|
|
1698
954
|
error: "invalid_client_metadata",
|
|
1699
955
|
error_description: "jwks must be a non-empty array of JWK objects or a JWKS document {keys:[...]}"
|
|
1700
956
|
});
|
|
1701
957
|
}
|
|
1702
|
-
}
|
|
958
|
+
}
|
|
959
|
+
if (tokenEndpointAuthMethod === "private_key_jwt" && !clientWithDefaults.jwks && !clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
|
|
1703
960
|
error: "invalid_client_metadata",
|
|
1704
|
-
error_description: "
|
|
961
|
+
error_description: "private_key_jwt requires either jwks or jwks_uri"
|
|
1705
962
|
});
|
|
1706
|
-
if (
|
|
963
|
+
if (clientWithDefaults.backchannel_logout_uri !== void 0) {
|
|
1707
964
|
if (opts.disableJwtPlugin) throw new APIError("BAD_REQUEST", {
|
|
1708
965
|
error: "invalid_client_metadata",
|
|
1709
966
|
error_description: "backchannel_logout_uri requires the jwt plugin (disableJwtPlugin must be false)"
|
|
1710
967
|
});
|
|
1711
968
|
let url;
|
|
1712
969
|
try {
|
|
1713
|
-
url = new URL(
|
|
970
|
+
url = new URL(clientWithDefaults.backchannel_logout_uri);
|
|
1714
971
|
} catch {
|
|
1715
972
|
throw new APIError("BAD_REQUEST", {
|
|
1716
973
|
error: "invalid_client_metadata",
|
|
@@ -1721,7 +978,7 @@ async function checkOAuthClient(client, opts, settings) {
|
|
|
1721
978
|
error: "invalid_client_metadata",
|
|
1722
979
|
error_description: "backchannel_logout_uri must use http or https"
|
|
1723
980
|
});
|
|
1724
|
-
if (
|
|
981
|
+
if (clientWithDefaults.backchannel_logout_uri.includes("#")) throw new APIError("BAD_REQUEST", {
|
|
1725
982
|
error: "invalid_client_metadata",
|
|
1726
983
|
error_description: "backchannel_logout_uri must not include a fragment component"
|
|
1727
984
|
});
|
|
@@ -1737,27 +994,27 @@ async function checkOAuthClient(client, opts, settings) {
|
|
|
1737
994
|
}
|
|
1738
995
|
}
|
|
1739
996
|
async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
1740
|
-
const body = ctx.body;
|
|
1741
|
-
const session = await getSessionFromCtx(ctx);
|
|
1742
|
-
if (settings.isRegister)
|
|
1743
|
-
if (session) await assertClientPrivileges(ctx, session, opts, "create");
|
|
1744
|
-
} else await assertClientPrivileges(ctx, session, opts, "create");
|
|
997
|
+
const body = applyOAuthClientRegistrationDefaults(ctx.body);
|
|
998
|
+
const session = settings.session !== void 0 ? settings.session : await getSessionFromCtx(ctx);
|
|
999
|
+
if (!settings.isRegister || session) await assertClientPrivileges(ctx, session, opts, "create");
|
|
1745
1000
|
const isPublic = body.token_endpoint_auth_method === "none";
|
|
1746
1001
|
const isPrivateKeyJwt = body.token_endpoint_auth_method === "private_key_jwt";
|
|
1747
|
-
|
|
1002
|
+
const isExtensionAuthMethod = isExtensionTokenEndpointAuthMethod(opts, body.token_endpoint_auth_method);
|
|
1003
|
+
await checkOAuthClient(body, opts, {
|
|
1748
1004
|
...settings,
|
|
1749
1005
|
ctx
|
|
1750
1006
|
});
|
|
1751
1007
|
const clientId = opts.generateClientId?.() || generateRandomString(32, "a-z", "A-Z");
|
|
1752
|
-
const clientSecret = isPublic || isPrivateKeyJwt ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
|
|
1008
|
+
const clientSecret = isPublic || isPrivateKeyJwt || isExtensionAuthMethod ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
|
|
1753
1009
|
const storedClientSecret = clientSecret ? await storeClientSecret(ctx, opts, clientSecret) : void 0;
|
|
1754
1010
|
const iat = Math.floor(Date.now() / 1e3);
|
|
1755
|
-
const referenceId = opts.clientReference ? await opts.clientReference({
|
|
1756
|
-
user: session
|
|
1757
|
-
session: session
|
|
1758
|
-
}) : void 0;
|
|
1011
|
+
const referenceId = settings.referenceId ?? (session && opts.clientReference ? await opts.clientReference({
|
|
1012
|
+
user: session.user,
|
|
1013
|
+
session: session.session
|
|
1014
|
+
}) : void 0);
|
|
1759
1015
|
const schema = oauthToSchema({
|
|
1760
|
-
...body
|
|
1016
|
+
...body,
|
|
1017
|
+
redirect_uris: body.redirect_uris ?? [],
|
|
1761
1018
|
disabled: void 0,
|
|
1762
1019
|
client_secret_expires_at: storedClientSecret ? settings.isRegister && opts?.clientRegistrationClientSecretExpiration ? toExpJWT(opts.clientRegistrationClientSecretExpiration, iat) : 0 : void 0,
|
|
1763
1020
|
client_id: clientId,
|
|
@@ -1767,24 +1024,38 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
|
1767
1024
|
user_id: referenceId ? void 0 : session?.session.userId,
|
|
1768
1025
|
reference_id: referenceId
|
|
1769
1026
|
});
|
|
1770
|
-
const
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1027
|
+
const resources = settings.resources ?? [];
|
|
1028
|
+
const responseBody = schemaToOAuth({
|
|
1029
|
+
...await runWithTransaction(ctx.context.adapter, async () => {
|
|
1030
|
+
const createdClient = await ctx.context.adapter.create({
|
|
1031
|
+
model: "oauthClient",
|
|
1032
|
+
data: {
|
|
1033
|
+
...schema,
|
|
1034
|
+
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
1035
|
+
updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
if (resources.length > 0) {
|
|
1039
|
+
const linkModel = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
|
|
1040
|
+
const now = /* @__PURE__ */ new Date();
|
|
1041
|
+
for (const resourceId of resources) await ctx.context.adapter.create({
|
|
1042
|
+
model: linkModel,
|
|
1043
|
+
forceAllowId: true,
|
|
1044
|
+
data: {
|
|
1045
|
+
id: buildClientResourceLinkId(clientId, resourceId),
|
|
1046
|
+
clientId,
|
|
1047
|
+
resourceId,
|
|
1048
|
+
createdAt: now
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
return createdClient;
|
|
1053
|
+
}),
|
|
1780
1054
|
clientSecret: clientSecret ? (opts.prefix?.clientSecret ?? "") + clientSecret : void 0
|
|
1781
|
-
}), {
|
|
1782
|
-
status: 201,
|
|
1783
|
-
headers: {
|
|
1784
|
-
"Cache-Control": "no-store",
|
|
1785
|
-
Pragma: "no-cache"
|
|
1786
|
-
}
|
|
1787
1055
|
});
|
|
1056
|
+
if (resources.length > 0) responseBody.resources = resources;
|
|
1057
|
+
ctx.setStatus(201);
|
|
1058
|
+
return ctx.json(responseBody);
|
|
1788
1059
|
}
|
|
1789
1060
|
/**
|
|
1790
1061
|
* Converts an OAuth 2.0 Dynamic Client Schema to a Database Schema
|
|
@@ -1793,7 +1064,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
|
1793
1064
|
* @returns
|
|
1794
1065
|
*/
|
|
1795
1066
|
function oauthToSchema(input) {
|
|
1796
|
-
const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: inputJwks, jwks_uri: jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, backchannel_logout_uri: backchannelLogoutUri, backchannel_logout_session_required: backchannelLogoutSessionRequired, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
|
|
1067
|
+
const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: inputJwks, jwks_uri: jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, backchannel_logout_uri: backchannelLogoutUri, backchannel_logout_session_required: backchannelLogoutSessionRequired, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, dpop_bound_access_tokens: dpopBoundAccessTokens, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
|
|
1797
1068
|
const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
|
|
1798
1069
|
const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
|
|
1799
1070
|
const scopes = _scope?.split(" ");
|
|
@@ -1833,6 +1104,7 @@ function oauthToSchema(input) {
|
|
|
1833
1104
|
skipConsent,
|
|
1834
1105
|
enableEndSession,
|
|
1835
1106
|
requirePKCE,
|
|
1107
|
+
dpopBoundAccessTokens,
|
|
1836
1108
|
subjectType,
|
|
1837
1109
|
referenceId,
|
|
1838
1110
|
metadata
|
|
@@ -1845,7 +1117,7 @@ function oauthToSchema(input) {
|
|
|
1845
1117
|
* @returns
|
|
1846
1118
|
*/
|
|
1847
1119
|
function schemaToOAuth(input) {
|
|
1848
|
-
const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, backchannelLogoutUri, backchannelLogoutSessionRequired, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, subjectType, referenceId, metadata } = input;
|
|
1120
|
+
const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, backchannelLogoutUri, backchannelLogoutSessionRequired, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, dpopBoundAccessTokens, subjectType, referenceId, metadata } = input;
|
|
1849
1121
|
const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
|
|
1850
1122
|
const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
|
|
1851
1123
|
const _scopes = scopes?.join(" ");
|
|
@@ -1881,6 +1153,7 @@ function schemaToOAuth(input) {
|
|
|
1881
1153
|
skip_consent: skipConsent ?? void 0,
|
|
1882
1154
|
enable_end_session: enableEndSession ?? void 0,
|
|
1883
1155
|
require_pkce: requirePKCE ?? void 0,
|
|
1156
|
+
dpop_bound_access_tokens: dpopBoundAccessTokens ?? void 0,
|
|
1884
1157
|
subject_type: subjectType ?? void 0,
|
|
1885
1158
|
reference_id: referenceId ?? void 0
|
|
1886
1159
|
};
|
|
@@ -2093,10 +1366,12 @@ async function rotateClientSecretEndpoint(ctx, opts) {
|
|
|
2093
1366
|
}
|
|
2094
1367
|
//#endregion
|
|
2095
1368
|
//#region src/oauthClient/index.ts
|
|
1369
|
+
const tokenEndpointAuthMethodSchema = z.string().trim().min(1);
|
|
1370
|
+
const grantTypesSchema = z.array(z.string().trim().min(1)).min(1);
|
|
2096
1371
|
const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/create-client", {
|
|
2097
1372
|
method: "POST",
|
|
2098
1373
|
body: z.object({
|
|
2099
|
-
redirect_uris: z.array(SafeUrlSchema).min(1),
|
|
1374
|
+
redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
2100
1375
|
scope: z.string().optional(),
|
|
2101
1376
|
client_name: z.string().optional(),
|
|
2102
1377
|
client_uri: z.string().optional(),
|
|
@@ -2110,20 +1385,11 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
2110
1385
|
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
2111
1386
|
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
2112
1387
|
backchannel_logout_session_required: z.boolean().optional(),
|
|
2113
|
-
token_endpoint_auth_method:
|
|
2114
|
-
"none",
|
|
2115
|
-
"client_secret_basic",
|
|
2116
|
-
"client_secret_post",
|
|
2117
|
-
"private_key_jwt"
|
|
2118
|
-
]).default("client_secret_basic").optional(),
|
|
1388
|
+
token_endpoint_auth_method: tokenEndpointAuthMethodSchema.optional(),
|
|
2119
1389
|
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(),
|
|
2120
1390
|
jwks_uri: z.string().optional(),
|
|
2121
|
-
grant_types:
|
|
2122
|
-
|
|
2123
|
-
"client_credentials",
|
|
2124
|
-
"refresh_token"
|
|
2125
|
-
])).default(["authorization_code"]).optional(),
|
|
2126
|
-
response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
|
|
1391
|
+
grant_types: grantTypesSchema.optional(),
|
|
1392
|
+
response_types: z.array(z.enum(["code"])).optional(),
|
|
2127
1393
|
type: z.enum([
|
|
2128
1394
|
"web",
|
|
2129
1395
|
"native",
|
|
@@ -2133,14 +1399,16 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
2133
1399
|
skip_consent: z.boolean().optional(),
|
|
2134
1400
|
enable_end_session: z.boolean().optional(),
|
|
2135
1401
|
require_pkce: z.boolean().optional(),
|
|
1402
|
+
dpop_bound_access_tokens: z.boolean().optional(),
|
|
2136
1403
|
subject_type: z.enum(["public", "pairwise"]).optional(),
|
|
2137
1404
|
metadata: z.record(z.string(), z.unknown()).optional()
|
|
2138
1405
|
}),
|
|
2139
1406
|
metadata: {
|
|
1407
|
+
noStore: true,
|
|
2140
1408
|
SERVER_ONLY: true,
|
|
2141
1409
|
openapi: {
|
|
2142
1410
|
description: "Register an OAuth2 application",
|
|
2143
|
-
responses: { "
|
|
1411
|
+
responses: { "201": {
|
|
2144
1412
|
description: "OAuth2 application registered successfully",
|
|
2145
1413
|
content: { "application/json": { schema: {
|
|
2146
1414
|
type: "object",
|
|
@@ -2216,24 +1484,12 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
2216
1484
|
},
|
|
2217
1485
|
token_endpoint_auth_method: {
|
|
2218
1486
|
type: "string",
|
|
2219
|
-
description: "Requested authentication method for the token endpoint"
|
|
2220
|
-
enum: [
|
|
2221
|
-
"none",
|
|
2222
|
-
"client_secret_basic",
|
|
2223
|
-
"client_secret_post"
|
|
2224
|
-
]
|
|
1487
|
+
description: "Requested authentication method for the token endpoint"
|
|
2225
1488
|
},
|
|
2226
1489
|
grant_types: {
|
|
2227
1490
|
type: "array",
|
|
2228
|
-
items: {
|
|
2229
|
-
|
|
2230
|
-
enum: [
|
|
2231
|
-
"authorization_code",
|
|
2232
|
-
"client_credentials",
|
|
2233
|
-
"refresh_token"
|
|
2234
|
-
]
|
|
2235
|
-
},
|
|
2236
|
-
description: "Requested authentication method for the token endpoint"
|
|
1491
|
+
items: { type: "string" },
|
|
1492
|
+
description: "Grant types the client may use at the token endpoint"
|
|
2237
1493
|
},
|
|
2238
1494
|
response_types: {
|
|
2239
1495
|
type: "array",
|
|
@@ -2241,7 +1497,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
2241
1497
|
type: "string",
|
|
2242
1498
|
enum: ["code"]
|
|
2243
1499
|
},
|
|
2244
|
-
description: "
|
|
1500
|
+
description: "Response types the client may use at the authorization endpoint"
|
|
2245
1501
|
},
|
|
2246
1502
|
public: {
|
|
2247
1503
|
type: "boolean",
|
|
@@ -2284,7 +1540,7 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
|
|
|
2284
1540
|
method: "POST",
|
|
2285
1541
|
use: [sessionMiddleware],
|
|
2286
1542
|
body: z.object({
|
|
2287
|
-
redirect_uris: z.array(SafeUrlSchema).min(1),
|
|
1543
|
+
redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
2288
1544
|
scope: z.string().optional(),
|
|
2289
1545
|
client_name: z.string().optional(),
|
|
2290
1546
|
client_uri: z.string().optional(),
|
|
@@ -2298,159 +1554,142 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
|
|
|
2298
1554
|
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
2299
1555
|
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
2300
1556
|
backchannel_logout_session_required: z.boolean().optional(),
|
|
2301
|
-
token_endpoint_auth_method:
|
|
2302
|
-
"none",
|
|
2303
|
-
"client_secret_basic",
|
|
2304
|
-
"client_secret_post",
|
|
2305
|
-
"private_key_jwt"
|
|
2306
|
-
]).default("client_secret_basic").optional(),
|
|
1557
|
+
token_endpoint_auth_method: tokenEndpointAuthMethodSchema.optional(),
|
|
2307
1558
|
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(),
|
|
2308
1559
|
jwks_uri: z.string().optional(),
|
|
2309
|
-
grant_types:
|
|
2310
|
-
|
|
2311
|
-
"client_credentials",
|
|
2312
|
-
"refresh_token"
|
|
2313
|
-
])).default(["authorization_code"]).optional(),
|
|
2314
|
-
response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
|
|
1560
|
+
grant_types: grantTypesSchema.optional(),
|
|
1561
|
+
response_types: z.array(z.enum(["code"])).optional(),
|
|
2315
1562
|
type: z.enum([
|
|
2316
1563
|
"web",
|
|
2317
1564
|
"native",
|
|
2318
1565
|
"user-agent-based"
|
|
2319
|
-
]).optional()
|
|
1566
|
+
]).optional(),
|
|
1567
|
+
dpop_bound_access_tokens: z.boolean().optional()
|
|
2320
1568
|
}),
|
|
2321
|
-
metadata: {
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
description: "OAuth2 application
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
},
|
|
2332
|
-
client_secret: {
|
|
2333
|
-
type: "string",
|
|
2334
|
-
description: "Secret key for the client"
|
|
2335
|
-
},
|
|
2336
|
-
client_secret_expires_at: {
|
|
2337
|
-
type: "number",
|
|
2338
|
-
description: "Time the client secret will expire. If 0, the client secret will never expire."
|
|
2339
|
-
},
|
|
2340
|
-
scope: {
|
|
2341
|
-
type: "string",
|
|
2342
|
-
description: "Space-separated scopes allowed by the client"
|
|
2343
|
-
},
|
|
2344
|
-
user_id: {
|
|
2345
|
-
type: "string",
|
|
2346
|
-
description: "ID of the user who registered the client, null if registered anonymously"
|
|
2347
|
-
},
|
|
2348
|
-
client_id_issued_at: {
|
|
2349
|
-
type: "number",
|
|
2350
|
-
description: "Creation timestamp of this client"
|
|
2351
|
-
},
|
|
2352
|
-
client_name: {
|
|
2353
|
-
type: "string",
|
|
2354
|
-
description: "Name of the OAuth2 application"
|
|
2355
|
-
},
|
|
2356
|
-
client_uri: {
|
|
2357
|
-
type: "string",
|
|
2358
|
-
description: "URI of the OAuth2 application"
|
|
2359
|
-
},
|
|
2360
|
-
logo_uri: {
|
|
2361
|
-
type: "string",
|
|
2362
|
-
description: "Icon URI for the application"
|
|
2363
|
-
},
|
|
2364
|
-
contacts: {
|
|
2365
|
-
type: "array",
|
|
2366
|
-
items: { type: "string" },
|
|
2367
|
-
description: "List representing ways to contact people responsible for this client, typically email addresses"
|
|
2368
|
-
},
|
|
2369
|
-
tos_uri: {
|
|
2370
|
-
type: "string",
|
|
2371
|
-
description: "Client's terms of service uri"
|
|
2372
|
-
},
|
|
2373
|
-
policy_uri: {
|
|
2374
|
-
type: "string",
|
|
2375
|
-
description: "Client's policy uri"
|
|
2376
|
-
},
|
|
2377
|
-
software_id: {
|
|
2378
|
-
type: "string",
|
|
2379
|
-
description: "Unique identifier assigned by the developer to help in the dynamic registration process"
|
|
2380
|
-
},
|
|
2381
|
-
software_version: {
|
|
2382
|
-
type: "string",
|
|
2383
|
-
description: "Version identifier for the software_id"
|
|
2384
|
-
},
|
|
2385
|
-
software_statement: {
|
|
2386
|
-
type: "string",
|
|
2387
|
-
description: "JWT containing metadata values about the client software as claims"
|
|
2388
|
-
},
|
|
2389
|
-
redirect_uris: {
|
|
2390
|
-
type: "array",
|
|
2391
|
-
items: {
|
|
1569
|
+
metadata: {
|
|
1570
|
+
noStore: true,
|
|
1571
|
+
openapi: {
|
|
1572
|
+
description: "Register an OAuth2 application",
|
|
1573
|
+
responses: { "201": {
|
|
1574
|
+
description: "OAuth2 application registered successfully",
|
|
1575
|
+
content: { "application/json": { schema: {
|
|
1576
|
+
type: "object",
|
|
1577
|
+
properties: {
|
|
1578
|
+
client_id: {
|
|
2392
1579
|
type: "string",
|
|
2393
|
-
|
|
1580
|
+
description: "Unique identifier for the client"
|
|
2394
1581
|
},
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
"
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
1582
|
+
client_secret: {
|
|
1583
|
+
type: "string",
|
|
1584
|
+
description: "Secret key for the client"
|
|
1585
|
+
},
|
|
1586
|
+
client_secret_expires_at: {
|
|
1587
|
+
type: "number",
|
|
1588
|
+
description: "Time the client secret will expire. If 0, the client secret will never expire."
|
|
1589
|
+
},
|
|
1590
|
+
scope: {
|
|
1591
|
+
type: "string",
|
|
1592
|
+
description: "Space-separated scopes allowed by the client"
|
|
1593
|
+
},
|
|
1594
|
+
user_id: {
|
|
1595
|
+
type: "string",
|
|
1596
|
+
description: "ID of the user who registered the client, null if registered anonymously"
|
|
1597
|
+
},
|
|
1598
|
+
client_id_issued_at: {
|
|
1599
|
+
type: "number",
|
|
1600
|
+
description: "Creation timestamp of this client"
|
|
1601
|
+
},
|
|
1602
|
+
client_name: {
|
|
1603
|
+
type: "string",
|
|
1604
|
+
description: "Name of the OAuth2 application"
|
|
1605
|
+
},
|
|
1606
|
+
client_uri: {
|
|
1607
|
+
type: "string",
|
|
1608
|
+
description: "URI of the OAuth2 application"
|
|
1609
|
+
},
|
|
1610
|
+
logo_uri: {
|
|
1611
|
+
type: "string",
|
|
1612
|
+
description: "Icon URI for the application"
|
|
1613
|
+
},
|
|
1614
|
+
contacts: {
|
|
1615
|
+
type: "array",
|
|
1616
|
+
items: { type: "string" },
|
|
1617
|
+
description: "List representing ways to contact people responsible for this client, typically email addresses"
|
|
1618
|
+
},
|
|
1619
|
+
tos_uri: {
|
|
1620
|
+
type: "string",
|
|
1621
|
+
description: "Client's terms of service uri"
|
|
1622
|
+
},
|
|
1623
|
+
policy_uri: {
|
|
1624
|
+
type: "string",
|
|
1625
|
+
description: "Client's policy uri"
|
|
1626
|
+
},
|
|
1627
|
+
software_id: {
|
|
1628
|
+
type: "string",
|
|
1629
|
+
description: "Unique identifier assigned by the developer to help in the dynamic registration process"
|
|
1630
|
+
},
|
|
1631
|
+
software_version: {
|
|
1632
|
+
type: "string",
|
|
1633
|
+
description: "Version identifier for the software_id"
|
|
1634
|
+
},
|
|
1635
|
+
software_statement: {
|
|
1636
|
+
type: "string",
|
|
1637
|
+
description: "JWT containing metadata values about the client software as claims"
|
|
1638
|
+
},
|
|
1639
|
+
redirect_uris: {
|
|
1640
|
+
type: "array",
|
|
1641
|
+
items: {
|
|
1642
|
+
type: "string",
|
|
1643
|
+
format: "uri"
|
|
1644
|
+
},
|
|
1645
|
+
description: "List of allowed redirect uris"
|
|
1646
|
+
},
|
|
1647
|
+
token_endpoint_auth_method: {
|
|
1648
|
+
type: "string",
|
|
1649
|
+
description: "Requested authentication method for the token endpoint"
|
|
1650
|
+
},
|
|
1651
|
+
grant_types: {
|
|
1652
|
+
type: "array",
|
|
1653
|
+
items: { type: "string" },
|
|
1654
|
+
description: "Grant types the client may use at the token endpoint"
|
|
1655
|
+
},
|
|
1656
|
+
response_types: {
|
|
1657
|
+
type: "array",
|
|
1658
|
+
items: {
|
|
1659
|
+
type: "string",
|
|
1660
|
+
enum: ["code"]
|
|
1661
|
+
},
|
|
1662
|
+
description: "Response types the client may use at the authorization endpoint"
|
|
1663
|
+
},
|
|
1664
|
+
public: {
|
|
1665
|
+
type: "boolean",
|
|
1666
|
+
description: "Whether the client is public as determined by the type"
|
|
1667
|
+
},
|
|
1668
|
+
type: {
|
|
2409
1669
|
type: "string",
|
|
1670
|
+
description: "Type of the client",
|
|
2410
1671
|
enum: [
|
|
2411
|
-
"
|
|
2412
|
-
"
|
|
2413
|
-
"
|
|
1672
|
+
"web",
|
|
1673
|
+
"native",
|
|
1674
|
+
"user-agent-based"
|
|
2414
1675
|
]
|
|
2415
1676
|
},
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
type: "array",
|
|
2420
|
-
items: {
|
|
2421
|
-
type: "string",
|
|
2422
|
-
enum: ["code"]
|
|
1677
|
+
disabled: {
|
|
1678
|
+
type: "boolean",
|
|
1679
|
+
description: "Whether the client is disabled"
|
|
2423
1680
|
},
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
type: {
|
|
2431
|
-
type: "string",
|
|
2432
|
-
description: "Type of the client",
|
|
2433
|
-
enum: [
|
|
2434
|
-
"web",
|
|
2435
|
-
"native",
|
|
2436
|
-
"user-agent-based"
|
|
2437
|
-
]
|
|
2438
|
-
},
|
|
2439
|
-
disabled: {
|
|
2440
|
-
type: "boolean",
|
|
2441
|
-
description: "Whether the client is disabled"
|
|
1681
|
+
metadata: {
|
|
1682
|
+
type: "object",
|
|
1683
|
+
additionalProperties: true,
|
|
1684
|
+
nullable: true,
|
|
1685
|
+
description: "Additional metadata for the application"
|
|
1686
|
+
}
|
|
2442
1687
|
},
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
}
|
|
2449
|
-
},
|
|
2450
|
-
required: ["client_id"]
|
|
2451
|
-
} } }
|
|
2452
|
-
} }
|
|
2453
|
-
} }
|
|
1688
|
+
required: ["client_id"]
|
|
1689
|
+
} } }
|
|
1690
|
+
} }
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
2454
1693
|
}, async (ctx) => {
|
|
2455
1694
|
return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
|
|
2456
1695
|
});
|
|
@@ -2509,11 +1748,7 @@ const adminUpdateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/updat
|
|
|
2509
1748
|
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
2510
1749
|
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
2511
1750
|
backchannel_logout_session_required: z.boolean().optional(),
|
|
2512
|
-
grant_types:
|
|
2513
|
-
"authorization_code",
|
|
2514
|
-
"client_credentials",
|
|
2515
|
-
"refresh_token"
|
|
2516
|
-
])).optional(),
|
|
1751
|
+
grant_types: grantTypesSchema.optional(),
|
|
2517
1752
|
response_types: z.array(z.enum(["code"])).optional(),
|
|
2518
1753
|
type: z.enum([
|
|
2519
1754
|
"web",
|
|
@@ -2523,6 +1758,7 @@ const adminUpdateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/updat
|
|
|
2523
1758
|
client_secret_expires_at: z.union([z.string(), z.number()]).optional(),
|
|
2524
1759
|
skip_consent: z.boolean().optional(),
|
|
2525
1760
|
enable_end_session: z.boolean().optional(),
|
|
1761
|
+
dpop_bound_access_tokens: z.boolean().optional(),
|
|
2526
1762
|
metadata: z.record(z.string(), z.unknown()).optional()
|
|
2527
1763
|
})
|
|
2528
1764
|
}),
|
|
@@ -2553,11 +1789,7 @@ const updateOAuthClient = (opts) => createAuthEndpoint("/oauth2/update-client",
|
|
|
2553
1789
|
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
2554
1790
|
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
2555
1791
|
backchannel_logout_session_required: z.boolean().optional(),
|
|
2556
|
-
grant_types:
|
|
2557
|
-
"authorization_code",
|
|
2558
|
-
"client_credentials",
|
|
2559
|
-
"refresh_token"
|
|
2560
|
-
])).optional(),
|
|
1792
|
+
grant_types: grantTypesSchema.optional(),
|
|
2561
1793
|
response_types: z.array(z.enum(["code"])).optional(),
|
|
2562
1794
|
type: z.enum([
|
|
2563
1795
|
"web",
|
|
@@ -2574,7 +1806,10 @@ const rotateClientSecret = (opts) => createAuthEndpoint("/oauth2/client/rotate-s
|
|
|
2574
1806
|
method: "POST",
|
|
2575
1807
|
use: [sessionMiddleware],
|
|
2576
1808
|
body: z.object({ client_id: z.string() }),
|
|
2577
|
-
metadata: {
|
|
1809
|
+
metadata: {
|
|
1810
|
+
noStore: true,
|
|
1811
|
+
openapi: { description: "Rotates a confidential client's secret" }
|
|
1812
|
+
}
|
|
2578
1813
|
}, async (ctx) => {
|
|
2579
1814
|
return rotateClientSecretEndpoint(ctx, opts);
|
|
2580
1815
|
});
|
|
@@ -2722,6 +1957,333 @@ const deleteOAuthConsent = (opts) => createAuthEndpoint("/oauth2/delete-consent"
|
|
|
2722
1957
|
return deleteConsentEndpoint(ctx, opts);
|
|
2723
1958
|
});
|
|
2724
1959
|
//#endregion
|
|
1960
|
+
//#region src/oauthResource/endpoints.ts
|
|
1961
|
+
/**
|
|
1962
|
+
* Gate every admin resource endpoint. Mirrors `assertClientPrivileges`:
|
|
1963
|
+
* a missing session → 401; a defined `resourcePrivileges` callback that
|
|
1964
|
+
* returns falsy → 401 with the original action context preserved.
|
|
1965
|
+
*
|
|
1966
|
+
* When `resourcePrivileges` is undefined, the gate degrades to "any
|
|
1967
|
+
* authenticated session can manage resources" — same forgiving default
|
|
1968
|
+
* as `clientPrivileges`. Operators who care about RBAC must define the
|
|
1969
|
+
* callback.
|
|
1970
|
+
*
|
|
1971
|
+
* @internal
|
|
1972
|
+
*/
|
|
1973
|
+
async function assertResourcePrivileges(ctx, session, opts, action, resourceId) {
|
|
1974
|
+
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1975
|
+
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
1976
|
+
if (!opts.resourcePrivileges) return;
|
|
1977
|
+
if (!await opts.resourcePrivileges({
|
|
1978
|
+
headers: ctx.headers,
|
|
1979
|
+
action,
|
|
1980
|
+
session: session.session,
|
|
1981
|
+
user: session.user,
|
|
1982
|
+
resourceId
|
|
1983
|
+
})) throw new APIError("UNAUTHORIZED");
|
|
1984
|
+
}
|
|
1985
|
+
const resourceModel = (opts) => opts.schema?.oauthResource?.modelName ?? "oauthResource";
|
|
1986
|
+
const linkModel = (opts) => opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
|
|
1987
|
+
const clientModel = (opts) => opts.schema?.oauthClient?.modelName ?? "oauthClient";
|
|
1988
|
+
/**
|
|
1989
|
+
* Decode a URL path-segment parameter.
|
|
1990
|
+
*
|
|
1991
|
+
* better-call's router (better-call@1.3.5) does NOT decode path params —
|
|
1992
|
+
* `tryDecode` is wired into cookie parsing only. So a raw HTTP caller hitting
|
|
1993
|
+
* `/admin/oauth2/resources/https%3A%2F%2Fapi.example.com` lands here with
|
|
1994
|
+
* `ctx.params.identifier === "https%3A%2F%2Fapi.example.com"`, which never
|
|
1995
|
+
* matches the stored `https://api.example.com` row. Decode every path
|
|
1996
|
+
* segment that holds a URI-valued identifier so the admin handlers behave
|
|
1997
|
+
* identically to in-process `auth.api.*` calls (which pass already-decoded
|
|
1998
|
+
* JS strings).
|
|
1999
|
+
*
|
|
2000
|
+
* Falls back to the raw string when decode fails so a malformed identifier
|
|
2001
|
+
* surfaces as a clean NOT_FOUND from the row lookup rather than a 500.
|
|
2002
|
+
*
|
|
2003
|
+
* @internal
|
|
2004
|
+
*/
|
|
2005
|
+
function decodePathParam(value) {
|
|
2006
|
+
try {
|
|
2007
|
+
return decodeURIComponent(value);
|
|
2008
|
+
} catch {
|
|
2009
|
+
return value;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Builds the create-payload from a normalized input. Fills in defaults
|
|
2014
|
+
* for the fields seedResources uses so the admin-CRUD and seed paths
|
|
2015
|
+
* write identical rows.
|
|
2016
|
+
*
|
|
2017
|
+
* @internal
|
|
2018
|
+
*/
|
|
2019
|
+
function buildResourceRow(input, now) {
|
|
2020
|
+
return {
|
|
2021
|
+
identifier: input.identifier,
|
|
2022
|
+
name: input.name ?? input.identifier,
|
|
2023
|
+
accessTokenTtl: input.accessTokenTtl ?? null,
|
|
2024
|
+
refreshTokenTtl: input.refreshTokenTtl ?? null,
|
|
2025
|
+
signingAlgorithm: input.signingAlgorithm ?? null,
|
|
2026
|
+
signingKeyId: input.signingKeyId ?? null,
|
|
2027
|
+
allowedScopes: input.allowedScopes ?? null,
|
|
2028
|
+
customClaims: input.customClaims ?? null,
|
|
2029
|
+
dpopBoundAccessTokensRequired: input.dpopBoundAccessTokensRequired ?? false,
|
|
2030
|
+
disabled: input.disabled ?? false,
|
|
2031
|
+
policyVersion: 1,
|
|
2032
|
+
metadata: input.metadata ?? null,
|
|
2033
|
+
createdAt: now,
|
|
2034
|
+
updatedAt: now
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
async function createResourceEndpoint(ctx, opts) {
|
|
2038
|
+
await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
|
|
2039
|
+
const input = ctx.body;
|
|
2040
|
+
if (!input?.identifier) throw new APIError("BAD_REQUEST", {
|
|
2041
|
+
error: "invalid_request",
|
|
2042
|
+
error_description: "identifier is required"
|
|
2043
|
+
});
|
|
2044
|
+
await assertIdentifierValid(opts, input.identifier);
|
|
2045
|
+
const now = /* @__PURE__ */ new Date();
|
|
2046
|
+
let created;
|
|
2047
|
+
try {
|
|
2048
|
+
created = await ctx.context.adapter.create({
|
|
2049
|
+
model: resourceModel(opts),
|
|
2050
|
+
data: buildResourceRow(input, now)
|
|
2051
|
+
});
|
|
2052
|
+
} catch (err) {
|
|
2053
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2054
|
+
if (/unique|duplicate|UNIQUE/i.test(message)) throw new APIError("BAD_REQUEST", {
|
|
2055
|
+
error: "invalid_request",
|
|
2056
|
+
error_description: `resource ${input.identifier} already exists`
|
|
2057
|
+
});
|
|
2058
|
+
throw err;
|
|
2059
|
+
}
|
|
2060
|
+
if (!created) throw new APIError("BAD_REQUEST", {
|
|
2061
|
+
error: "invalid_request",
|
|
2062
|
+
error_description: `resource ${input.identifier} could not be created`
|
|
2063
|
+
});
|
|
2064
|
+
invalidateResourceCache(created.identifier);
|
|
2065
|
+
return ctx.json(created, { status: 201 });
|
|
2066
|
+
}
|
|
2067
|
+
async function listResourcesEndpoint(ctx, opts) {
|
|
2068
|
+
await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "list");
|
|
2069
|
+
const rows = await ctx.context.adapter.findMany({ model: resourceModel(opts) });
|
|
2070
|
+
return ctx.json(rows ?? []);
|
|
2071
|
+
}
|
|
2072
|
+
async function getResourceByIdentifierEndpoint(ctx, opts) {
|
|
2073
|
+
const identifier = decodePathParam(ctx.params.identifier);
|
|
2074
|
+
await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "read", identifier);
|
|
2075
|
+
const row = await ctx.context.adapter.findOne({
|
|
2076
|
+
model: resourceModel(opts),
|
|
2077
|
+
where: [{
|
|
2078
|
+
field: "identifier",
|
|
2079
|
+
value: identifier
|
|
2080
|
+
}]
|
|
2081
|
+
});
|
|
2082
|
+
if (!row) throw new APIError("NOT_FOUND", {
|
|
2083
|
+
error: "not_found",
|
|
2084
|
+
error_description: `resource ${identifier} not found`
|
|
2085
|
+
});
|
|
2086
|
+
return ctx.json(row);
|
|
2087
|
+
}
|
|
2088
|
+
async function updateResourceEndpoint(ctx, opts) {
|
|
2089
|
+
const identifier = decodePathParam(ctx.params.identifier);
|
|
2090
|
+
await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "update", identifier);
|
|
2091
|
+
if (!await ctx.context.adapter.findOne({
|
|
2092
|
+
model: resourceModel(opts),
|
|
2093
|
+
where: [{
|
|
2094
|
+
field: "identifier",
|
|
2095
|
+
value: identifier
|
|
2096
|
+
}]
|
|
2097
|
+
})) throw new APIError("NOT_FOUND", {
|
|
2098
|
+
error: "not_found",
|
|
2099
|
+
error_description: `resource ${identifier} not found`
|
|
2100
|
+
});
|
|
2101
|
+
const update = { updatedAt: /* @__PURE__ */ new Date() };
|
|
2102
|
+
for (const key of [
|
|
2103
|
+
"name",
|
|
2104
|
+
"accessTokenTtl",
|
|
2105
|
+
"refreshTokenTtl",
|
|
2106
|
+
"signingAlgorithm",
|
|
2107
|
+
"signingKeyId",
|
|
2108
|
+
"allowedScopes",
|
|
2109
|
+
"customClaims",
|
|
2110
|
+
"dpopBoundAccessTokensRequired",
|
|
2111
|
+
"disabled",
|
|
2112
|
+
"metadata"
|
|
2113
|
+
]) if (Object.prototype.hasOwnProperty.call(ctx.body, key)) update[key] = ctx.body[key];
|
|
2114
|
+
await ctx.context.adapter.update({
|
|
2115
|
+
model: resourceModel(opts),
|
|
2116
|
+
where: [{
|
|
2117
|
+
field: "identifier",
|
|
2118
|
+
value: identifier
|
|
2119
|
+
}],
|
|
2120
|
+
update
|
|
2121
|
+
});
|
|
2122
|
+
invalidateResourceCache(identifier);
|
|
2123
|
+
const refreshed = await ctx.context.adapter.findOne({
|
|
2124
|
+
model: resourceModel(opts),
|
|
2125
|
+
where: [{
|
|
2126
|
+
field: "identifier",
|
|
2127
|
+
value: identifier
|
|
2128
|
+
}]
|
|
2129
|
+
});
|
|
2130
|
+
if (!refreshed) throw new APIError("NOT_FOUND", {
|
|
2131
|
+
error: "not_found",
|
|
2132
|
+
error_description: `resource ${identifier} not found`
|
|
2133
|
+
});
|
|
2134
|
+
return ctx.json(refreshed);
|
|
2135
|
+
}
|
|
2136
|
+
async function deleteResourceEndpoint(ctx, opts) {
|
|
2137
|
+
const identifier = decodePathParam(ctx.params.identifier);
|
|
2138
|
+
await assertResourcePrivileges(ctx, await getSessionFromCtx(ctx), opts, "delete", identifier);
|
|
2139
|
+
if (!await ctx.context.adapter.findOne({
|
|
2140
|
+
model: resourceModel(opts),
|
|
2141
|
+
where: [{
|
|
2142
|
+
field: "identifier",
|
|
2143
|
+
value: identifier
|
|
2144
|
+
}]
|
|
2145
|
+
})) throw new APIError("NOT_FOUND", {
|
|
2146
|
+
error: "not_found",
|
|
2147
|
+
error_description: `resource ${identifier} not found`
|
|
2148
|
+
});
|
|
2149
|
+
await ctx.context.adapter.delete({
|
|
2150
|
+
model: resourceModel(opts),
|
|
2151
|
+
where: [{
|
|
2152
|
+
field: "identifier",
|
|
2153
|
+
value: identifier
|
|
2154
|
+
}]
|
|
2155
|
+
});
|
|
2156
|
+
invalidateResourceCache(identifier);
|
|
2157
|
+
return ctx.json({ deleted: true });
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Link a client to a resource.
|
|
2161
|
+
*
|
|
2162
|
+
* Route: `POST /admin/oauth2/resources/:identifier/clients/:client_id`.
|
|
2163
|
+
* Path params carry the identifiers (RESTful linkage) — no body required.
|
|
2164
|
+
* Used by admins when {@link OAuthOptions.enforcePerClientResources} is on.
|
|
2165
|
+
*/
|
|
2166
|
+
async function linkClientResourceEndpoint(ctx, opts) {
|
|
2167
|
+
const session = await getSessionFromCtx(ctx);
|
|
2168
|
+
const resourceId = decodePathParam(ctx.params.identifier);
|
|
2169
|
+
const clientId = decodePathParam(ctx.params.client_id);
|
|
2170
|
+
await assertResourcePrivileges(ctx, session, opts, "link", resourceId);
|
|
2171
|
+
if (!await ctx.context.adapter.findOne({
|
|
2172
|
+
model: resourceModel(opts),
|
|
2173
|
+
where: [{
|
|
2174
|
+
field: "identifier",
|
|
2175
|
+
value: resourceId
|
|
2176
|
+
}]
|
|
2177
|
+
})) throw new APIError("NOT_FOUND", {
|
|
2178
|
+
error: "not_found",
|
|
2179
|
+
error_description: `resource ${resourceId} not found`
|
|
2180
|
+
});
|
|
2181
|
+
if (!await ctx.context.adapter.findOne({
|
|
2182
|
+
model: clientModel(opts),
|
|
2183
|
+
where: [{
|
|
2184
|
+
field: "clientId",
|
|
2185
|
+
value: clientId
|
|
2186
|
+
}]
|
|
2187
|
+
})) throw new APIError("NOT_FOUND", {
|
|
2188
|
+
error: "not_found",
|
|
2189
|
+
error_description: `client ${clientId} not found`
|
|
2190
|
+
});
|
|
2191
|
+
const id = buildClientResourceLinkId(clientId, resourceId);
|
|
2192
|
+
try {
|
|
2193
|
+
await ctx.context.adapter.create({
|
|
2194
|
+
model: linkModel(opts),
|
|
2195
|
+
forceAllowId: true,
|
|
2196
|
+
data: {
|
|
2197
|
+
id,
|
|
2198
|
+
clientId,
|
|
2199
|
+
resourceId,
|
|
2200
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
} catch (err) {
|
|
2204
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2205
|
+
if (/unique|duplicate|UNIQUE/i.test(message)) return ctx.json({
|
|
2206
|
+
linked: true,
|
|
2207
|
+
alreadyLinked: true
|
|
2208
|
+
});
|
|
2209
|
+
throw err;
|
|
2210
|
+
}
|
|
2211
|
+
return ctx.json({ linked: true });
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Unlink a client from a resource.
|
|
2215
|
+
*
|
|
2216
|
+
* Route: `DELETE /admin/oauth2/resources/:identifier/clients/:client_id`.
|
|
2217
|
+
* Path params carry the identifiers (RESTful linkage) — no body required.
|
|
2218
|
+
*/
|
|
2219
|
+
async function unlinkClientResourceEndpoint(ctx, opts) {
|
|
2220
|
+
const session = await getSessionFromCtx(ctx);
|
|
2221
|
+
const resourceId = decodePathParam(ctx.params.identifier);
|
|
2222
|
+
const clientId = decodePathParam(ctx.params.client_id);
|
|
2223
|
+
await assertResourcePrivileges(ctx, session, opts, "unlink", resourceId);
|
|
2224
|
+
await ctx.context.adapter.deleteMany({
|
|
2225
|
+
model: linkModel(opts),
|
|
2226
|
+
where: [{
|
|
2227
|
+
field: "clientId",
|
|
2228
|
+
value: clientId
|
|
2229
|
+
}, {
|
|
2230
|
+
field: "resourceId",
|
|
2231
|
+
value: resourceId
|
|
2232
|
+
}]
|
|
2233
|
+
});
|
|
2234
|
+
return ctx.json({ unlinked: true });
|
|
2235
|
+
}
|
|
2236
|
+
//#endregion
|
|
2237
|
+
//#region src/oauthResource/index.ts
|
|
2238
|
+
/**
|
|
2239
|
+
* Shared body schema for create/update — every field is optional except
|
|
2240
|
+
* `identifier` on create (validated in the handler). Update accepts a
|
|
2241
|
+
* subset; the handler only writes fields that are explicitly present.
|
|
2242
|
+
*/
|
|
2243
|
+
const resourceBodySchema = z.object({
|
|
2244
|
+
identifier: z.string().min(1).optional(),
|
|
2245
|
+
name: z.string().optional(),
|
|
2246
|
+
accessTokenTtl: z.number().int().positive().nullable().optional(),
|
|
2247
|
+
refreshTokenTtl: z.number().int().positive().nullable().optional(),
|
|
2248
|
+
signingAlgorithm: z.enum(JWS_ALGORITHMS).nullable().optional(),
|
|
2249
|
+
signingKeyId: z.string().nullable().optional(),
|
|
2250
|
+
allowedScopes: z.array(z.string()).nullable().optional(),
|
|
2251
|
+
customClaims: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
2252
|
+
dpopBoundAccessTokensRequired: z.boolean().optional(),
|
|
2253
|
+
disabled: z.boolean().optional(),
|
|
2254
|
+
metadata: z.record(z.string(), z.unknown()).nullable().optional()
|
|
2255
|
+
});
|
|
2256
|
+
const adminCreateOAuthResource = (opts) => createAuthEndpoint("/admin/oauth2/resources", {
|
|
2257
|
+
method: "POST",
|
|
2258
|
+
body: resourceBodySchema.required({ identifier: true }),
|
|
2259
|
+
metadata: { SERVER_ONLY: true }
|
|
2260
|
+
}, async (ctx) => createResourceEndpoint(ctx, opts));
|
|
2261
|
+
const adminListOAuthResources = (opts) => createAuthEndpoint("/admin/oauth2/resources", {
|
|
2262
|
+
method: "GET",
|
|
2263
|
+
metadata: { SERVER_ONLY: true }
|
|
2264
|
+
}, async (ctx) => listResourcesEndpoint(ctx, opts));
|
|
2265
|
+
const adminGetOAuthResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier", {
|
|
2266
|
+
method: "GET",
|
|
2267
|
+
metadata: { SERVER_ONLY: true }
|
|
2268
|
+
}, async (ctx) => getResourceByIdentifierEndpoint(ctx, opts));
|
|
2269
|
+
const adminUpdateOAuthResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier", {
|
|
2270
|
+
method: "PATCH",
|
|
2271
|
+
body: resourceBodySchema,
|
|
2272
|
+
metadata: { SERVER_ONLY: true }
|
|
2273
|
+
}, async (ctx) => updateResourceEndpoint(ctx, opts));
|
|
2274
|
+
const adminDeleteOAuthResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier", {
|
|
2275
|
+
method: "DELETE",
|
|
2276
|
+
metadata: { SERVER_ONLY: true }
|
|
2277
|
+
}, async (ctx) => deleteResourceEndpoint(ctx, opts));
|
|
2278
|
+
const adminLinkClientResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier/clients/:client_id", {
|
|
2279
|
+
method: "POST",
|
|
2280
|
+
metadata: { SERVER_ONLY: true }
|
|
2281
|
+
}, async (ctx) => linkClientResourceEndpoint(ctx, opts));
|
|
2282
|
+
const adminUnlinkClientResource = (opts) => createAuthEndpoint("/admin/oauth2/resources/:identifier/clients/:client_id", {
|
|
2283
|
+
method: "DELETE",
|
|
2284
|
+
metadata: { SERVER_ONLY: true }
|
|
2285
|
+
}, async (ctx) => unlinkClientResourceEndpoint(ctx, opts));
|
|
2286
|
+
//#endregion
|
|
2725
2287
|
//#region src/revoke.ts
|
|
2726
2288
|
/**
|
|
2727
2289
|
* IMPORTANT NOTES:
|
|
@@ -2737,23 +2299,23 @@ const deleteOAuthConsent = (opts) => createAuthEndpoint("/oauth2/delete-consent"
|
|
|
2737
2299
|
* delete. Once the token is confirmed to be a valid JWT for this server, the
|
|
2738
2300
|
* endpoint reports `unsupported_token_type` (RFC 7009 §2.2.1) instead of a
|
|
2739
2301
|
* silent success, so callers can tell that no server-side revocation happened.
|
|
2740
|
-
* An expired or
|
|
2741
|
-
* successful no-op. Session-bound
|
|
2742
|
-
* the session-liveness check in
|
|
2302
|
+
* An expired JWT or a JWT with an audience rejected by the OAuth resource model
|
|
2303
|
+
* is already inactive and still resolves as a successful no-op. Session-bound
|
|
2304
|
+
* tokens (carrying `sid`) are cut off early by the session-liveness check in
|
|
2305
|
+
* introspection and userinfo.
|
|
2743
2306
|
*/
|
|
2744
2307
|
async function revokeJwtAccessToken(ctx, opts, token) {
|
|
2745
2308
|
const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
|
|
2746
2309
|
const jwtPluginOptions = jwtPlugin?.options;
|
|
2747
2310
|
try {
|
|
2748
|
-
await
|
|
2311
|
+
const verified = await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
|
|
2749
2312
|
jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
|
|
2750
2313
|
return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
|
|
2751
2314
|
},
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
});
|
|
2315
|
+
jwksCacheKey: jwtPlugin
|
|
2316
|
+
})), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL });
|
|
2317
|
+
const userInfoAudience = `${ctx.context.baseURL}/oauth2/userinfo`;
|
|
2318
|
+
if (!verified.payload.azp || !await isAudienceClaimAllowed(ctx, opts, verified.payload.aud, [userInfoAudience])) return null;
|
|
2757
2319
|
} catch (error) {
|
|
2758
2320
|
if (error instanceof Error) {
|
|
2759
2321
|
if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
|
|
@@ -2831,7 +2393,7 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
|
|
|
2831
2393
|
}
|
|
2832
2394
|
if (!refreshToken.clientId || refreshToken.clientId !== clientId) return null;
|
|
2833
2395
|
const iat = Math.floor(Date.now() / 1e3);
|
|
2834
|
-
if (!await ctx.context.adapter.
|
|
2396
|
+
if (!await ctx.context.adapter.incrementOne({
|
|
2835
2397
|
model: "oauthRefreshToken",
|
|
2836
2398
|
where: [{
|
|
2837
2399
|
field: "id",
|
|
@@ -2841,7 +2403,8 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
|
|
|
2841
2403
|
operator: "eq",
|
|
2842
2404
|
value: null
|
|
2843
2405
|
}],
|
|
2844
|
-
|
|
2406
|
+
increment: {},
|
|
2407
|
+
set: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
|
|
2845
2408
|
})) {
|
|
2846
2409
|
await invalidateRefreshFamily(ctx, clientId, refreshToken.userId);
|
|
2847
2410
|
throw new APIError$1("BAD_REQUEST", {
|
|
@@ -2884,17 +2447,17 @@ async function revokeAccessToken(ctx, opts, clientId, token) {
|
|
|
2884
2447
|
async function revokeEndpoint(ctx, opts) {
|
|
2885
2448
|
let { token, token_type_hint } = ctx.body;
|
|
2886
2449
|
if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
|
|
2887
|
-
const { clientId: client_id, clientSecret: client_secret,
|
|
2450
|
+
const { clientId: client_id, clientSecret: client_secret, preVerified, authMethod } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/revoke`));
|
|
2888
2451
|
if (!client_id) throw new APIError$1("UNAUTHORIZED", {
|
|
2889
2452
|
error_description: "missing required credentials",
|
|
2890
2453
|
error: "invalid_client"
|
|
2891
2454
|
});
|
|
2892
|
-
if (typeof token === "string"
|
|
2455
|
+
if (typeof token === "string") token = stripAccessTokenAuthorizationScheme(token);
|
|
2893
2456
|
if (!token?.length) throw new APIError$1("BAD_REQUEST", {
|
|
2894
2457
|
error_description: "missing a required token for introspection",
|
|
2895
2458
|
error: "invalid_request"
|
|
2896
2459
|
});
|
|
2897
|
-
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0,
|
|
2460
|
+
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, void 0, authMethod);
|
|
2898
2461
|
try {
|
|
2899
2462
|
if (token_type_hint === void 0 || token_type_hint === "access_token") try {
|
|
2900
2463
|
return await revokeAccessToken(ctx, opts, client.clientId, token);
|
|
@@ -3067,6 +2630,11 @@ const schema = {
|
|
|
3067
2630
|
type: "boolean",
|
|
3068
2631
|
required: false
|
|
3069
2632
|
},
|
|
2633
|
+
dpopBoundAccessTokens: {
|
|
2634
|
+
type: "boolean",
|
|
2635
|
+
required: false,
|
|
2636
|
+
defaultValue: false
|
|
2637
|
+
},
|
|
3070
2638
|
referenceId: {
|
|
3071
2639
|
type: "string",
|
|
3072
2640
|
required: false
|
|
@@ -3077,6 +2645,104 @@ const schema = {
|
|
|
3077
2645
|
}
|
|
3078
2646
|
}
|
|
3079
2647
|
},
|
|
2648
|
+
oauthResource: {
|
|
2649
|
+
modelName: "oauthResource",
|
|
2650
|
+
fields: {
|
|
2651
|
+
identifier: {
|
|
2652
|
+
type: "string",
|
|
2653
|
+
required: true,
|
|
2654
|
+
unique: true
|
|
2655
|
+
},
|
|
2656
|
+
name: {
|
|
2657
|
+
type: "string",
|
|
2658
|
+
required: true
|
|
2659
|
+
},
|
|
2660
|
+
accessTokenTtl: {
|
|
2661
|
+
type: "number",
|
|
2662
|
+
required: false
|
|
2663
|
+
},
|
|
2664
|
+
refreshTokenTtl: {
|
|
2665
|
+
type: "number",
|
|
2666
|
+
required: false
|
|
2667
|
+
},
|
|
2668
|
+
signingAlgorithm: {
|
|
2669
|
+
type: "string",
|
|
2670
|
+
required: false
|
|
2671
|
+
},
|
|
2672
|
+
signingKeyId: {
|
|
2673
|
+
type: "string",
|
|
2674
|
+
required: false
|
|
2675
|
+
},
|
|
2676
|
+
allowedScopes: {
|
|
2677
|
+
type: "string[]",
|
|
2678
|
+
required: false
|
|
2679
|
+
},
|
|
2680
|
+
customClaims: {
|
|
2681
|
+
type: "json",
|
|
2682
|
+
required: false
|
|
2683
|
+
},
|
|
2684
|
+
dpopBoundAccessTokensRequired: {
|
|
2685
|
+
type: "boolean",
|
|
2686
|
+
required: false,
|
|
2687
|
+
defaultValue: false
|
|
2688
|
+
},
|
|
2689
|
+
disabled: {
|
|
2690
|
+
type: "boolean",
|
|
2691
|
+
required: false,
|
|
2692
|
+
defaultValue: false
|
|
2693
|
+
},
|
|
2694
|
+
createdAt: {
|
|
2695
|
+
type: "date",
|
|
2696
|
+
required: false
|
|
2697
|
+
},
|
|
2698
|
+
updatedAt: {
|
|
2699
|
+
type: "date",
|
|
2700
|
+
required: false
|
|
2701
|
+
},
|
|
2702
|
+
policyVersion: {
|
|
2703
|
+
type: "number",
|
|
2704
|
+
required: false,
|
|
2705
|
+
defaultValue: 1
|
|
2706
|
+
},
|
|
2707
|
+
metadata: {
|
|
2708
|
+
type: "json",
|
|
2709
|
+
required: false
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
},
|
|
2713
|
+
oauthClientResource: {
|
|
2714
|
+
modelName: "oauthClientResource",
|
|
2715
|
+
fields: {
|
|
2716
|
+
clientId: {
|
|
2717
|
+
type: "string",
|
|
2718
|
+
required: true,
|
|
2719
|
+
references: {
|
|
2720
|
+
model: "oauthClient",
|
|
2721
|
+
field: "clientId",
|
|
2722
|
+
onDelete: "cascade"
|
|
2723
|
+
},
|
|
2724
|
+
index: true
|
|
2725
|
+
},
|
|
2726
|
+
resourceId: {
|
|
2727
|
+
type: "string",
|
|
2728
|
+
required: true,
|
|
2729
|
+
references: {
|
|
2730
|
+
model: "oauthResource",
|
|
2731
|
+
field: "identifier",
|
|
2732
|
+
onDelete: "cascade"
|
|
2733
|
+
},
|
|
2734
|
+
index: true
|
|
2735
|
+
},
|
|
2736
|
+
metadata: {
|
|
2737
|
+
type: "json",
|
|
2738
|
+
required: false
|
|
2739
|
+
},
|
|
2740
|
+
createdAt: {
|
|
2741
|
+
type: "date",
|
|
2742
|
+
required: false
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
},
|
|
3080
2746
|
oauthRefreshToken: { fields: {
|
|
3081
2747
|
token: {
|
|
3082
2748
|
type: "string",
|
|
@@ -3129,6 +2795,10 @@ const schema = {
|
|
|
3129
2795
|
type: "date",
|
|
3130
2796
|
required: false
|
|
3131
2797
|
},
|
|
2798
|
+
confirmation: {
|
|
2799
|
+
type: "json",
|
|
2800
|
+
required: false
|
|
2801
|
+
},
|
|
3132
2802
|
scopes: {
|
|
3133
2803
|
type: "string[]",
|
|
3134
2804
|
required: true
|
|
@@ -3192,6 +2862,10 @@ const schema = {
|
|
|
3192
2862
|
type: "date",
|
|
3193
2863
|
required: false
|
|
3194
2864
|
},
|
|
2865
|
+
confirmation: {
|
|
2866
|
+
type: "json",
|
|
2867
|
+
required: false
|
|
2868
|
+
},
|
|
3195
2869
|
scopes: {
|
|
3196
2870
|
type: "string[]",
|
|
3197
2871
|
required: true
|
|
@@ -3245,6 +2919,17 @@ const schema = {
|
|
|
3245
2919
|
};
|
|
3246
2920
|
//#endregion
|
|
3247
2921
|
//#region src/oauth.ts
|
|
2922
|
+
/**
|
|
2923
|
+
* Default scopes advertised and accepted when a configuration sets none. Shared
|
|
2924
|
+
* with the MCP preset so the resource metadata it serves matches what the
|
|
2925
|
+
* authorization-server metadata advertises.
|
|
2926
|
+
*/
|
|
2927
|
+
const DEFAULT_OAUTH_SCOPES = [
|
|
2928
|
+
"openid",
|
|
2929
|
+
"profile",
|
|
2930
|
+
"email",
|
|
2931
|
+
"offline_access"
|
|
2932
|
+
];
|
|
3248
2933
|
const oAuthState = defineRequestState(() => null);
|
|
3249
2934
|
const getOAuthProviderState = oAuthState.get;
|
|
3250
2935
|
const signedQueryIssuedAtMsKey = "signedQueryIssuedAtMs";
|
|
@@ -3266,12 +2951,7 @@ const oauthProvider = (options) => {
|
|
|
3266
2951
|
const _allowedScopes = clientRegistrationAllowedScopes ? new Set([...clientRegistrationAllowedScopes, ...options.clientRegistrationDefaultScopes]) : new Set([...options.clientRegistrationDefaultScopes]);
|
|
3267
2952
|
clientRegistrationAllowedScopes = Array.from(_allowedScopes);
|
|
3268
2953
|
}
|
|
3269
|
-
const scopes = new Set((options.scopes ??
|
|
3270
|
-
"openid",
|
|
3271
|
-
"profile",
|
|
3272
|
-
"email",
|
|
3273
|
-
"offline_access"
|
|
3274
|
-
]).filter((val) => val.length));
|
|
2954
|
+
const scopes = new Set((options.scopes ?? DEFAULT_OAUTH_SCOPES).filter((val) => val.length));
|
|
3275
2955
|
if (clientRegistrationAllowedScopes) {
|
|
3276
2956
|
for (const sc of clientRegistrationAllowedScopes) if (!scopes.has(sc)) throw new BetterAuthError(`clientRegistrationAllowedScope ${sc} not found in scopes`);
|
|
3277
2957
|
}
|
|
@@ -3313,6 +2993,7 @@ const oauthProvider = (options) => {
|
|
|
3313
2993
|
claims: Array.from(claims),
|
|
3314
2994
|
clientRegistrationAllowedScopes
|
|
3315
2995
|
};
|
|
2996
|
+
validateOAuthProviderExtensions(opts.extensions);
|
|
3316
2997
|
if (opts.pairwiseSecret && opts.pairwiseSecret.length < 32) throw new BetterAuthError("pairwiseSecret must be at least 32 characters long for adequate HMAC-SHA256 security");
|
|
3317
2998
|
if (opts.grantTypes && opts.grantTypes.includes("refresh_token") && !opts.grantTypes.includes("authorization_code")) throw new BetterAuthError("refresh_token grant requires authorization_code grant");
|
|
3318
2999
|
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");
|
|
@@ -3348,16 +3029,7 @@ const oauthProvider = (options) => {
|
|
|
3348
3029
|
}
|
|
3349
3030
|
if (isAuthServerMetadataRequest) {
|
|
3350
3031
|
if (opts.scopes?.includes("openid")) return { response: createMetadataResponse(oidcServerMetadata(endpointCtx, opts)) };
|
|
3351
|
-
return { response: createMetadataResponse(
|
|
3352
|
-
...authServerMetadata(endpointCtx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx)?.options, {
|
|
3353
|
-
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
3354
|
-
dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
|
|
3355
|
-
public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
|
|
3356
|
-
grant_types_supported: opts.grantTypes,
|
|
3357
|
-
jwt_disabled: opts.disableJwtPlugin
|
|
3358
|
-
}),
|
|
3359
|
-
...mergeDiscoveryMetadata(opts.clientDiscovery)
|
|
3360
|
-
}) };
|
|
3032
|
+
return { response: createMetadataResponse(oauthAuthorizationServerMetadata(endpointCtx, opts)) };
|
|
3361
3033
|
}
|
|
3362
3034
|
if (isOpenIdConfigRequest) return { response: createMetadataResponse(oidcServerMetadata(endpointCtx, opts)) };
|
|
3363
3035
|
};
|
|
@@ -3456,7 +3128,7 @@ const oauthProvider = (options) => {
|
|
|
3456
3128
|
type: "array",
|
|
3457
3129
|
items: { type: "string" }
|
|
3458
3130
|
},
|
|
3459
|
-
description: "Requested
|
|
3131
|
+
description: "Requested protected resource(s) for the access token. May be supplied multiple times as repeated 'resource' query parameters (RFC 8707) or as an array of strings."
|
|
3460
3132
|
},
|
|
3461
3133
|
{
|
|
3462
3134
|
name: "prompt",
|
|
@@ -3506,8 +3178,10 @@ const oauthProvider = (options) => {
|
|
|
3506
3178
|
version: PACKAGE_VERSION,
|
|
3507
3179
|
options: opts,
|
|
3508
3180
|
onRequest: handleIssuerMetadataRequest,
|
|
3509
|
-
init: (ctx) => {
|
|
3181
|
+
init: async (ctx) => {
|
|
3510
3182
|
if (ctx.options.secondaryStorage && ctx.options.session?.storeSessionInDatabase !== true) throw new BetterAuthError("OAuth Provider requires `session.storeSessionInDatabase: true` when using secondaryStorage");
|
|
3183
|
+
await seedResources(ctx, opts);
|
|
3184
|
+
logEnforcePerClientResourcesResolution(opts);
|
|
3511
3185
|
if (!opts.disableJwtPlugin) {
|
|
3512
3186
|
const jwtPluginOptions = getJwtPlugin(ctx)?.options;
|
|
3513
3187
|
const issuer = jwtPluginOptions?.jwt?.issuer ?? ctx.baseURL;
|
|
@@ -3589,16 +3263,7 @@ const oauthProvider = (options) => {
|
|
|
3589
3263
|
metadata: { SERVER_ONLY: true }
|
|
3590
3264
|
}, async (ctx) => {
|
|
3591
3265
|
if (opts.scopes && opts.scopes.includes("openid")) return oidcServerMetadata(ctx, opts);
|
|
3592
|
-
else return
|
|
3593
|
-
...authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
|
|
3594
|
-
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
3595
|
-
dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
|
|
3596
|
-
public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
|
|
3597
|
-
grant_types_supported: opts.grantTypes,
|
|
3598
|
-
jwt_disabled: opts.disableJwtPlugin
|
|
3599
|
-
}),
|
|
3600
|
-
...mergeDiscoveryMetadata(opts.clientDiscovery)
|
|
3601
|
-
};
|
|
3266
|
+
else return oauthAuthorizationServerMetadata(ctx, opts);
|
|
3602
3267
|
}),
|
|
3603
3268
|
getOpenIdConfig: createAuthEndpoint("/.well-known/openid-configuration", {
|
|
3604
3269
|
method: "GET",
|
|
@@ -3663,12 +3328,9 @@ const oauthProvider = (options) => {
|
|
|
3663
3328
|
}),
|
|
3664
3329
|
oauth2Token: createOAuthEndpoint("/oauth2/token", {
|
|
3665
3330
|
method: "POST",
|
|
3331
|
+
cloneRequest: true,
|
|
3666
3332
|
body: z.object({
|
|
3667
|
-
grant_type: z.string().
|
|
3668
|
-
"authorization_code",
|
|
3669
|
-
"client_credentials",
|
|
3670
|
-
"refresh_token"
|
|
3671
|
-
])),
|
|
3333
|
+
grant_type: z.string().trim().min(1),
|
|
3672
3334
|
client_id: z.string().optional(),
|
|
3673
3335
|
client_secret: z.string().optional(),
|
|
3674
3336
|
client_assertion: z.string().optional(),
|
|
@@ -3679,7 +3341,7 @@ const oauthProvider = (options) => {
|
|
|
3679
3341
|
refresh_token: z.string().optional(),
|
|
3680
3342
|
resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional(),
|
|
3681
3343
|
scope: z.string().optional()
|
|
3682
|
-
}),
|
|
3344
|
+
}).passthrough(),
|
|
3683
3345
|
errorCodesByField: {
|
|
3684
3346
|
grant_type: {
|
|
3685
3347
|
missing: "invalid_request",
|
|
@@ -3688,9 +3350,17 @@ const oauthProvider = (options) => {
|
|
|
3688
3350
|
resource: { invalid: "invalid_target" }
|
|
3689
3351
|
},
|
|
3690
3352
|
metadata: {
|
|
3353
|
+
noStore: true,
|
|
3691
3354
|
allowedMediaTypes: ["application/x-www-form-urlencoded"],
|
|
3692
3355
|
openapi: {
|
|
3693
3356
|
description: "Obtain an OAuth2.1 access token",
|
|
3357
|
+
parameters: [{
|
|
3358
|
+
name: "DPoP",
|
|
3359
|
+
in: "header",
|
|
3360
|
+
required: false,
|
|
3361
|
+
schema: { type: "string" },
|
|
3362
|
+
description: "RFC 9449 DPoP proof JWT for issuing DPoP-bound tokens"
|
|
3363
|
+
}],
|
|
3694
3364
|
requestBody: {
|
|
3695
3365
|
required: true,
|
|
3696
3366
|
content: { "application/json": { schema: {
|
|
@@ -3698,11 +3368,6 @@ const oauthProvider = (options) => {
|
|
|
3698
3368
|
properties: {
|
|
3699
3369
|
grant_type: {
|
|
3700
3370
|
type: "string",
|
|
3701
|
-
enum: [
|
|
3702
|
-
"authorization_code",
|
|
3703
|
-
"client_credentials",
|
|
3704
|
-
"refresh_token"
|
|
3705
|
-
],
|
|
3706
3371
|
description: "OAuth2 grant type"
|
|
3707
3372
|
},
|
|
3708
3373
|
client_id: {
|
|
@@ -3739,7 +3404,7 @@ const oauthProvider = (options) => {
|
|
|
3739
3404
|
items: { type: "string" },
|
|
3740
3405
|
description: "Multiple resources (URLs)"
|
|
3741
3406
|
}],
|
|
3742
|
-
description: "Requested
|
|
3407
|
+
description: "Requested protected resource(s) for the access token"
|
|
3743
3408
|
},
|
|
3744
3409
|
scope: {
|
|
3745
3410
|
type: "string",
|
|
@@ -3762,7 +3427,7 @@ const oauthProvider = (options) => {
|
|
|
3762
3427
|
token_type: {
|
|
3763
3428
|
type: "string",
|
|
3764
3429
|
description: "The type of the token issued",
|
|
3765
|
-
enum: ["Bearer"]
|
|
3430
|
+
enum: ["Bearer", "DPoP"]
|
|
3766
3431
|
},
|
|
3767
3432
|
expires_in: {
|
|
3768
3433
|
type: "number",
|
|
@@ -3804,6 +3469,10 @@ const oauthProvider = (options) => {
|
|
|
3804
3469
|
}
|
|
3805
3470
|
}
|
|
3806
3471
|
}, async (ctx) => {
|
|
3472
|
+
if (ctx.request) {
|
|
3473
|
+
const repeated = await extractRepeatedResourceFromForm(ctx.request);
|
|
3474
|
+
if (repeated && repeated.length > 1) ctx.body.resource = repeated;
|
|
3475
|
+
}
|
|
3807
3476
|
return tokenEndpoint(ctx, opts);
|
|
3808
3477
|
}),
|
|
3809
3478
|
oauth2Introspect: createOAuthEndpoint("/oauth2/introspect", {
|
|
@@ -3817,6 +3486,7 @@ const oauthProvider = (options) => {
|
|
|
3817
3486
|
token_type_hint: z.string().optional()
|
|
3818
3487
|
}),
|
|
3819
3488
|
metadata: {
|
|
3489
|
+
noStore: true,
|
|
3820
3490
|
allowedMediaTypes: ["application/x-www-form-urlencoded"],
|
|
3821
3491
|
openapi: {
|
|
3822
3492
|
description: "Introspect an OAuth2 access or refresh token",
|
|
@@ -3988,90 +3658,99 @@ const oauthProvider = (options) => {
|
|
|
3988
3658
|
}),
|
|
3989
3659
|
oauth2UserInfo: createAuthEndpoint("/oauth2/userinfo", {
|
|
3990
3660
|
method: ["GET", "POST"],
|
|
3991
|
-
metadata: {
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
"
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
3661
|
+
metadata: {
|
|
3662
|
+
noStore: true,
|
|
3663
|
+
openapi: {
|
|
3664
|
+
description: "Get OpenID Connect user information (UserInfo endpoint)",
|
|
3665
|
+
security: [{ bearerAuth: [] }, { OAuth2: [
|
|
3666
|
+
"openid",
|
|
3667
|
+
"profile",
|
|
3668
|
+
"email"
|
|
3669
|
+
] }],
|
|
3670
|
+
parameters: [{
|
|
3671
|
+
name: "Authorization",
|
|
3672
|
+
in: "header",
|
|
3673
|
+
required: false,
|
|
3674
|
+
schema: { type: "string" },
|
|
3675
|
+
description: "Bearer or DPoP access token"
|
|
3676
|
+
}, {
|
|
3677
|
+
name: "DPoP",
|
|
3678
|
+
in: "header",
|
|
3679
|
+
required: false,
|
|
3680
|
+
schema: { type: "string" },
|
|
3681
|
+
description: "RFC 9449 DPoP proof JWT when using a DPoP-bound access token"
|
|
3682
|
+
}],
|
|
3683
|
+
responses: {
|
|
3684
|
+
"200": {
|
|
3685
|
+
description: "User information retrieved successfully",
|
|
3686
|
+
content: { "application/json": { schema: {
|
|
3687
|
+
type: "object",
|
|
3688
|
+
properties: {
|
|
3689
|
+
sub: {
|
|
3690
|
+
type: "string",
|
|
3691
|
+
description: "Subject identifier (user ID)"
|
|
3692
|
+
},
|
|
3693
|
+
email: {
|
|
3694
|
+
type: "string",
|
|
3695
|
+
format: "email",
|
|
3696
|
+
nullable: true,
|
|
3697
|
+
description: "User's email address, included if 'email' scope is granted"
|
|
3698
|
+
},
|
|
3699
|
+
name: {
|
|
3700
|
+
type: "string",
|
|
3701
|
+
nullable: true,
|
|
3702
|
+
description: "User's full name, included if 'profile' scope is granted"
|
|
3703
|
+
},
|
|
3704
|
+
picture: {
|
|
3705
|
+
type: "string",
|
|
3706
|
+
format: "uri",
|
|
3707
|
+
nullable: true,
|
|
3708
|
+
description: "User's profile picture URL, included if 'profile' scope is granted"
|
|
3709
|
+
},
|
|
3710
|
+
given_name: {
|
|
3711
|
+
type: "string",
|
|
3712
|
+
nullable: true,
|
|
3713
|
+
description: "User's given name, included if 'profile' scope is granted"
|
|
3714
|
+
},
|
|
3715
|
+
family_name: {
|
|
3716
|
+
type: "string",
|
|
3717
|
+
nullable: true,
|
|
3718
|
+
description: "User's family name, included if 'profile' scope is granted"
|
|
3719
|
+
},
|
|
3720
|
+
email_verified: {
|
|
3721
|
+
type: "boolean",
|
|
3722
|
+
nullable: true,
|
|
3723
|
+
description: "Whether the email is verified, included if 'email' scope is granted"
|
|
3724
|
+
}
|
|
4031
3725
|
},
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
3726
|
+
required: ["sub"]
|
|
3727
|
+
} } }
|
|
3728
|
+
},
|
|
3729
|
+
"401": {
|
|
3730
|
+
description: "Unauthorized - invalid or missing access token",
|
|
3731
|
+
content: { "application/json": { schema: {
|
|
3732
|
+
type: "object",
|
|
3733
|
+
properties: {
|
|
3734
|
+
error: { type: "string" },
|
|
3735
|
+
error_description: { type: "string" }
|
|
4036
3736
|
},
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
3737
|
+
required: ["error"]
|
|
3738
|
+
} } }
|
|
3739
|
+
},
|
|
3740
|
+
"403": {
|
|
3741
|
+
description: "Forbidden - insufficient scope",
|
|
3742
|
+
content: { "application/json": { schema: {
|
|
3743
|
+
type: "object",
|
|
3744
|
+
properties: {
|
|
3745
|
+
error: { type: "string" },
|
|
3746
|
+
error_description: { type: "string" }
|
|
4041
3747
|
},
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
description: "Whether the email is verified, included if 'email' scope is granted"
|
|
4046
|
-
}
|
|
4047
|
-
},
|
|
4048
|
-
required: ["sub"]
|
|
4049
|
-
} } }
|
|
4050
|
-
},
|
|
4051
|
-
"401": {
|
|
4052
|
-
description: "Unauthorized - invalid or missing access token",
|
|
4053
|
-
content: { "application/json": { schema: {
|
|
4054
|
-
type: "object",
|
|
4055
|
-
properties: {
|
|
4056
|
-
error: { type: "string" },
|
|
4057
|
-
error_description: { type: "string" }
|
|
4058
|
-
},
|
|
4059
|
-
required: ["error"]
|
|
4060
|
-
} } }
|
|
4061
|
-
},
|
|
4062
|
-
"403": {
|
|
4063
|
-
description: "Forbidden - insufficient scope",
|
|
4064
|
-
content: { "application/json": { schema: {
|
|
4065
|
-
type: "object",
|
|
4066
|
-
properties: {
|
|
4067
|
-
error: { type: "string" },
|
|
4068
|
-
error_description: { type: "string" }
|
|
4069
|
-
},
|
|
4070
|
-
required: ["error"]
|
|
4071
|
-
} } }
|
|
3748
|
+
required: ["error"]
|
|
3749
|
+
} } }
|
|
3750
|
+
}
|
|
4072
3751
|
}
|
|
4073
3752
|
}
|
|
4074
|
-
}
|
|
3753
|
+
}
|
|
4075
3754
|
}, async (ctx) => {
|
|
4076
3755
|
return userInfoEndpoint(ctx, opts);
|
|
4077
3756
|
}),
|
|
@@ -4108,194 +3787,149 @@ const oauthProvider = (options) => {
|
|
|
4108
3787
|
}),
|
|
4109
3788
|
registerOAuthClient: createOAuthEndpoint("/oauth2/register", {
|
|
4110
3789
|
method: "POST",
|
|
4111
|
-
body:
|
|
4112
|
-
redirect_uris: z.array(SafeUrlSchema).min(1).min(1),
|
|
4113
|
-
scope: z.string().optional(),
|
|
4114
|
-
client_name: z.string().optional(),
|
|
4115
|
-
client_uri: z.string().optional(),
|
|
4116
|
-
logo_uri: z.string().optional(),
|
|
4117
|
-
contacts: z.array(z.string().min(1)).min(1).optional(),
|
|
4118
|
-
tos_uri: z.string().optional(),
|
|
4119
|
-
policy_uri: z.string().optional(),
|
|
4120
|
-
software_id: z.string().optional(),
|
|
4121
|
-
software_version: z.string().optional(),
|
|
4122
|
-
software_statement: z.string().optional(),
|
|
4123
|
-
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
4124
|
-
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
4125
|
-
backchannel_logout_session_required: z.boolean().optional(),
|
|
4126
|
-
token_endpoint_auth_method: z.enum([
|
|
4127
|
-
"none",
|
|
4128
|
-
"client_secret_basic",
|
|
4129
|
-
"client_secret_post",
|
|
4130
|
-
"private_key_jwt"
|
|
4131
|
-
]).default("client_secret_basic").optional(),
|
|
4132
|
-
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(),
|
|
4133
|
-
jwks_uri: z.string().optional(),
|
|
4134
|
-
grant_types: z.array(z.enum([
|
|
4135
|
-
"authorization_code",
|
|
4136
|
-
"client_credentials",
|
|
4137
|
-
"refresh_token"
|
|
4138
|
-
])).default(["authorization_code"]).optional(),
|
|
4139
|
-
response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
|
|
4140
|
-
type: z.enum([
|
|
4141
|
-
"web",
|
|
4142
|
-
"native",
|
|
4143
|
-
"user-agent-based"
|
|
4144
|
-
]).optional(),
|
|
4145
|
-
subject_type: z.enum(["public", "pairwise"]).optional(),
|
|
4146
|
-
skip_consent: z.never({ error: "skip_consent cannot be set during dynamic client registration" }).optional()
|
|
4147
|
-
}),
|
|
3790
|
+
body: clientRegistrationRequestSchema,
|
|
4148
3791
|
errorCodesByField: {
|
|
4149
3792
|
redirect_uris: "invalid_redirect_uri",
|
|
4150
3793
|
post_logout_redirect_uris: "invalid_redirect_uri",
|
|
4151
|
-
software_statement: "invalid_software_statement"
|
|
3794
|
+
software_statement: "invalid_software_statement",
|
|
3795
|
+
resources: "invalid_target"
|
|
4152
3796
|
},
|
|
4153
3797
|
defaultError: "invalid_client_metadata",
|
|
4154
|
-
metadata: {
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
description: "OAuth2 application
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
},
|
|
4165
|
-
client_secret: {
|
|
4166
|
-
type: "string",
|
|
4167
|
-
description: "Secret key for the client"
|
|
4168
|
-
},
|
|
4169
|
-
client_secret_expires_at: {
|
|
4170
|
-
type: "number",
|
|
4171
|
-
description: "Time the client secret will expire. If 0, the client secret will never expire."
|
|
4172
|
-
},
|
|
4173
|
-
scope: {
|
|
4174
|
-
type: "string",
|
|
4175
|
-
description: "Space-separated scopes allowed by the client"
|
|
4176
|
-
},
|
|
4177
|
-
user_id: {
|
|
4178
|
-
type: "string",
|
|
4179
|
-
description: "ID of the user who registered the client, null if registered anonymously"
|
|
4180
|
-
},
|
|
4181
|
-
client_id_issued_at: {
|
|
4182
|
-
type: "number",
|
|
4183
|
-
description: "Creation timestamp of this client"
|
|
4184
|
-
},
|
|
4185
|
-
client_name: {
|
|
4186
|
-
type: "string",
|
|
4187
|
-
description: "Name of the OAuth2 application"
|
|
4188
|
-
},
|
|
4189
|
-
client_uri: {
|
|
4190
|
-
type: "string",
|
|
4191
|
-
description: "Name of the OAuth2 application"
|
|
4192
|
-
},
|
|
4193
|
-
logo_uri: {
|
|
4194
|
-
type: "string",
|
|
4195
|
-
description: "Icon URL for the application"
|
|
4196
|
-
},
|
|
4197
|
-
contacts: {
|
|
4198
|
-
type: "array",
|
|
4199
|
-
items: { type: "string" },
|
|
4200
|
-
description: "List representing ways to contact people responsible for this client, typically email addresses"
|
|
4201
|
-
},
|
|
4202
|
-
tos_uri: {
|
|
4203
|
-
type: "string",
|
|
4204
|
-
description: "Client's terms of service uri"
|
|
4205
|
-
},
|
|
4206
|
-
policy_uri: {
|
|
4207
|
-
type: "string",
|
|
4208
|
-
description: "Client's policy uri"
|
|
4209
|
-
},
|
|
4210
|
-
software_id: {
|
|
4211
|
-
type: "string",
|
|
4212
|
-
description: "Unique identifier assigned by the developer to help in the dynamic registration process"
|
|
4213
|
-
},
|
|
4214
|
-
software_version: {
|
|
4215
|
-
type: "string",
|
|
4216
|
-
description: "Version identifier for the software_id"
|
|
4217
|
-
},
|
|
4218
|
-
software_statement: {
|
|
4219
|
-
type: "string",
|
|
4220
|
-
description: "JWT containing metadata values about the client software as claims"
|
|
4221
|
-
},
|
|
4222
|
-
redirect_uris: {
|
|
4223
|
-
type: "array",
|
|
4224
|
-
items: {
|
|
3798
|
+
metadata: {
|
|
3799
|
+
noStore: true,
|
|
3800
|
+
openapi: {
|
|
3801
|
+
description: "Register an OAuth2 application",
|
|
3802
|
+
responses: { "201": {
|
|
3803
|
+
description: "OAuth2 application registered successfully",
|
|
3804
|
+
content: { "application/json": { schema: {
|
|
3805
|
+
type: "object",
|
|
3806
|
+
properties: {
|
|
3807
|
+
client_id: {
|
|
4225
3808
|
type: "string",
|
|
4226
|
-
|
|
3809
|
+
description: "Unique identifier for the client"
|
|
4227
3810
|
},
|
|
4228
|
-
|
|
4229
|
-
},
|
|
4230
|
-
post_logout_redirect_uris: {
|
|
4231
|
-
type: "array",
|
|
4232
|
-
items: {
|
|
3811
|
+
client_secret: {
|
|
4233
3812
|
type: "string",
|
|
4234
|
-
|
|
3813
|
+
description: "Secret key for the client"
|
|
4235
3814
|
},
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
description: "RP URL to receive signed Logout Tokens when the end-user's OP session terminates"
|
|
4242
|
-
},
|
|
4243
|
-
backchannel_logout_session_required: {
|
|
4244
|
-
type: "boolean",
|
|
4245
|
-
description: "Whether the RP requires a `sid` claim in every Logout Token"
|
|
4246
|
-
},
|
|
4247
|
-
token_endpoint_auth_method: {
|
|
4248
|
-
type: "string",
|
|
4249
|
-
description: "Requested authentication method for the token endpoint",
|
|
4250
|
-
enum: [
|
|
4251
|
-
"none",
|
|
4252
|
-
"client_secret_basic",
|
|
4253
|
-
"client_secret_post",
|
|
4254
|
-
"private_key_jwt"
|
|
4255
|
-
]
|
|
4256
|
-
},
|
|
4257
|
-
grant_types: {
|
|
4258
|
-
type: "array",
|
|
4259
|
-
items: {
|
|
3815
|
+
client_secret_expires_at: {
|
|
3816
|
+
type: "number",
|
|
3817
|
+
description: "Time the client secret will expire. If 0, the client secret will never expire."
|
|
3818
|
+
},
|
|
3819
|
+
scope: {
|
|
4260
3820
|
type: "string",
|
|
4261
|
-
|
|
4262
|
-
"authorization_code",
|
|
4263
|
-
"client_credentials",
|
|
4264
|
-
"refresh_token"
|
|
4265
|
-
]
|
|
3821
|
+
description: "Space-separated scopes allowed by the client"
|
|
4266
3822
|
},
|
|
4267
|
-
|
|
4268
|
-
},
|
|
4269
|
-
response_types: {
|
|
4270
|
-
type: "array",
|
|
4271
|
-
items: {
|
|
3823
|
+
user_id: {
|
|
4272
3824
|
type: "string",
|
|
4273
|
-
|
|
3825
|
+
description: "ID of the user who registered the client, null if registered anonymously"
|
|
4274
3826
|
},
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
"
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
3827
|
+
client_id_issued_at: {
|
|
3828
|
+
type: "number",
|
|
3829
|
+
description: "Creation timestamp of this client"
|
|
3830
|
+
},
|
|
3831
|
+
client_name: {
|
|
3832
|
+
type: "string",
|
|
3833
|
+
description: "Name of the OAuth2 application"
|
|
3834
|
+
},
|
|
3835
|
+
client_uri: {
|
|
3836
|
+
type: "string",
|
|
3837
|
+
description: "Name of the OAuth2 application"
|
|
3838
|
+
},
|
|
3839
|
+
logo_uri: {
|
|
3840
|
+
type: "string",
|
|
3841
|
+
description: "Icon URL for the application"
|
|
3842
|
+
},
|
|
3843
|
+
contacts: {
|
|
3844
|
+
type: "array",
|
|
3845
|
+
items: { type: "string" },
|
|
3846
|
+
description: "List representing ways to contact people responsible for this client, typically email addresses"
|
|
3847
|
+
},
|
|
3848
|
+
tos_uri: {
|
|
3849
|
+
type: "string",
|
|
3850
|
+
description: "Client's terms of service uri"
|
|
3851
|
+
},
|
|
3852
|
+
policy_uri: {
|
|
3853
|
+
type: "string",
|
|
3854
|
+
description: "Client's policy uri"
|
|
3855
|
+
},
|
|
3856
|
+
software_id: {
|
|
3857
|
+
type: "string",
|
|
3858
|
+
description: "Unique identifier assigned by the developer to help in the dynamic registration process"
|
|
3859
|
+
},
|
|
3860
|
+
software_version: {
|
|
3861
|
+
type: "string",
|
|
3862
|
+
description: "Version identifier for the software_id"
|
|
3863
|
+
},
|
|
3864
|
+
software_statement: {
|
|
3865
|
+
type: "string",
|
|
3866
|
+
description: "JWT containing metadata values about the client software as claims"
|
|
3867
|
+
},
|
|
3868
|
+
redirect_uris: {
|
|
3869
|
+
type: "array",
|
|
3870
|
+
items: {
|
|
3871
|
+
type: "string",
|
|
3872
|
+
format: "uri"
|
|
3873
|
+
},
|
|
3874
|
+
description: "List of allowed redirect uris"
|
|
3875
|
+
},
|
|
3876
|
+
post_logout_redirect_uris: {
|
|
3877
|
+
type: "array",
|
|
3878
|
+
items: {
|
|
3879
|
+
type: "string",
|
|
3880
|
+
format: "uri"
|
|
3881
|
+
},
|
|
3882
|
+
description: "List of allowed logout redirect uris"
|
|
3883
|
+
},
|
|
3884
|
+
backchannel_logout_uri: {
|
|
3885
|
+
type: "string",
|
|
3886
|
+
format: "uri",
|
|
3887
|
+
description: "RP URL to receive signed Logout Tokens when the end-user's OP session terminates"
|
|
3888
|
+
},
|
|
3889
|
+
backchannel_logout_session_required: {
|
|
3890
|
+
type: "boolean",
|
|
3891
|
+
description: "Whether the RP requires a `sid` claim in every Logout Token"
|
|
3892
|
+
},
|
|
3893
|
+
token_endpoint_auth_method: {
|
|
3894
|
+
type: "string",
|
|
3895
|
+
description: "Requested authentication method for the token endpoint"
|
|
3896
|
+
},
|
|
3897
|
+
grant_types: {
|
|
3898
|
+
type: "array",
|
|
3899
|
+
items: { type: "string" },
|
|
3900
|
+
description: "Grant types the client may use at the token endpoint"
|
|
3901
|
+
},
|
|
3902
|
+
response_types: {
|
|
3903
|
+
type: "array",
|
|
3904
|
+
items: {
|
|
3905
|
+
type: "string",
|
|
3906
|
+
enum: ["code"]
|
|
3907
|
+
},
|
|
3908
|
+
description: "Response types the client may use at the authorization endpoint"
|
|
3909
|
+
},
|
|
3910
|
+
public: {
|
|
3911
|
+
type: "boolean",
|
|
3912
|
+
description: "Whether the client is public as determined by the type"
|
|
3913
|
+
},
|
|
3914
|
+
type: {
|
|
3915
|
+
type: "string",
|
|
3916
|
+
description: "Type of the client",
|
|
3917
|
+
enum: [
|
|
3918
|
+
"web",
|
|
3919
|
+
"native",
|
|
3920
|
+
"user-agent-based"
|
|
3921
|
+
]
|
|
3922
|
+
},
|
|
3923
|
+
disabled: {
|
|
3924
|
+
type: "boolean",
|
|
3925
|
+
description: "Whether the client is disabled"
|
|
3926
|
+
}
|
|
4289
3927
|
},
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
required: ["client_id"]
|
|
4296
|
-
} } }
|
|
4297
|
-
} }
|
|
4298
|
-
} }
|
|
3928
|
+
required: ["client_id"]
|
|
3929
|
+
} } }
|
|
3930
|
+
} }
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
4299
3933
|
}, async (ctx) => {
|
|
4300
3934
|
return registerEndpoint(ctx, opts);
|
|
4301
3935
|
}),
|
|
@@ -4312,7 +3946,14 @@ const oauthProvider = (options) => {
|
|
|
4312
3946
|
getOAuthConsent: getOAuthConsent(opts),
|
|
4313
3947
|
getOAuthConsents: getOAuthConsents(opts),
|
|
4314
3948
|
updateOAuthConsent: updateOAuthConsent(opts),
|
|
4315
|
-
deleteOAuthConsent: deleteOAuthConsent(opts)
|
|
3949
|
+
deleteOAuthConsent: deleteOAuthConsent(opts),
|
|
3950
|
+
adminCreateOAuthResource: adminCreateOAuthResource(opts),
|
|
3951
|
+
adminListOAuthResources: adminListOAuthResources(opts),
|
|
3952
|
+
adminGetOAuthResource: adminGetOAuthResource(opts),
|
|
3953
|
+
adminUpdateOAuthResource: adminUpdateOAuthResource(opts),
|
|
3954
|
+
adminDeleteOAuthResource: adminDeleteOAuthResource(opts),
|
|
3955
|
+
adminLinkClientResource: adminLinkClientResource(opts),
|
|
3956
|
+
adminUnlinkClientResource: adminUnlinkClientResource(opts)
|
|
4316
3957
|
},
|
|
4317
3958
|
schema: mergeSchema(schema, opts?.schema),
|
|
4318
3959
|
rateLimit: [
|
|
@@ -4558,6 +4199,20 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
4558
4199
|
requestedScopes = client.scopes ?? opts.scopes ?? [];
|
|
4559
4200
|
query.scope = requestedScopes.join(" ");
|
|
4560
4201
|
}
|
|
4202
|
+
if (query.resource !== void 0) try {
|
|
4203
|
+
await resolveResourcePolicy(ctx, opts, {
|
|
4204
|
+
resource: query.resource,
|
|
4205
|
+
clientId: client.clientId,
|
|
4206
|
+
requestedScopes
|
|
4207
|
+
});
|
|
4208
|
+
} catch (err) {
|
|
4209
|
+
if (err instanceof APIError$1) {
|
|
4210
|
+
const error = err.body?.error ?? "invalid_target";
|
|
4211
|
+
const description = err.body?.error_description ?? "requested resource invalid";
|
|
4212
|
+
return handleRedirect(ctx, formatErrorURL(query.redirect_uri, error, description, query.state, getIssuer(ctx, opts)));
|
|
4213
|
+
}
|
|
4214
|
+
throw err;
|
|
4215
|
+
}
|
|
4561
4216
|
const pkceRequired = isPKCERequired(client, requestedScopes);
|
|
4562
4217
|
if (pkceRequired) {
|
|
4563
4218
|
if (!query.code_challenge || !query.code_challenge_method) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", pkceRequired.valueOf(), query.state, getIssuer(ctx, opts)));
|
|
@@ -4566,9 +4221,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
4566
4221
|
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)));
|
|
4567
4222
|
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)));
|
|
4568
4223
|
}
|
|
4569
|
-
const
|
|
4570
|
-
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)));
|
|
4571
|
-
const requestedResources = toResourceList(resource) ?? [];
|
|
4224
|
+
const requestedResources = toResourceList(query.resource) ?? [];
|
|
4572
4225
|
const session = await getSessionFromCtx(ctx);
|
|
4573
4226
|
const maxAgeSeconds = query.max_age;
|
|
4574
4227
|
const hasSatisfiedMaxAge = session != null && maxAgeSeconds !== void 0 && isWithinMaxAge(new Date(session.session.createdAt), maxAgeSeconds, /* @__PURE__ */ new Date());
|
|
@@ -4720,126 +4373,13 @@ async function signParams(ctx, opts, flags) {
|
|
|
4720
4373
|
const params = serializeAuthorizationQuery(ctx.query);
|
|
4721
4374
|
params.set("exp", String(exp));
|
|
4722
4375
|
params.set(signedQueryIssuedAtParam, String(issuedAt));
|
|
4376
|
+
params.delete("sig");
|
|
4723
4377
|
params.delete(postLoginClearedParam);
|
|
4724
4378
|
if (flags?.postLoginClearedForSession) params.set(postLoginClearedParam, flags.postLoginClearedForSession);
|
|
4725
|
-
|
|
4726
|
-
params.
|
|
4379
|
+
setSignedOAuthQueryParameterNames(params);
|
|
4380
|
+
const signature = await makeSignature(canonicalizeOAuthQueryParams(params).toString(), ctx.context.secret);
|
|
4381
|
+
params.set("sig", signature);
|
|
4727
4382
|
return params.toString();
|
|
4728
4383
|
}
|
|
4729
4384
|
//#endregion
|
|
4730
|
-
|
|
4731
|
-
function authServerMetadata(ctx, opts, overrides) {
|
|
4732
|
-
const baseURL = ctx.context.baseURL;
|
|
4733
|
-
const backchannelSupported = !overrides?.jwt_disabled;
|
|
4734
|
-
return {
|
|
4735
|
-
scopes_supported: overrides?.scopes_supported,
|
|
4736
|
-
issuer: validateIssuerUrl(opts?.jwt?.issuer ?? baseURL),
|
|
4737
|
-
authorization_endpoint: `${baseURL}/oauth2/authorize`,
|
|
4738
|
-
token_endpoint: `${baseURL}/oauth2/token`,
|
|
4739
|
-
jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
|
|
4740
|
-
registration_endpoint: overrides?.dynamic_client_registration_supported ? `${baseURL}/oauth2/register` : void 0,
|
|
4741
|
-
introspection_endpoint: `${baseURL}/oauth2/introspect`,
|
|
4742
|
-
revocation_endpoint: `${baseURL}/oauth2/revoke`,
|
|
4743
|
-
response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
|
|
4744
|
-
response_modes_supported: ["query"],
|
|
4745
|
-
grant_types_supported: overrides?.grant_types_supported ?? [
|
|
4746
|
-
"authorization_code",
|
|
4747
|
-
"client_credentials",
|
|
4748
|
-
"refresh_token"
|
|
4749
|
-
],
|
|
4750
|
-
token_endpoint_auth_methods_supported: [
|
|
4751
|
-
...overrides?.public_client_supported ? ["none"] : [],
|
|
4752
|
-
"client_secret_basic",
|
|
4753
|
-
"client_secret_post",
|
|
4754
|
-
"private_key_jwt"
|
|
4755
|
-
],
|
|
4756
|
-
token_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
|
|
4757
|
-
introspection_endpoint_auth_methods_supported: [
|
|
4758
|
-
"client_secret_basic",
|
|
4759
|
-
"client_secret_post",
|
|
4760
|
-
"private_key_jwt"
|
|
4761
|
-
],
|
|
4762
|
-
introspection_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
|
|
4763
|
-
revocation_endpoint_auth_methods_supported: [
|
|
4764
|
-
"client_secret_basic",
|
|
4765
|
-
"client_secret_post",
|
|
4766
|
-
"private_key_jwt"
|
|
4767
|
-
],
|
|
4768
|
-
revocation_endpoint_auth_signing_alg_values_supported: [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS],
|
|
4769
|
-
code_challenge_methods_supported: ["S256"],
|
|
4770
|
-
authorization_response_iss_parameter_supported: true,
|
|
4771
|
-
backchannel_logout_supported: backchannelSupported,
|
|
4772
|
-
backchannel_logout_session_supported: backchannelSupported
|
|
4773
|
-
};
|
|
4774
|
-
}
|
|
4775
|
-
function oidcServerMetadata(ctx, opts) {
|
|
4776
|
-
const baseURL = ctx.context.baseURL;
|
|
4777
|
-
const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
|
|
4778
|
-
return {
|
|
4779
|
-
...authServerMetadata(ctx, jwtPluginOptions, {
|
|
4780
|
-
scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
|
|
4781
|
-
dynamic_client_registration_supported: opts.allowDynamicClientRegistration,
|
|
4782
|
-
public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
|
|
4783
|
-
grant_types_supported: opts.grantTypes,
|
|
4784
|
-
jwt_disabled: opts.disableJwtPlugin
|
|
4785
|
-
}),
|
|
4786
|
-
claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
|
|
4787
|
-
userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
|
|
4788
|
-
subject_types_supported: opts.pairwiseSecret ? ["public", "pairwise"] : ["public"],
|
|
4789
|
-
id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
|
|
4790
|
-
end_session_endpoint: `${baseURL}/oauth2/end-session`,
|
|
4791
|
-
acr_values_supported: ["urn:mace:incommon:iap:bronze"],
|
|
4792
|
-
prompt_values_supported: [
|
|
4793
|
-
"login",
|
|
4794
|
-
"consent",
|
|
4795
|
-
"create",
|
|
4796
|
-
"select_account",
|
|
4797
|
-
"none"
|
|
4798
|
-
],
|
|
4799
|
-
...mergeDiscoveryMetadata(opts.clientDiscovery)
|
|
4800
|
-
};
|
|
4801
|
-
}
|
|
4802
|
-
const METADATA_CACHE_CONTROL = "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400";
|
|
4803
|
-
function metadataResponse(body, extraHeaders) {
|
|
4804
|
-
const headers = new Headers(extraHeaders);
|
|
4805
|
-
if (!headers.has("Cache-Control")) headers.set("Cache-Control", METADATA_CACHE_CONTROL);
|
|
4806
|
-
headers.set("Content-Type", "application/json");
|
|
4807
|
-
return new Response(JSON.stringify(body), {
|
|
4808
|
-
status: 200,
|
|
4809
|
-
headers
|
|
4810
|
-
});
|
|
4811
|
-
}
|
|
4812
|
-
/**
|
|
4813
|
-
* Provides an exportable `/.well-known/oauth-authorization-server`.
|
|
4814
|
-
*
|
|
4815
|
-
* Useful when basePath prevents the endpoint from being located at the root
|
|
4816
|
-
* and must be provided manually.
|
|
4817
|
-
*
|
|
4818
|
-
* @external
|
|
4819
|
-
*/
|
|
4820
|
-
const oauthProviderAuthServerMetadata = (auth, opts) => {
|
|
4821
|
-
return async (request) => {
|
|
4822
|
-
return metadataResponse(await auth.api.getOAuthServerConfig({
|
|
4823
|
-
request,
|
|
4824
|
-
asResponse: false
|
|
4825
|
-
}), opts?.headers);
|
|
4826
|
-
};
|
|
4827
|
-
};
|
|
4828
|
-
/**
|
|
4829
|
-
* Provides an exportable `/.well-known/openid-configuration`.
|
|
4830
|
-
*
|
|
4831
|
-
* Useful when basePath prevents the endpoint from being located at the root
|
|
4832
|
-
* and must be provided manually.
|
|
4833
|
-
*
|
|
4834
|
-
* @external
|
|
4835
|
-
*/
|
|
4836
|
-
const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
|
|
4837
|
-
return async (request) => {
|
|
4838
|
-
return metadataResponse(await auth.api.getOpenIdConfig({
|
|
4839
|
-
request,
|
|
4840
|
-
asResponse: false
|
|
4841
|
-
}), opts?.headers);
|
|
4842
|
-
};
|
|
4843
|
-
};
|
|
4844
|
-
//#endregion
|
|
4845
|
-
export { authServerMetadata, checkOAuthClient, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata };
|
|
4385
|
+
export { DEFAULT_OAUTH_SCOPES, ResourceUriSchema, authServerMetadata, checkOAuthClient, consumeClientAssertion, extendOAuthProvider, getIssuer, getOAuthProviderApi, getOAuthProviderState, metadataResponse, oauthAuthorizationServerMetadata, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata, raiseResourceServerChallenge };
|