@hammadj/better-auth-oauth-provider 1.5.0-beta.9

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/index.mjs ADDED
@@ -0,0 +1,3724 @@
1
+ import { a as getJwtPlugin, c as parseClientMetadata, d as storeToken, f as validateClientCredentials, i as getClient, l as parsePrompt, m as mcpHandler, n as decryptStoredClientSecret, r as deleteFromPrompt, s as getStoredToken, t as basicToClientCredentials, u as storeClientSecret } from "./utils-BSruxDcm.mjs";
2
+ import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
3
+ import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
4
+ import { APIError as APIError$1 } from "better-call";
5
+ import { BetterAuthError } from "@better-auth/core/error";
6
+ import { constantTimeEqual, generateRandomString, makeSignature } from "better-auth/crypto";
7
+ import { defineRequestState } from "@better-auth/core/context";
8
+ import { logger } from "@better-auth/core/env";
9
+ import { parseSetCookieHeader } from "better-auth/cookies";
10
+ import { mergeSchema } from "better-auth/db";
11
+ import * as z from "zod";
12
+ import { signJWT, toExpJWT } from "better-auth/plugins";
13
+ import { SignJWT, compactVerify, createLocalJWKSet, decodeJwt } from "jose";
14
+
15
+ //#region src/metadata.ts
16
+ function authServerMetadata(ctx, opts, overrides) {
17
+ const baseURL = ctx.context.baseURL;
18
+ return {
19
+ scopes_supported: overrides?.scopes_supported,
20
+ issuer: opts?.jwt?.issuer ?? baseURL,
21
+ authorization_endpoint: `${baseURL}/oauth2/authorize`,
22
+ token_endpoint: `${baseURL}/oauth2/token`,
23
+ jwks_uri: overrides?.jwt_disabled ? void 0 : opts?.jwks?.remoteUrl ?? `${baseURL}${opts?.jwks?.jwksPath ?? "/jwks"}`,
24
+ registration_endpoint: `${baseURL}/oauth2/register`,
25
+ introspection_endpoint: `${baseURL}/oauth2/introspect`,
26
+ revocation_endpoint: `${baseURL}/oauth2/revoke`,
27
+ response_types_supported: overrides?.grant_types_supported && !overrides.grant_types_supported.includes("authorization_code") ? [] : ["code"],
28
+ response_modes_supported: ["query"],
29
+ grant_types_supported: overrides?.grant_types_supported ?? [
30
+ "authorization_code",
31
+ "client_credentials",
32
+ "refresh_token"
33
+ ],
34
+ token_endpoint_auth_methods_supported: [
35
+ ...overrides?.public_client_supported ? ["none"] : [],
36
+ "client_secret_basic",
37
+ "client_secret_post"
38
+ ],
39
+ introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
40
+ revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
41
+ code_challenge_methods_supported: ["S256"]
42
+ };
43
+ }
44
+ function oidcServerMetadata(ctx, opts) {
45
+ const baseURL = ctx.context.baseURL;
46
+ const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
47
+ return {
48
+ ...authServerMetadata(ctx, jwtPluginOptions, {
49
+ scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
50
+ public_client_supported: opts.allowUnauthenticatedClientRegistration,
51
+ grant_types_supported: opts.grantTypes,
52
+ jwt_disabled: opts.disableJwtPlugin
53
+ }),
54
+ claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
55
+ userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
56
+ subject_types_supported: ["public"],
57
+ id_token_signing_alg_values_supported: jwtPluginOptions?.jwks?.keyPairConfig?.alg ? [jwtPluginOptions?.jwks?.keyPairConfig?.alg] : opts.disableJwtPlugin ? ["HS256"] : ["EdDSA"],
58
+ end_session_endpoint: `${baseURL}/oauth2/end-session`,
59
+ acr_values_supported: ["urn:mace:incommon:iap:bronze"],
60
+ prompt_values_supported: [
61
+ "login",
62
+ "consent",
63
+ "create",
64
+ "select_account"
65
+ ]
66
+ };
67
+ }
68
+ /**
69
+ * Provides an exportable `/.well-known/oauth-authorization-server`.
70
+ *
71
+ * Useful when basePath prevents the endpoint from being located at the root
72
+ * and must be provided manually.
73
+ *
74
+ * @external
75
+ */
76
+ const oauthProviderAuthServerMetadata = (auth, opts) => {
77
+ return async (_request) => {
78
+ const res = await auth.api.getOAuthServerConfig();
79
+ return new Response(JSON.stringify(res), {
80
+ status: 200,
81
+ headers: {
82
+ "Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
83
+ ...opts?.headers,
84
+ "Content-Type": "application/json"
85
+ }
86
+ });
87
+ };
88
+ };
89
+ /**
90
+ * Provides an exportable `/.well-known/openid-configuration`.
91
+ *
92
+ * Useful when basePath prevents the endpoint from being located at the root
93
+ * and must be provided manually.
94
+ *
95
+ * @external
96
+ */
97
+ const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
98
+ return async (_request) => {
99
+ const res = await auth.api.getOpenIdConfig();
100
+ return new Response(JSON.stringify(res), {
101
+ status: 200,
102
+ headers: {
103
+ "Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
104
+ ...opts?.headers,
105
+ "Content-Type": "application/json"
106
+ }
107
+ });
108
+ };
109
+ };
110
+
111
+ //#endregion
112
+ //#region src/authorize.ts
113
+ /**
114
+ * Formats an error url
115
+ */
116
+ function formatErrorURL(url, error, description, state) {
117
+ const searchParams = new URLSearchParams({
118
+ error,
119
+ error_description: description
120
+ });
121
+ state && searchParams.append("state", state);
122
+ return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
123
+ }
124
+ const handleRedirect = (ctx, uri) => {
125
+ if (ctx.headers?.get("accept")?.includes("application/json")) return {
126
+ redirect: true,
127
+ url: uri.toString()
128
+ };
129
+ else throw ctx.redirect(uri);
130
+ };
131
+ /**
132
+ * Error page url if redirect_uri has not been verified yet
133
+ * Generates Url for custom error page
134
+ */
135
+ function getErrorURL(ctx, error, description) {
136
+ return formatErrorURL(ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`, error, description);
137
+ }
138
+ async function authorizeEndpoint(ctx, opts, settings) {
139
+ if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError$1("NOT_FOUND");
140
+ if (!ctx.request) throw new APIError$1("UNAUTHORIZED", {
141
+ error_description: "request not found",
142
+ error: "invalid_request"
143
+ });
144
+ const query = ctx.query;
145
+ if (!query.client_id) throw ctx.redirect(getErrorURL(ctx, "invalid_client", "client_id is required"));
146
+ if (!query.response_type) throw ctx.redirect(getErrorURL(ctx, "invalid_request", "response_type is required"));
147
+ const promptSet = ctx.query?.prompt ? parsePrompt(ctx.query?.prompt) : void 0;
148
+ if (promptSet?.has("select_account") && !opts.selectAccount?.page) throw ctx.redirect(getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
149
+ if (!(query.response_type === "code")) throw ctx.redirect(getErrorURL(ctx, "unsupported_response_type", "unsupported response type"));
150
+ const client = await getClient(ctx, opts, query.client_id);
151
+ if (!client) throw ctx.redirect(getErrorURL(ctx, "invalid_client", "client_id is required"));
152
+ if (client.disabled) throw ctx.redirect(getErrorURL(ctx, "client_disabled", "client is disabled"));
153
+ if (!client.redirectUris?.find((url) => url === query.redirect_uri) || !query.redirect_uri) throw ctx.redirect(getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
154
+ let requestedScopes = query.scope?.split(" ").filter((s) => s);
155
+ if (requestedScopes) {
156
+ const validScopes = new Set(client.scopes ?? opts.scopes);
157
+ const invalidScopes = requestedScopes.filter((scope) => {
158
+ return !validScopes?.has(scope) || scope === "offline_access" && (query.code_challenge_method !== "S256" || !query.code_challenge);
159
+ });
160
+ if (invalidScopes.length) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_scope", `The following scopes are invalid: ${invalidScopes.join(", ")}`, query.state));
161
+ }
162
+ if (!requestedScopes) {
163
+ requestedScopes = client.scopes ?? opts.scopes ?? [];
164
+ query.scope = requestedScopes.join(" ");
165
+ }
166
+ if (!query.code_challenge || !query.code_challenge_method) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", "pkce is required", query.state));
167
+ if (!["S256"].includes(query.code_challenge_method)) throw ctx.redirect(formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method", query.state));
168
+ const session = await getSessionFromCtx(ctx);
169
+ if (!session || promptSet?.has("login") || promptSet?.has("create")) return redirectWithPromptCode(ctx, opts, promptSet?.has("create") ? "create" : "login");
170
+ if (settings?.isAuthorize && promptSet?.has("select_account")) return redirectWithPromptCode(ctx, opts, "select_account");
171
+ if (settings?.isAuthorize && opts.selectAccount) {
172
+ if (await opts.selectAccount.shouldRedirect({
173
+ headers: ctx.request.headers,
174
+ user: session.user,
175
+ session: session.session,
176
+ scopes: requestedScopes
177
+ })) return redirectWithPromptCode(ctx, opts, "select_account");
178
+ }
179
+ if (opts.signup?.shouldRedirect) {
180
+ const signupRedirect = await opts.signup.shouldRedirect({
181
+ headers: ctx.request.headers,
182
+ user: session.user,
183
+ session: session.session,
184
+ scopes: requestedScopes
185
+ });
186
+ if (signupRedirect) return redirectWithPromptCode(ctx, opts, "create", typeof signupRedirect === "string" ? signupRedirect : void 0);
187
+ }
188
+ if (!settings?.postLogin && opts.postLogin) {
189
+ if (await opts.postLogin.shouldRedirect({
190
+ headers: ctx.request.headers,
191
+ user: session.user,
192
+ session: session.session,
193
+ scopes: requestedScopes
194
+ })) return redirectWithPromptCode(ctx, opts, "post_login");
195
+ }
196
+ if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent");
197
+ const referenceId = await opts.postLogin?.consentReferenceId?.({
198
+ user: session.user,
199
+ session: session.session,
200
+ scopes: requestedScopes
201
+ });
202
+ if (client.skipConsent) return redirectWithAuthorizationCode(ctx, opts, {
203
+ query,
204
+ clientId: client.clientId,
205
+ userId: session.user.id,
206
+ sessionId: session.session.id,
207
+ referenceId
208
+ });
209
+ const consent = await ctx.context.adapter.findOne({
210
+ model: "oauthConsent",
211
+ where: [
212
+ {
213
+ field: "clientId",
214
+ value: client.clientId
215
+ },
216
+ {
217
+ field: "userId",
218
+ value: session.user.id
219
+ },
220
+ ...referenceId ? [{
221
+ field: "referenceId",
222
+ value: referenceId
223
+ }] : []
224
+ ]
225
+ });
226
+ if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val))) return redirectWithPromptCode(ctx, opts, "consent");
227
+ return redirectWithAuthorizationCode(ctx, opts, {
228
+ query,
229
+ clientId: client.clientId,
230
+ userId: session.user.id,
231
+ sessionId: session.session.id,
232
+ referenceId
233
+ });
234
+ }
235
+ async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
236
+ const code = generateRandomString(32, "a-z", "A-Z", "0-9");
237
+ const iat = Math.floor(Date.now() / 1e3);
238
+ const exp = iat + (opts.codeExpiresIn ?? 600);
239
+ const data = {
240
+ identifier: await storeToken(opts.storeTokens, code, "authorization_code"),
241
+ updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
242
+ expiresAt: /* @__PURE__ */ new Date(exp * 1e3),
243
+ value: JSON.stringify({
244
+ type: "authorization_code",
245
+ query: ctx.query,
246
+ userId: verificationValue.userId,
247
+ sessionId: verificationValue?.sessionId,
248
+ referenceId: verificationValue.referenceId
249
+ })
250
+ };
251
+ ctx.context.verification_id ? await ctx.context.internalAdapter.updateVerificationValue(ctx.context.verification_id, data) : await ctx.context.internalAdapter.createVerificationValue({
252
+ ...data,
253
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3)
254
+ });
255
+ const redirectUriWithCode = new URL(verificationValue.query.redirect_uri);
256
+ redirectUriWithCode.searchParams.set("code", code);
257
+ if (verificationValue.query.state) redirectUriWithCode.searchParams.set("state", verificationValue.query.state);
258
+ return handleRedirect(ctx, redirectUriWithCode.toString());
259
+ }
260
+ async function redirectWithPromptCode(ctx, opts, type, page) {
261
+ const queryParams = await signParams(ctx, opts);
262
+ let path = opts.loginPage;
263
+ if (type === "select_account") path = opts.selectAccount?.page ?? opts.loginPage;
264
+ else if (type === "post_login") {
265
+ if (!opts.postLogin?.page) throw new APIError$1("INTERNAL_SERVER_ERROR", { error_description: "postLogin should have been defined" });
266
+ path = opts.postLogin?.page;
267
+ } else if (type === "consent") path = opts.consentPage;
268
+ else if (type === "create") path = opts.signup?.page ?? opts.loginPage;
269
+ return handleRedirect(ctx, `${page ?? path}?${queryParams}`);
270
+ }
271
+ async function signParams(ctx, opts) {
272
+ const exp = Math.floor(Date.now() / 1e3) + (opts.codeExpiresIn ?? 600);
273
+ const params = new URLSearchParams(ctx.query);
274
+ params.set("exp", String(exp));
275
+ const signature = await makeSignature(params.toString(), ctx.context.secret);
276
+ params.append("sig", signature);
277
+ return params.toString();
278
+ }
279
+
280
+ //#endregion
281
+ //#region src/consent.ts
282
+ async function consentEndpoint(ctx, opts) {
283
+ const _query = (await oAuthState.get())?.query;
284
+ if (!_query) throw new APIError("BAD_REQUEST", {
285
+ error_description: "missing oauth query",
286
+ error: "invalid_request"
287
+ });
288
+ const query = new URLSearchParams(_query);
289
+ const originalRequestedScopes = query.get("scope")?.split(" ") ?? [];
290
+ const clientId = query.get("client_id");
291
+ if (!clientId) throw new APIError("BAD_REQUEST", {
292
+ error_description: "client_id is required",
293
+ error: "invalid_client"
294
+ });
295
+ const requestedScopes = ctx.body.scope?.split(" ");
296
+ if (requestedScopes) {
297
+ if (!requestedScopes.every((sc) => originalRequestedScopes?.includes(sc))) throw new APIError("BAD_REQUEST", {
298
+ error_description: "Scope not originally requested",
299
+ error: "invalid_request"
300
+ });
301
+ }
302
+ if (!(ctx.body.accept === true)) return {
303
+ redirect: true,
304
+ uri: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0)
305
+ };
306
+ const session = await getSessionFromCtx(ctx);
307
+ const referenceId = await opts.postLogin?.consentReferenceId?.({
308
+ user: session?.user,
309
+ session: session?.session,
310
+ scopes: requestedScopes ?? originalRequestedScopes
311
+ });
312
+ const foundConsent = await ctx.context.adapter.findOne({
313
+ model: "oauthConsent",
314
+ where: [
315
+ {
316
+ field: "clientId",
317
+ value: clientId
318
+ },
319
+ {
320
+ field: "userId",
321
+ value: session?.user.id
322
+ },
323
+ ...referenceId ? [{
324
+ field: "referenceId",
325
+ value: referenceId
326
+ }] : []
327
+ ]
328
+ });
329
+ const iat = Math.floor(Date.now() / 1e3);
330
+ const consent = {
331
+ clientId,
332
+ userId: session?.user.id,
333
+ scopes: requestedScopes ?? originalRequestedScopes,
334
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3),
335
+ updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
336
+ referenceId
337
+ };
338
+ foundConsent?.id ? await ctx.context.adapter.update({
339
+ model: "oauthConsent",
340
+ where: [{
341
+ field: "id",
342
+ value: foundConsent.id
343
+ }],
344
+ update: {
345
+ scopes: consent.scopes,
346
+ updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
347
+ }
348
+ }) : await ctx.context.adapter.create({
349
+ model: "oauthConsent",
350
+ data: {
351
+ ...consent,
352
+ scopes: consent.scopes
353
+ }
354
+ });
355
+ ctx?.headers?.set("accept", "application/json");
356
+ ctx.query = deleteFromPrompt(query, "consent");
357
+ ctx.context.postLogin = true;
358
+ const { url } = await authorizeEndpoint(ctx, opts);
359
+ return {
360
+ redirect: true,
361
+ uri: url
362
+ };
363
+ }
364
+
365
+ //#endregion
366
+ //#region src/continue.ts
367
+ async function continueEndpoint(ctx, opts) {
368
+ if (ctx.body.selected === true) return await selected(ctx, opts);
369
+ else if (ctx.body.created === true) return await created(ctx, opts);
370
+ else if (ctx.body.postLogin === true) return await postLogin(ctx, opts);
371
+ else throw new APIError("BAD_REQUEST", {
372
+ error_description: "Missing parameters",
373
+ error: "invalid_request"
374
+ });
375
+ }
376
+ async function selected(ctx, opts) {
377
+ const _query = (await oAuthState.get())?.query;
378
+ if (!_query) throw new APIError("BAD_REQUEST", {
379
+ error_description: "missing oauth query",
380
+ error: "invalid_request"
381
+ });
382
+ ctx.headers?.set("accept", "application/json");
383
+ ctx.query = deleteFromPrompt(new URLSearchParams(_query), "select_account");
384
+ const { url } = await authorizeEndpoint(ctx, opts);
385
+ return {
386
+ redirect: true,
387
+ uri: url
388
+ };
389
+ }
390
+ async function created(ctx, opts) {
391
+ const _query = (await oAuthState.get())?.query;
392
+ if (!_query) throw new APIError("BAD_REQUEST", {
393
+ error_description: "missing oauth query",
394
+ error: "invalid_request"
395
+ });
396
+ ctx.query = deleteFromPrompt(new URLSearchParams(_query), "create");
397
+ const { url } = await authorizeEndpoint(ctx, opts);
398
+ return {
399
+ redirect: true,
400
+ uri: url
401
+ };
402
+ }
403
+ async function postLogin(ctx, opts) {
404
+ const _query = (await oAuthState.get())?.query;
405
+ if (!_query) throw new APIError("BAD_REQUEST", {
406
+ error_description: "missing oauth query",
407
+ error: "invalid_request"
408
+ });
409
+ const query = new URLSearchParams(_query);
410
+ ctx.headers?.set("accept", "application/json");
411
+ ctx.query = Object.fromEntries(query);
412
+ const { url } = await authorizeEndpoint(ctx, opts, { postLogin: true });
413
+ return {
414
+ redirect: true,
415
+ uri: url
416
+ };
417
+ }
418
+
419
+ //#endregion
420
+ //#region src/userinfo.ts
421
+ /**
422
+ * Provides shared /userinfo and id_token claims functionality
423
+ *
424
+ * @see https://openid.net/specs/openid-connect-core-1_0.html#NormalClaims
425
+ */
426
+ function userNormalClaims(user, scopes) {
427
+ const name = user.name.split(" ").filter((v) => v !== "");
428
+ const profile = {
429
+ name: user.name ?? void 0,
430
+ picture: user.image ?? void 0,
431
+ given_name: name.length > 1 ? name.slice(0, -1).join(" ") : void 0,
432
+ family_name: name.length > 1 ? name.at(-1) : void 0
433
+ };
434
+ const email = {
435
+ email: user.email ?? void 0,
436
+ email_verified: user.emailVerified ?? false
437
+ };
438
+ return {
439
+ sub: user.id ?? void 0,
440
+ ...scopes.includes("profile") ? profile : {},
441
+ ...scopes.includes("email") ? email : {}
442
+ };
443
+ }
444
+ /**
445
+ * Handles the /oauth2/userinfo endpoint
446
+ */
447
+ async function userInfoEndpoint(ctx, opts) {
448
+ if (!ctx.request) throw new APIError("UNAUTHORIZED", {
449
+ error_description: "request not found",
450
+ error: "invalid_request"
451
+ });
452
+ const authorization = ctx.request.headers.get("authorization");
453
+ const token = typeof authorization === "string" && authorization?.startsWith("Bearer ") ? authorization?.replace("Bearer ", "") : authorization;
454
+ if (!token?.length) throw new APIError("UNAUTHORIZED", {
455
+ error_description: "authorization header not found",
456
+ error: "invalid_request"
457
+ });
458
+ const jwt = await validateAccessToken(ctx, opts, token);
459
+ const scopes = jwt.scope?.split(" ");
460
+ if (!scopes?.includes("openid")) throw new APIError("BAD_REQUEST", {
461
+ error_description: "Missing required scope",
462
+ error: "invalid_scope"
463
+ });
464
+ if (!jwt.sub) throw new APIError("BAD_REQUEST", {
465
+ error_description: "user not found",
466
+ error: "invalid_request"
467
+ });
468
+ const user = await ctx.context.internalAdapter.findUserById(jwt.sub);
469
+ if (!user) throw new APIError("BAD_REQUEST", {
470
+ error_description: "user not found",
471
+ error: "invalid_request"
472
+ });
473
+ const baseUserClaims = userNormalClaims(user, scopes ?? []);
474
+ const additionalInfoUserClaims = opts.customUserInfoClaims && scopes?.length ? await opts.customUserInfoClaims({
475
+ user,
476
+ scopes,
477
+ jwt
478
+ }) : {};
479
+ return {
480
+ ...baseUserClaims,
481
+ ...additionalInfoUserClaims
482
+ };
483
+ }
484
+
485
+ //#endregion
486
+ //#region src/token.ts
487
+ /**
488
+ * Handles the /oauth2/token endpoint by delegating
489
+ * the grant types
490
+ */
491
+ async function tokenEndpoint(ctx, opts) {
492
+ const grantType = ctx.body?.grant_type;
493
+ if (opts.grantTypes && grantType && !opts.grantTypes.includes(grantType)) throw new APIError("BAD_REQUEST", {
494
+ error_description: `unsupported grant_type ${grantType}`,
495
+ error: "unsupported_grant_type"
496
+ });
497
+ switch (grantType) {
498
+ case "authorization_code": return handleAuthorizationCodeGrant(ctx, opts);
499
+ case "client_credentials": return handleClientCredentialsGrant(ctx, opts);
500
+ case "refresh_token": return handleRefreshTokenGrant(ctx, opts);
501
+ case void 0: throw new APIError("BAD_REQUEST", {
502
+ error_description: "missing required grant_type",
503
+ error: "unsupported_grant_type"
504
+ });
505
+ default: throw new APIError("BAD_REQUEST", {
506
+ error_description: `unsupported grant_type ${grantType}`,
507
+ error: "unsupported_grant_type"
508
+ });
509
+ }
510
+ }
511
+ async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, overrides) {
512
+ const iat = overrides?.iat ?? Math.floor(Date.now() / 1e3);
513
+ const exp = overrides?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
514
+ const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
515
+ user,
516
+ scopes,
517
+ resource: ctx.body.resource,
518
+ referenceId,
519
+ metadata: parseClientMetadata(client.metadata)
520
+ }) : {};
521
+ const jwtPluginOptions = getJwtPlugin(ctx.context).options;
522
+ return signJWT(ctx, {
523
+ options: jwtPluginOptions,
524
+ payload: {
525
+ ...customClaims,
526
+ sub: user.id,
527
+ aud: typeof audience === "string" ? audience : audience?.length === 1 ? audience.at(0) : audience,
528
+ azp: client.clientId,
529
+ scope: scopes.join(" "),
530
+ sid: overrides?.sid,
531
+ iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
532
+ iat,
533
+ exp
534
+ }
535
+ });
536
+ }
537
+ /**
538
+ * Creates a user id token in code_authorization with scope of 'openid'
539
+ * and hybrid/implicit (not yet implemented) flows
540
+ */
541
+ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId) {
542
+ const iat = Math.floor(Date.now() / 1e3);
543
+ const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
544
+ const userClaims = userNormalClaims(user, scopes);
545
+ const authTime = Math.floor((ctx.context.session?.session.createdAt ?? /* @__PURE__ */ new Date(iat * 1e3)).getTime() / 1e3);
546
+ const acr = "urn:mace:incommon:iap:bronze";
547
+ const customClaims = opts.customIdTokenClaims ? await opts.customIdTokenClaims({
548
+ user,
549
+ scopes,
550
+ metadata: parseClientMetadata(client.metadata)
551
+ }) : {};
552
+ const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
553
+ const payload = {
554
+ ...customClaims,
555
+ ...userClaims,
556
+ auth_time: authTime,
557
+ acr,
558
+ iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
559
+ sub: user.id,
560
+ aud: client.clientId,
561
+ nonce,
562
+ iat,
563
+ exp,
564
+ sid: client.enableEndSession ? sessionId : void 0
565
+ };
566
+ if (opts.disableJwtPlugin && !client.clientSecret) return;
567
+ return opts.disableJwtPlugin ? new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(new TextEncoder().encode(await decryptStoredClientSecret(ctx, opts.storeClientSecret, client.clientSecret))) : signJWT(ctx, {
568
+ options: jwtPluginOptions,
569
+ payload
570
+ });
571
+ }
572
+ /**
573
+ * Encodes a refresh token for a client
574
+ */
575
+ async function encodeRefreshToken(opts, token, sessionId) {
576
+ return (opts.prefix?.refreshToken ?? "") + (opts.formatRefreshToken?.encrypt ? opts.formatRefreshToken.encrypt(token, sessionId) : token);
577
+ }
578
+ /**
579
+ * Decodes a refresh token for a client
580
+ *
581
+ * @internal
582
+ */
583
+ async function decodeRefreshToken(opts, token) {
584
+ if (opts.prefix?.refreshToken) if (token.startsWith(opts.prefix.refreshToken)) token = token.replace(opts.prefix.refreshToken, "");
585
+ else throw new APIError("BAD_REQUEST", {
586
+ error_description: "refresh token not found",
587
+ error: "invalid_token"
588
+ });
589
+ return opts.formatRefreshToken?.decrypt ? opts.formatRefreshToken?.decrypt(token) : { token };
590
+ }
591
+ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, referenceId, refreshId) {
592
+ const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
593
+ const exp = payload?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
594
+ const token = opts.generateOpaqueAccessToken ? await opts.generateOpaqueAccessToken() : generateRandomString(32, "A-Z", "a-z");
595
+ await ctx.context.adapter.create({
596
+ model: "oauthAccessToken",
597
+ data: {
598
+ token: await storeToken(opts.storeTokens, token, "access_token"),
599
+ clientId: client.clientId,
600
+ sessionId: payload?.sid,
601
+ userId: user?.id,
602
+ referenceId,
603
+ refreshId,
604
+ scopes,
605
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3),
606
+ expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
607
+ }
608
+ });
609
+ return (opts.prefix?.opaqueAccessToken ?? "") + token;
610
+ }
611
+ async function createRefreshToken(ctx, opts, user, referenceId, client, scopes, payload, originalRefresh) {
612
+ const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
613
+ const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
614
+ const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
615
+ const sessionId = payload?.sid;
616
+ if (originalRefresh?.id) await ctx.context.adapter.update({
617
+ model: "oauthRefreshToken",
618
+ where: [{
619
+ field: "id",
620
+ value: originalRefresh.id
621
+ }],
622
+ update: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
623
+ });
624
+ return {
625
+ id: (await ctx.context.adapter.create({
626
+ model: "oauthRefreshToken",
627
+ data: {
628
+ token: await storeToken(opts.storeTokens, token, "refresh_token"),
629
+ clientId: client.clientId,
630
+ sessionId,
631
+ userId: user.id,
632
+ referenceId,
633
+ scopes,
634
+ createdAt: /* @__PURE__ */ new Date(iat * 1e3),
635
+ expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
636
+ }
637
+ })).id,
638
+ token: await encodeRefreshToken(opts, token, sessionId)
639
+ };
640
+ }
641
+ /**
642
+ * Checks the resource parameter, if provided,
643
+ * and returns a valid audience based on the request
644
+ */
645
+ async function checkResource(ctx, opts, scopes) {
646
+ const resource = ctx.body.resource;
647
+ const audience = typeof resource === "string" ? [resource] : resource ? [...resource] : void 0;
648
+ if (audience) {
649
+ if (scopes.includes("openid")) audience.push(`${ctx.context.baseURL}/oauth2/userinfo`);
650
+ const validAudiences = new Set([...opts.validAudiences ?? [ctx.context.baseURL], scopes?.includes("openid") ? `${ctx.context.baseURL}/oauth2/userinfo` : void 0].flat().filter((v) => v?.length));
651
+ for (const aud of audience) if (!validAudiences.has(aud)) throw new APIError("BAD_REQUEST", {
652
+ error_description: "requested resource invalid",
653
+ error: "invalid_request"
654
+ });
655
+ }
656
+ return audience?.length === 1 ? audience.at(0) : audience;
657
+ }
658
+ async function createUserTokens(ctx, opts, client, scopes, user, referenceId, sessionId, nonce, additional) {
659
+ const iat = Math.floor(Date.now() / 1e3);
660
+ const defaultExp = iat + (opts.accessTokenExpiresIn ?? 3600);
661
+ const exp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
662
+ return prev < curr ? prev : curr;
663
+ }, defaultExp) : defaultExp;
664
+ const audience = await checkResource(ctx, opts, scopes);
665
+ const isRefreshToken = additional?.refreshToken?.scopes?.includes("offline_access") || scopes.includes("offline_access");
666
+ const isJwtAccessToken = audience && !opts.disableJwtPlugin;
667
+ const isIdToken = scopes.includes("openid");
668
+ const earlyRefreshToken = isRefreshToken && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
669
+ iat,
670
+ exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
671
+ sid: sessionId
672
+ }, additional?.refreshToken) : void 0;
673
+ const [accessToken, refreshToken, idToken] = await Promise.all([
674
+ isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, {
675
+ iat,
676
+ exp,
677
+ sid: sessionId
678
+ }) : createOpaqueAccessToken(ctx, opts, user, client, scopes, {
679
+ iat,
680
+ exp,
681
+ sid: sessionId
682
+ }, referenceId, earlyRefreshToken?.id),
683
+ earlyRefreshToken ? earlyRefreshToken : isRefreshToken ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
684
+ iat,
685
+ exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
686
+ sid: sessionId
687
+ }, additional?.refreshToken) : void 0,
688
+ isIdToken ? createIdToken(ctx, opts, user, client, scopes, nonce, sessionId) : void 0
689
+ ]);
690
+ return ctx.json({
691
+ access_token: accessToken,
692
+ expires_in: exp - iat,
693
+ expires_at: exp,
694
+ token_type: "Bearer",
695
+ refresh_token: refreshToken?.token,
696
+ scope: scopes.join(" "),
697
+ id_token: idToken
698
+ }, { headers: {
699
+ "Cache-Control": "no-store",
700
+ Pragma: "no-cache"
701
+ } });
702
+ }
703
+ /** Checks verification value */
704
+ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri) {
705
+ const verification = await ctx.context.internalAdapter.findVerificationValue(await storeToken(opts.storeTokens, code, "authorization_code"));
706
+ const verificationValue = verification ? JSON.parse(verification?.value) : void 0;
707
+ if (!verification) throw new APIError("UNAUTHORIZED", {
708
+ error_description: "Invalid code",
709
+ error: "invalid_verification"
710
+ });
711
+ if (verification?.id) await ctx.context.internalAdapter.deleteVerificationValue(verification.id);
712
+ if (!verification.expiresAt || verification.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("UNAUTHORIZED", {
713
+ error_description: "code expired",
714
+ error: "invalid_verification"
715
+ });
716
+ if (!verificationValue) throw new APIError("UNAUTHORIZED", {
717
+ error_description: "missing verification value content",
718
+ error: "invalid_verification"
719
+ });
720
+ if (verificationValue.type !== "authorization_code") throw new APIError("UNAUTHORIZED", {
721
+ error_description: "incorrect verification type",
722
+ error: "invalid_verification"
723
+ });
724
+ if (verificationValue.query.client_id !== client_id) throw new APIError("UNAUTHORIZED", {
725
+ error_description: "invalid client_id",
726
+ error: "invalid_client"
727
+ });
728
+ if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
729
+ error_description: "missing user_id on challenge",
730
+ error: "invalid_user"
731
+ });
732
+ if (verificationValue.query?.redirect_uri && verificationValue.query?.redirect_uri !== redirect_uri) throw new APIError("BAD_REQUEST", {
733
+ error_description: "missing verification redirect_uri",
734
+ error: "invalid_request"
735
+ });
736
+ return verificationValue;
737
+ }
738
+ /**
739
+ * Obtains new Session Jwt and Refresh Tokens using a code
740
+ */
741
+ async function handleAuthorizationCodeGrant(ctx, opts) {
742
+ let { client_id, client_secret, code, code_verifier, redirect_uri } = ctx.body;
743
+ const authorization = ctx.request?.headers.get("authorization") || null;
744
+ if (authorization?.startsWith("Basic ")) {
745
+ const res = basicToClientCredentials(authorization);
746
+ client_id = res?.client_id;
747
+ client_secret = res?.client_secret;
748
+ }
749
+ if (!client_id) throw new APIError("BAD_REQUEST", {
750
+ error_description: "client_id is required",
751
+ error: "invalid_request"
752
+ });
753
+ if (!code) throw new APIError("BAD_REQUEST", {
754
+ error_description: "code is required",
755
+ error: "invalid_request"
756
+ });
757
+ if (!redirect_uri) throw new APIError("BAD_REQUEST", {
758
+ error_description: "redirect_uri is required",
759
+ error: "invalid_request"
760
+ });
761
+ const isAuthCodeWithSecret = client_id && client_secret;
762
+ const isAuthCodeWithPkce = client_id && code && code_verifier;
763
+ if (!(isAuthCodeWithPkce || isAuthCodeWithSecret)) throw new APIError("BAD_REQUEST", {
764
+ error_description: "Missing a required credential value for authorization_code grant",
765
+ error: "invalid_request"
766
+ });
767
+ /** Get and check Verification Value */
768
+ const verificationValue = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri);
769
+ const scopes = verificationValue.query.scope?.split(" ");
770
+ if (!scopes) throw new APIError("INTERNAL_SERVER_ERROR", {
771
+ error_description: "verification scope unset",
772
+ error: "invalid_scope"
773
+ });
774
+ /** Verify Client */
775
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes);
776
+ /** Check challenge */
777
+ const challenge = code_verifier && verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0;
778
+ if (isAuthCodeWithSecret && (challenge || verificationValue?.query?.code_challenge) && challenge !== verificationValue.query?.code_challenge) throw new APIError("UNAUTHORIZED", {
779
+ error_description: "code verification failed",
780
+ error: "invalid_request"
781
+ });
782
+ if (isAuthCodeWithPkce && challenge !== verificationValue.query?.code_challenge) throw new APIError("UNAUTHORIZED", {
783
+ error_description: "code verification failed",
784
+ error: "invalid_request"
785
+ });
786
+ /** Get user */
787
+ if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
788
+ error_description: "missing user, user may have been deleted",
789
+ error: "invalid_user"
790
+ });
791
+ const user = await ctx.context.internalAdapter.findUserById(verificationValue.userId);
792
+ if (!user) throw new APIError("BAD_REQUEST", {
793
+ error_description: "missing user, user may have been deleted",
794
+ error: "invalid_user"
795
+ });
796
+ const session = await ctx.context.adapter.findOne({
797
+ model: "session",
798
+ where: [{
799
+ field: "id",
800
+ value: verificationValue.sessionId
801
+ }]
802
+ });
803
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
804
+ error_description: "session no longer exists",
805
+ error: "invalid_request"
806
+ });
807
+ return createUserTokens(ctx, opts, client, verificationValue.query.scope?.split(" ") ?? [], user, verificationValue.referenceId, session.id, verificationValue.query?.nonce);
808
+ }
809
+ /**
810
+ * Grant that allows direct access to an API using the application's credentials
811
+ * This grant is for M2M so the concept of a user id does not exist on the token.
812
+ *
813
+ * MUST follow https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
814
+ */
815
+ async function handleClientCredentialsGrant(ctx, opts) {
816
+ let { client_id, client_secret, scope } = ctx.body;
817
+ const authorization = ctx.request?.headers.get("authorization") || null;
818
+ if (authorization?.startsWith("Basic ")) {
819
+ const res = basicToClientCredentials(authorization);
820
+ client_id = res?.client_id;
821
+ client_secret = res?.client_secret;
822
+ }
823
+ if (!client_id) throw new APIError("BAD_REQUEST", {
824
+ error_description: "Missing required client_id",
825
+ error: "invalid_grant"
826
+ });
827
+ if (!client_secret) throw new APIError("BAD_REQUEST", {
828
+ error_description: "Missing a required client_secret",
829
+ error: "invalid_grant"
830
+ });
831
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
832
+ let requestedScopes = scope?.split(" ");
833
+ if (requestedScopes) {
834
+ const validScopes = new Set(client.scopes ?? opts.scopes);
835
+ const oidcScopes = new Set([
836
+ "openid",
837
+ "profile",
838
+ "email",
839
+ "offline_access"
840
+ ]);
841
+ const invalidScopes = requestedScopes.filter((scope) => {
842
+ return !validScopes?.has(scope) || oidcScopes.has(scope);
843
+ });
844
+ if (invalidScopes.length) throw new APIError("BAD_REQUEST", {
845
+ error_description: `The following scopes are invalid: ${invalidScopes.join(", ")}`,
846
+ error: "invalid_scope"
847
+ });
848
+ }
849
+ if (!requestedScopes) requestedScopes = client.scopes ?? opts.clientCredentialGrantDefaultScopes ?? opts.scopes ?? [];
850
+ const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
851
+ const audience = await checkResource(ctx, opts, requestedScopes);
852
+ const iat = Math.floor(Date.now() / 1e3);
853
+ const defaultExp = iat + (opts.m2mAccessTokenExpiresIn ?? 3600);
854
+ const exp = opts.scopeExpirations && requestedScopes ? requestedScopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
855
+ return prev < curr ? prev : curr;
856
+ }, defaultExp) : defaultExp;
857
+ const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
858
+ scopes: requestedScopes,
859
+ resource: ctx.body.resource,
860
+ metadata: parseClientMetadata(client.metadata)
861
+ }) : {};
862
+ const accessToken = audience && !opts.disableJwtPlugin ? await signJWT(ctx, {
863
+ options: jwtPluginOptions,
864
+ payload: {
865
+ ...customClaims,
866
+ aud: audience,
867
+ azp: client.clientId,
868
+ scope: requestedScopes.join(" "),
869
+ iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
870
+ iat,
871
+ exp
872
+ }
873
+ }) : await createOpaqueAccessToken(ctx, opts, void 0, client, requestedScopes, {
874
+ iat,
875
+ exp
876
+ });
877
+ return ctx.json({
878
+ access_token: accessToken,
879
+ expires_in: exp - iat,
880
+ expires_at: exp,
881
+ token_type: "Bearer",
882
+ scope: requestedScopes.join(" ")
883
+ }, { headers: {
884
+ "Cache-Control": "no-store",
885
+ Pragma: "no-cache"
886
+ } });
887
+ }
888
+ /**
889
+ * Obtains new Session Jwt and Refresh Tokens using a refresh token
890
+ *
891
+ * Refresh tokens will only allow the same or lesser scopes as the initial authorize request.
892
+ * To add scopes, you must restart the authorize process again.
893
+ */
894
+ async function handleRefreshTokenGrant(ctx, opts) {
895
+ let { client_id, client_secret, refresh_token, scope } = ctx.body;
896
+ const authorization = ctx.request?.headers.get("authorization") || null;
897
+ if (authorization?.startsWith("Basic ")) {
898
+ const res = basicToClientCredentials(authorization);
899
+ client_id = res?.client_id;
900
+ client_secret = res?.client_secret;
901
+ }
902
+ if (!client_id) throw new APIError("BAD_REQUEST", {
903
+ error_description: "Missing required client_id",
904
+ error: "invalid_grant"
905
+ });
906
+ if (!refresh_token) throw new APIError("BAD_REQUEST", {
907
+ error_description: "Missing a required refresh_token for refresh_token grant",
908
+ error: "invalid_grant"
909
+ });
910
+ const decodedRefresh = await decodeRefreshToken(opts, refresh_token);
911
+ const refreshToken = await ctx.context.adapter.findOne({
912
+ model: "oauthRefreshToken",
913
+ where: [{
914
+ field: "token",
915
+ value: await getStoredToken(opts.storeTokens, decodedRefresh.token, "refresh_token")
916
+ }]
917
+ });
918
+ if (!refreshToken) throw new APIError("BAD_REQUEST", {
919
+ error_description: "session not found",
920
+ error: "invalid_request"
921
+ });
922
+ if (refreshToken.clientId !== client_id) throw new APIError("BAD_REQUEST", {
923
+ error_description: "invalid client_id",
924
+ error: "invalid_client"
925
+ });
926
+ if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
927
+ error_description: "invalid refresh token",
928
+ error: "invalid_request"
929
+ });
930
+ if (refreshToken.revoked) {
931
+ await ctx.context.adapter.deleteMany({
932
+ model: "oauthRefreshToken",
933
+ where: [{
934
+ field: "clientId",
935
+ value: client_id
936
+ }, {
937
+ field: "userId",
938
+ value: refreshToken.userId
939
+ }]
940
+ });
941
+ throw new APIError("BAD_REQUEST", {
942
+ error_description: "invalid refresh token",
943
+ error: "invalid_request"
944
+ });
945
+ }
946
+ const scopes = refreshToken?.scopes;
947
+ const requestedScopes = scope?.split(" ");
948
+ if (requestedScopes) {
949
+ const validScopes = new Set(scopes);
950
+ for (const requestedScope of requestedScopes) if (!validScopes.has(requestedScope)) throw new APIError("BAD_REQUEST", {
951
+ error_description: `unable to issue scope ${requestedScope}`,
952
+ error: "invalid_scope"
953
+ });
954
+ }
955
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes);
956
+ const user = await ctx.context.internalAdapter.findUserById(refreshToken.userId);
957
+ if (!user) throw new APIError("BAD_REQUEST", {
958
+ error_description: "user not found",
959
+ error: "invalid_request"
960
+ });
961
+ return createUserTokens(ctx, opts, client, requestedScopes ?? scopes, user, refreshToken.referenceId, refreshToken.sessionId, void 0, { refreshToken });
962
+ }
963
+
964
+ //#endregion
965
+ //#region src/introspect.ts
966
+ /**
967
+ * IMPORTANT NOTES:
968
+ * Introspection follows RFC7662
969
+ * https://datatracker.ietf.org/doc/html/rfc7662
970
+ * - APIError: Continue catches (returnable to client)
971
+ * - Error: Should immediately stop catches (internal error)
972
+ */
973
+ /**
974
+ * Validates a JWT access token against the configured JWKs.
975
+ *
976
+ * @returns RFC7662 introspection format
977
+ */
978
+ async function validateJwtAccessToken(ctx, opts, token, clientId) {
979
+ const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
980
+ const jwtPluginOptions = jwtPlugin?.options;
981
+ let jwtPayload;
982
+ try {
983
+ jwtPayload = await verifyJwsAccessToken(token, {
984
+ jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
985
+ return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
986
+ },
987
+ verifyOptions: {
988
+ audience: opts.validAudiences ?? ctx.context.baseURL,
989
+ issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL
990
+ }
991
+ });
992
+ } catch (error) {
993
+ if (error instanceof Error) {
994
+ if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
995
+ error_description: "invalid JWT signature",
996
+ error: "invalid_request"
997
+ });
998
+ else if (error.name === "JWTExpired") return { active: false };
999
+ else if (error.name === "JWTInvalid") return { active: false };
1000
+ throw error;
1001
+ }
1002
+ throw new Error(error);
1003
+ }
1004
+ let client;
1005
+ if (jwtPayload.azp) {
1006
+ client = await getClient(ctx, opts, jwtPayload.azp);
1007
+ if (!client || client?.disabled) return { active: false };
1008
+ if (clientId && jwtPayload.azp !== clientId) return { active: false };
1009
+ }
1010
+ const sessionId = jwtPayload.sid;
1011
+ if (sessionId) {
1012
+ const session = await ctx.context.adapter.findOne({
1013
+ model: "session",
1014
+ where: [{
1015
+ field: "id",
1016
+ value: sessionId
1017
+ }]
1018
+ });
1019
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) jwtPayload.sid = void 0;
1020
+ }
1021
+ if (jwtPayload.azp) jwtPayload.client_id = jwtPayload.azp;
1022
+ jwtPayload.active = true;
1023
+ return jwtPayload;
1024
+ }
1025
+ /**
1026
+ * Searches for an opaque access token in the database and validates it
1027
+ *
1028
+ * @returns RFC7662 introspection format
1029
+ */
1030
+ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
1031
+ let tokenValue = token;
1032
+ if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
1033
+ else throw new APIError$1("BAD_REQUEST", {
1034
+ error_description: "opaque access token not found",
1035
+ error: "invalid_request"
1036
+ });
1037
+ const accessToken = await ctx.context.adapter.findOne({
1038
+ model: "oauthAccessToken",
1039
+ where: [{
1040
+ field: "token",
1041
+ value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
1042
+ }]
1043
+ });
1044
+ if (!accessToken) throw new APIError$1("BAD_REQUEST", {
1045
+ error_description: "opaque access token not found",
1046
+ error: "invalid_token"
1047
+ });
1048
+ if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
1049
+ let client;
1050
+ if (accessToken.clientId) {
1051
+ client = await getClient(ctx, opts, accessToken.clientId);
1052
+ if (!client || client?.disabled) return { active: false };
1053
+ if (clientId && accessToken.clientId !== clientId) return { active: false };
1054
+ }
1055
+ let sessionId = accessToken.sessionId ?? void 0;
1056
+ if (sessionId) {
1057
+ const session = await ctx.context.adapter.findOne({
1058
+ model: "session",
1059
+ where: [{
1060
+ field: "id",
1061
+ value: sessionId
1062
+ }]
1063
+ });
1064
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) sessionId = void 0;
1065
+ }
1066
+ let user;
1067
+ if (accessToken.userId) user = await ctx.context.internalAdapter.findUserById(accessToken?.userId);
1068
+ const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
1069
+ user,
1070
+ scopes: accessToken.scopes,
1071
+ referenceId: accessToken?.referenceId,
1072
+ metadata: parseClientMetadata(client?.metadata)
1073
+ }) : {};
1074
+ const jwtPluginOptions = (opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options;
1075
+ return {
1076
+ ...customClaims,
1077
+ active: true,
1078
+ iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
1079
+ client_id: accessToken.clientId,
1080
+ sub: user?.id,
1081
+ sid: sessionId,
1082
+ exp: Math.floor(accessToken.expiresAt.getTime() / 1e3),
1083
+ iat: Math.floor(accessToken.createdAt.getTime() / 1e3),
1084
+ scope: accessToken.scopes?.join(" ")
1085
+ };
1086
+ }
1087
+ /**
1088
+ * Validates a refresh token in the session store.
1089
+ *
1090
+ * @returns payload in RFC7662 introspection format
1091
+ */
1092
+ async function validateRefreshToken(ctx, opts, token, clientId) {
1093
+ const refreshToken = await ctx.context.adapter.findOne({
1094
+ model: "oauthRefreshToken",
1095
+ where: [{
1096
+ field: "token",
1097
+ value: await getStoredToken(opts.storeTokens, token, "refresh_token")
1098
+ }]
1099
+ });
1100
+ if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
1101
+ error_description: "token not found",
1102
+ error: "invalid_token"
1103
+ });
1104
+ if (!refreshToken.clientId || refreshToken.clientId !== clientId) return { active: false };
1105
+ if (!refreshToken.expiresAt || refreshToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
1106
+ if (refreshToken.revoked) return { active: false };
1107
+ let sessionId = refreshToken.sessionId ?? void 0;
1108
+ if (sessionId) {
1109
+ const session = await ctx.context.adapter.findOne({
1110
+ model: "session",
1111
+ where: [{
1112
+ field: "id",
1113
+ value: refreshToken.sessionId
1114
+ }]
1115
+ });
1116
+ if (!session || session.expiresAt < /* @__PURE__ */ new Date()) sessionId = void 0;
1117
+ }
1118
+ let user = void 0;
1119
+ if (refreshToken.userId) user = await ctx.context.internalAdapter.findUserById(refreshToken?.userId) ?? void 0;
1120
+ return {
1121
+ active: true,
1122
+ client_id: clientId,
1123
+ iss: ((opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options)?.jwt?.issuer ?? ctx.context.baseURL,
1124
+ sub: user?.id,
1125
+ sid: sessionId,
1126
+ exp: Math.floor(refreshToken.expiresAt.getTime() / 1e3),
1127
+ iat: Math.floor(refreshToken.createdAt.getTime() / 1e3),
1128
+ scope: refreshToken.scopes?.join(" ")
1129
+ };
1130
+ }
1131
+ /**
1132
+ * We don't know the access token format so we try to validate it
1133
+ * as a JWT first, then as an opaque token.
1134
+ *
1135
+ * @returns RFC7662 introspection format
1136
+ *
1137
+ * @internal
1138
+ */
1139
+ async function validateAccessToken(ctx, opts, token, clientId) {
1140
+ try {
1141
+ return await validateJwtAccessToken(ctx, opts, token, clientId);
1142
+ } catch (err) {
1143
+ if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
1144
+ else throw new Error(err);
1145
+ }
1146
+ try {
1147
+ return await validateOpaqueAccessToken(ctx, opts, token, clientId);
1148
+ } catch (err) {
1149
+ if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
1150
+ else throw new Error("Unknown error validating access token");
1151
+ }
1152
+ throw new APIError$1("BAD_REQUEST", {
1153
+ error_description: "Invalid access token",
1154
+ error: "invalid_request"
1155
+ });
1156
+ }
1157
+ async function introspectEndpoint(ctx, opts) {
1158
+ let { client_id, client_secret, token, token_type_hint } = ctx.body;
1159
+ const authorization = ctx.request?.headers.get("authorization") || null;
1160
+ if (authorization?.startsWith("Basic ")) {
1161
+ const res = basicToClientCredentials(authorization);
1162
+ client_id = res?.client_id;
1163
+ client_secret = res?.client_secret;
1164
+ }
1165
+ if (!client_id || !client_secret) throw new APIError$1("UNAUTHORIZED", {
1166
+ error_description: "missing required credentials",
1167
+ error: "invalid_client"
1168
+ });
1169
+ if (token && typeof token === "string" && token.startsWith("Bearer ")) token = token.replace("Bearer ", "");
1170
+ if (!token?.length) throw new APIError$1("BAD_REQUEST", {
1171
+ error_description: "missing a required token for introspection",
1172
+ error: "invalid_request"
1173
+ });
1174
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
1175
+ try {
1176
+ if (token_type_hint === void 0 || token_type_hint === "access_token") try {
1177
+ return await validateAccessToken(ctx, opts, token, client.clientId);
1178
+ } catch (error) {
1179
+ if (error instanceof APIError$1) {
1180
+ if (token_type_hint === "access_token") throw error;
1181
+ } else if (error instanceof Error) throw error;
1182
+ else throw new Error(error);
1183
+ }
1184
+ if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
1185
+ return await validateRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId);
1186
+ } catch (error) {
1187
+ if (error instanceof APIError$1) {
1188
+ if (token_type_hint === "refresh_token") throw error;
1189
+ } else if (error instanceof Error) throw error;
1190
+ else throw new Error(error);
1191
+ }
1192
+ throw new APIError$1("BAD_REQUEST", {
1193
+ error_description: "token not found",
1194
+ error: "invalid_request"
1195
+ });
1196
+ } catch (error) {
1197
+ if (error instanceof APIError$1) {
1198
+ if (error.name === "BAD_REQUEST") return { active: false };
1199
+ throw error;
1200
+ } else if (error instanceof Error) {
1201
+ logger.error("Introspection error:", error.message, error.stack);
1202
+ throw new APIError$1("INTERNAL_SERVER_ERROR");
1203
+ } else {
1204
+ logger.error("Introspection error:", error);
1205
+ throw new APIError$1("INTERNAL_SERVER_ERROR");
1206
+ }
1207
+ }
1208
+ }
1209
+
1210
+ //#endregion
1211
+ //#region src/logout.ts
1212
+ /**
1213
+ * IMPORTANT NOTES:
1214
+ * Follows OIDC RP-Initiated Logout
1215
+ *
1216
+ * @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html
1217
+ */
1218
+ async function rpInitiatedLogoutEndpoint(ctx, opts) {
1219
+ const { id_token_hint, client_id, post_logout_redirect_uri, state } = ctx.query;
1220
+ const baseURL = ctx.context.baseURL;
1221
+ const jwtPluginOptions = (opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options;
1222
+ const jwksUrl = jwtPluginOptions?.jwks?.remoteUrl ?? `${baseURL}${jwtPluginOptions?.jwks?.jwksPath ?? "/jwks"}`;
1223
+ let clientId = client_id;
1224
+ if (!clientId) {
1225
+ let decoded;
1226
+ try {
1227
+ decoded = decodeJwt(id_token_hint);
1228
+ } catch (_e) {
1229
+ throw new APIError$1("UNAUTHORIZED", {
1230
+ error_description: "invalid id token",
1231
+ error: "invalid_token"
1232
+ });
1233
+ }
1234
+ clientId = decoded?.aud;
1235
+ if (!clientId) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1236
+ error_description: "id token missing audience",
1237
+ error: "invalid_request"
1238
+ });
1239
+ }
1240
+ const client = await getClient(ctx, opts, clientId);
1241
+ if (!client) throw new APIError$1("BAD_REQUEST", {
1242
+ error_description: "client doesn't exist",
1243
+ error: "invalid_client"
1244
+ });
1245
+ if (client.disabled) throw new APIError$1("BAD_REQUEST", {
1246
+ error_description: "client is disabled",
1247
+ error: "invalid_client"
1248
+ });
1249
+ if (!client.enableEndSession) throw new APIError$1("UNAUTHORIZED", {
1250
+ error_description: "client unable to logout",
1251
+ error: "invalid_client"
1252
+ });
1253
+ let idTokenPayload;
1254
+ if (opts.disableJwtPlugin) {
1255
+ const clientSecret = client.clientSecret;
1256
+ if (!clientSecret) throw new APIError$1("UNAUTHORIZED", {
1257
+ error_description: "missing required credentials",
1258
+ error: "invalid_client"
1259
+ });
1260
+ const secret = await decryptStoredClientSecret(ctx, opts.storeClientSecret, clientSecret);
1261
+ const { payload } = await compactVerify(id_token_hint, new TextEncoder().encode(secret));
1262
+ const idToken = new TextDecoder().decode(payload);
1263
+ idTokenPayload = JSON.parse(idToken);
1264
+ } else {
1265
+ const { payload } = await compactVerify(id_token_hint, createLocalJWKSet(await getJwks(id_token_hint, { jwksFetch: jwksUrl })));
1266
+ const idToken = new TextDecoder().decode(payload);
1267
+ idTokenPayload = JSON.parse(idToken);
1268
+ }
1269
+ if (!idTokenPayload) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1270
+ error_description: "missing payload",
1271
+ error: "invalid_request"
1272
+ });
1273
+ if ((jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL) !== idTokenPayload.iss) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1274
+ error_description: "invalid issuer",
1275
+ error: "invalid_request"
1276
+ });
1277
+ const idTokenAudience = typeof idTokenPayload.aud === "string" ? [idTokenPayload.aud] : idTokenPayload.aud;
1278
+ if (!idTokenAudience) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1279
+ error_description: "id token missing audience",
1280
+ error: "invalid_request"
1281
+ });
1282
+ if (client_id && !idTokenAudience.includes(client_id)) throw new APIError$1("BAD_REQUEST", {
1283
+ error_description: "audience mismatch",
1284
+ error: "invalid_request"
1285
+ });
1286
+ const sessionId = idTokenPayload.sid;
1287
+ if (!sessionId) throw new APIError$1("INTERNAL_SERVER_ERROR", {
1288
+ error_description: "id token missing session",
1289
+ error: "invalid_request"
1290
+ });
1291
+ try {
1292
+ const session = await ctx.context.adapter.findOne({
1293
+ model: "session",
1294
+ where: [{
1295
+ field: "id",
1296
+ value: sessionId
1297
+ }]
1298
+ });
1299
+ session?.token ? await ctx.context.internalAdapter.deleteSession(session?.token) : session?.id ? await ctx.context.adapter.delete({
1300
+ model: "session",
1301
+ where: [{
1302
+ field: "id",
1303
+ value: session.id
1304
+ }]
1305
+ }) : await ctx.context.adapter.delete({
1306
+ model: "session",
1307
+ where: [{
1308
+ field: "id",
1309
+ value: sessionId
1310
+ }]
1311
+ });
1312
+ } catch {}
1313
+ if (post_logout_redirect_uri) {
1314
+ if (client.postLogoutRedirectUris?.includes(post_logout_redirect_uri)) {
1315
+ const redirectUri = new URL(post_logout_redirect_uri);
1316
+ if (state) redirectUri.searchParams.set("state", state);
1317
+ return handleRedirect(ctx, redirectUri.toString());
1318
+ }
1319
+ }
1320
+ }
1321
+
1322
+ //#endregion
1323
+ //#region src/register.ts
1324
+ async function registerEndpoint(ctx, opts) {
1325
+ if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
1326
+ error: "access_denied",
1327
+ error_description: "Client registration is disabled"
1328
+ });
1329
+ const body = ctx.body;
1330
+ const session = await getSessionFromCtx(ctx);
1331
+ if (!(session || opts.allowUnauthenticatedClientRegistration)) throw new APIError("UNAUTHORIZED", {
1332
+ error: "invalid_token",
1333
+ error_description: "Authentication required for client registration"
1334
+ });
1335
+ const isPublic = body.token_endpoint_auth_method === "none";
1336
+ if (!session && !isPublic) throw new APIError("UNAUTHORIZED", {
1337
+ error: "invalid_request",
1338
+ error_description: "Authentication required for confidential client registration"
1339
+ });
1340
+ if (!ctx.body.scope) ctx.body.scope = (opts.clientRegistrationDefaultScopes ?? opts.scopes)?.join(" ");
1341
+ return createOAuthClientEndpoint(ctx, opts, { isRegister: true });
1342
+ }
1343
+ async function checkOAuthClient(client, opts, settings) {
1344
+ const isPublic = client.token_endpoint_auth_method === "none";
1345
+ if (client.type) {
1346
+ if (isPublic && !(client.type === "native" || client.type === "user-agent-based")) throw new APIError("BAD_REQUEST", {
1347
+ error: "invalid_client_metadata",
1348
+ error_description: `Type must be 'native' or 'user-agent-based' for public applications`
1349
+ });
1350
+ else if (!isPublic && !(client.type === "web")) throw new APIError("BAD_REQUEST", {
1351
+ error: "invalid_client_metadata",
1352
+ error_description: `Type must be 'web' for confidential applications`
1353
+ });
1354
+ }
1355
+ if ((!client.grant_types || client.grant_types.includes("authorization_code")) && (!client.redirect_uris || client.redirect_uris.length === 0)) throw new APIError("BAD_REQUEST", {
1356
+ error: "invalid_redirect_uri",
1357
+ error_description: "Redirect URIs are required for authorization_code and implicit grant types"
1358
+ });
1359
+ const grantTypes = client.grant_types ?? ["authorization_code"];
1360
+ const responseTypes = client.response_types ?? ["code"];
1361
+ if (grantTypes.includes("authorization_code") && !responseTypes.includes("code")) throw new APIError("BAD_REQUEST", {
1362
+ error: "invalid_client_metadata",
1363
+ error_description: "When 'authorization_code' grant type is used, 'code' response type must be included"
1364
+ });
1365
+ const requestedScopes = (client?.scope)?.split(" ").filter((v) => v.length);
1366
+ const allowedScopes = settings?.isRegister ? opts.clientRegistrationAllowedScopes ?? opts.scopes : opts.scopes;
1367
+ if (allowedScopes) {
1368
+ const validScopes = new Set(allowedScopes);
1369
+ for (const requestedScope of requestedScopes ?? []) if (!validScopes?.has(requestedScope)) throw new APIError("BAD_REQUEST", {
1370
+ error: "invalid_scope",
1371
+ error_description: `cannot request scope ${requestedScope}`
1372
+ });
1373
+ }
1374
+ }
1375
+ async function createOAuthClientEndpoint(ctx, opts, settings) {
1376
+ const body = ctx.body;
1377
+ const session = await getSessionFromCtx(ctx);
1378
+ const isPublic = body.token_endpoint_auth_method === "none";
1379
+ await checkOAuthClient(ctx.body, opts, settings);
1380
+ const clientId = opts.generateClientId?.() || generateRandomString(32, "a-z", "A-Z");
1381
+ const clientSecret = isPublic ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
1382
+ const storedClientSecret = clientSecret ? await storeClientSecret(ctx, opts, clientSecret) : void 0;
1383
+ const iat = Math.floor(Date.now() / 1e3);
1384
+ const referenceId = opts.clientReference ? await opts.clientReference({
1385
+ user: session?.user,
1386
+ session: session?.session
1387
+ }) : void 0;
1388
+ const schema = oauthToSchema({
1389
+ ...body ?? {},
1390
+ disabled: void 0,
1391
+ jwks: void 0,
1392
+ jwks_uri: void 0,
1393
+ client_secret_expires_at: storedClientSecret ? settings.isRegister && opts?.clientRegistrationClientSecretExpiration ? toExpJWT(opts.clientRegistrationClientSecretExpiration, iat) : 0 : void 0,
1394
+ client_id: clientId,
1395
+ client_secret: storedClientSecret,
1396
+ client_id_issued_at: iat,
1397
+ public: isPublic,
1398
+ user_id: referenceId ? void 0 : session?.session.userId,
1399
+ reference_id: referenceId
1400
+ });
1401
+ const client = await ctx.context.adapter.create({
1402
+ model: "oauthClient",
1403
+ data: schema
1404
+ });
1405
+ return ctx.json(schemaToOAuth({
1406
+ ...client,
1407
+ clientSecret: clientSecret ? (opts.prefix?.clientSecret ?? "") + clientSecret : void 0
1408
+ }), {
1409
+ status: 201,
1410
+ headers: {
1411
+ "Cache-Control": "no-store",
1412
+ Pragma: "no-cache"
1413
+ }
1414
+ });
1415
+ }
1416
+ /**
1417
+ * Converts an OAuth 2.0 Dynamic Client Schema to a Database Schema
1418
+ *
1419
+ * @param input
1420
+ * @returns
1421
+ */
1422
+ function oauthToSchema(input) {
1423
+ 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: _jwks, jwks_uri: _jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1424
+ const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
1425
+ const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
1426
+ const scopes = _scope?.split(" ");
1427
+ const metadataObj = {
1428
+ ...rest && Object.keys(rest).length ? rest : {},
1429
+ ...inputMetadata && typeof inputMetadata === "object" ? inputMetadata : {}
1430
+ };
1431
+ return {
1432
+ clientId,
1433
+ clientSecret,
1434
+ disabled,
1435
+ scopes,
1436
+ userId,
1437
+ createdAt,
1438
+ expiresAt,
1439
+ name,
1440
+ uri,
1441
+ icon,
1442
+ contacts,
1443
+ tos,
1444
+ policy,
1445
+ softwareId,
1446
+ softwareVersion,
1447
+ softwareStatement,
1448
+ redirectUris,
1449
+ postLogoutRedirectUris,
1450
+ tokenEndpointAuthMethod,
1451
+ grantTypes,
1452
+ responseTypes,
1453
+ public: _public,
1454
+ type,
1455
+ skipConsent,
1456
+ enableEndSession,
1457
+ referenceId,
1458
+ metadata: Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0
1459
+ };
1460
+ }
1461
+ /**
1462
+ * Converts a Database Schema to an OAuth 2.0 Dynamic Client Schema
1463
+ * @param input
1464
+ * @param cleaned - default true, determines if the output has only Oauth 2.0 compatible data
1465
+ * @returns
1466
+ */
1467
+ function schemaToOAuth(input) {
1468
+ const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, skipConsent, enableEndSession, referenceId, metadata } = input;
1469
+ const _expiresAt = expiresAt ? Math.round(expiresAt.getTime() / 1e3) : void 0;
1470
+ const _createdAt = createdAt ? Math.round(createdAt.getTime() / 1e3) : void 0;
1471
+ const _scopes = scopes?.join(" ");
1472
+ return {
1473
+ ...parseClientMetadata(metadata),
1474
+ client_id: clientId,
1475
+ client_secret: clientSecret ?? void 0,
1476
+ client_secret_expires_at: clientSecret ? _expiresAt ?? 0 : void 0,
1477
+ scope: _scopes ?? void 0,
1478
+ user_id: userId ?? void 0,
1479
+ client_id_issued_at: _createdAt ?? void 0,
1480
+ client_name: name ?? void 0,
1481
+ client_uri: uri ?? void 0,
1482
+ logo_uri: icon ?? void 0,
1483
+ contacts: contacts ?? void 0,
1484
+ tos_uri: tos ?? void 0,
1485
+ policy_uri: policy ?? void 0,
1486
+ software_id: softwareId ?? void 0,
1487
+ software_version: softwareVersion ?? void 0,
1488
+ software_statement: softwareStatement ?? void 0,
1489
+ redirect_uris: redirectUris ?? void 0,
1490
+ post_logout_redirect_uris: postLogoutRedirectUris ?? void 0,
1491
+ token_endpoint_auth_method: tokenEndpointAuthMethod ?? void 0,
1492
+ grant_types: grantTypes ?? void 0,
1493
+ response_types: responseTypes ?? void 0,
1494
+ public: _public ?? void 0,
1495
+ type: type ?? void 0,
1496
+ disabled: disabled ?? void 0,
1497
+ skip_consent: skipConsent ?? void 0,
1498
+ enable_end_session: enableEndSession ?? void 0,
1499
+ reference_id: referenceId ?? void 0
1500
+ };
1501
+ }
1502
+
1503
+ //#endregion
1504
+ //#region src/types/zod.ts
1505
+ /**
1506
+ * Reusable URL validation that disallows javascript: scheme
1507
+ */
1508
+ const SafeUrlSchema = z.url().superRefine((val, ctx) => {
1509
+ if (!URL.canParse(val)) {
1510
+ ctx.addIssue({
1511
+ code: "custom",
1512
+ message: "URL must be parseable",
1513
+ fatal: true
1514
+ });
1515
+ return z.NEVER;
1516
+ }
1517
+ }).refine((url) => {
1518
+ const u = new URL(url);
1519
+ return u.protocol !== "javascript:" && u.protocol !== "data:" && u.protocol !== "vbscript:";
1520
+ }, { message: "URL cannot use javascript:, data:, or vbscript: scheme" });
1521
+
1522
+ //#endregion
1523
+ //#region src/oauthClient/endpoints.ts
1524
+ async function getClientEndpoint(ctx, opts) {
1525
+ const session = await getSessionFromCtx(ctx);
1526
+ if (!session) throw new APIError("UNAUTHORIZED");
1527
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1528
+ if (opts.clientPrivileges && !await opts.clientPrivileges({
1529
+ headers: ctx.headers,
1530
+ action: "read",
1531
+ session: session.session,
1532
+ user: session.user
1533
+ })) throw new APIError("UNAUTHORIZED");
1534
+ const client = await getClient(ctx, opts, ctx.query.client_id);
1535
+ if (!client) throw new APIError("NOT_FOUND", {
1536
+ error_description: "client not found",
1537
+ error: "not_found"
1538
+ });
1539
+ if (client.userId) {
1540
+ if (client.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
1541
+ } else if (client.referenceId && opts.clientReference) {
1542
+ if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
1543
+ } else throw new APIError("UNAUTHORIZED");
1544
+ const res = schemaToOAuth(client);
1545
+ res.client_secret = void 0;
1546
+ return res;
1547
+ }
1548
+ /**
1549
+ * Provides public client fields for any logged-in user.
1550
+ * This is commonly used to display information on login flow pages.
1551
+ */
1552
+ async function getClientPublicEndpoint(ctx, opts) {
1553
+ const client = await getClient(ctx, opts, ctx.query.client_id);
1554
+ if (!client) throw new APIError("NOT_FOUND", {
1555
+ error_description: "client not found",
1556
+ error: "not_found"
1557
+ });
1558
+ if (client.disabled) throw new APIError("NOT_FOUND", {
1559
+ error_description: "client not found",
1560
+ error: "not_found"
1561
+ });
1562
+ return schemaToOAuth({
1563
+ clientId: client.clientId,
1564
+ name: client.name,
1565
+ uri: client.uri,
1566
+ contacts: client.contacts,
1567
+ icon: client.icon,
1568
+ tos: client.tos,
1569
+ policy: client.policy
1570
+ });
1571
+ }
1572
+ async function getClientsEndpoint(ctx, opts) {
1573
+ const session = await getSessionFromCtx(ctx);
1574
+ if (!session) throw new APIError("UNAUTHORIZED");
1575
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1576
+ if (opts.clientPrivileges && !await opts.clientPrivileges({
1577
+ headers: ctx.headers,
1578
+ action: "list",
1579
+ session: session.session,
1580
+ user: session.user
1581
+ })) throw new APIError("UNAUTHORIZED");
1582
+ const referenceId = await opts.clientReference?.(session);
1583
+ if (referenceId) return await ctx.context.adapter.findMany({
1584
+ model: "oauthClient",
1585
+ where: [{
1586
+ field: "referenceId",
1587
+ value: referenceId
1588
+ }]
1589
+ }).then((res) => {
1590
+ if (!res) return null;
1591
+ return res.map((v) => {
1592
+ const res = schemaToOAuth(v);
1593
+ res.client_secret = void 0;
1594
+ return res;
1595
+ });
1596
+ });
1597
+ else if (session.user.id) return await ctx.context.adapter.findMany({
1598
+ model: "oauthClient",
1599
+ where: [{
1600
+ field: "userId",
1601
+ value: session.user.id
1602
+ }]
1603
+ }).then((res) => {
1604
+ if (!res) return null;
1605
+ return res.map((v) => {
1606
+ const res = schemaToOAuth(v);
1607
+ res.client_secret = void 0;
1608
+ return res;
1609
+ });
1610
+ });
1611
+ else throw new APIError("BAD_REQUEST", { message: "either user_id or reference_id must be provided" });
1612
+ }
1613
+ async function deleteClientEndpoint(ctx, opts) {
1614
+ const session = await getSessionFromCtx(ctx);
1615
+ if (!session) throw new APIError("UNAUTHORIZED");
1616
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1617
+ if (opts.clientPrivileges && !await opts.clientPrivileges({
1618
+ headers: ctx.headers,
1619
+ action: "delete",
1620
+ session: session.session,
1621
+ user: session.user
1622
+ })) throw new APIError("UNAUTHORIZED");
1623
+ const clientId = ctx.body.client_id;
1624
+ if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
1625
+ error_description: "trusted clients must be updated manually",
1626
+ error: "invalid_client"
1627
+ });
1628
+ const client = await getClient(ctx, opts, clientId);
1629
+ if (!client) throw new APIError("NOT_FOUND", {
1630
+ error_description: "client not found",
1631
+ error: "not_found"
1632
+ });
1633
+ if (client.userId) {
1634
+ if (client.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
1635
+ } else if (client.referenceId && opts.clientReference) {
1636
+ if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
1637
+ } else throw new APIError("UNAUTHORIZED");
1638
+ await ctx.context.adapter.delete({
1639
+ model: "oauthClient",
1640
+ where: [{
1641
+ field: "clientId",
1642
+ value: clientId
1643
+ }]
1644
+ });
1645
+ }
1646
+ async function updateClientEndpoint(ctx, opts) {
1647
+ const session = await getSessionFromCtx(ctx);
1648
+ if (!session) throw new APIError("UNAUTHORIZED");
1649
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1650
+ if (opts.clientPrivileges && !await opts.clientPrivileges({
1651
+ headers: ctx.headers,
1652
+ action: "update",
1653
+ session: session.session,
1654
+ user: session.user
1655
+ })) throw new APIError("UNAUTHORIZED");
1656
+ const clientId = ctx.body.client_id;
1657
+ if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
1658
+ error_description: "trusted clients must be updated manually",
1659
+ error: "invalid_client"
1660
+ });
1661
+ const client = await getClient(ctx, opts, clientId);
1662
+ if (!client) throw new APIError("NOT_FOUND", {
1663
+ error_description: "client not found",
1664
+ error: "not_found"
1665
+ });
1666
+ if (client.userId) {
1667
+ if (client.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
1668
+ } else if (client.referenceId && opts.clientReference) {
1669
+ if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
1670
+ } else throw new APIError("UNAUTHORIZED");
1671
+ const updates = ctx.body.update;
1672
+ if (Object.keys(updates).length === 0) {
1673
+ const res = schemaToOAuth(client);
1674
+ res.client_secret = void 0;
1675
+ return res;
1676
+ }
1677
+ await checkOAuthClient({
1678
+ ...schemaToOAuth(client),
1679
+ ...updates
1680
+ }, opts);
1681
+ const updatedClient = await ctx.context.adapter.update({
1682
+ model: "oauthClient",
1683
+ where: [{
1684
+ field: "clientId",
1685
+ value: clientId
1686
+ }],
1687
+ update: oauthToSchema(updates)
1688
+ });
1689
+ if (!updatedClient) throw new APIError("INTERNAL_SERVER_ERROR", {
1690
+ error_description: "unable to update client",
1691
+ error: "invalid_client"
1692
+ });
1693
+ const res = schemaToOAuth(updatedClient);
1694
+ res.client_secret = void 0;
1695
+ return res;
1696
+ }
1697
+ async function rotateClientSecretEndpoint(ctx, opts) {
1698
+ const session = await getSessionFromCtx(ctx);
1699
+ if (!session) throw new APIError("UNAUTHORIZED");
1700
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1701
+ if (opts.clientPrivileges && !await opts.clientPrivileges({
1702
+ headers: ctx.headers,
1703
+ action: "rotate",
1704
+ session: session.session,
1705
+ user: session.user
1706
+ })) throw new APIError("UNAUTHORIZED");
1707
+ const clientId = ctx.body.client_id;
1708
+ if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
1709
+ error_description: "trusted clients must be updated manually",
1710
+ error: "invalid_client"
1711
+ });
1712
+ const client = await getClient(ctx, opts, clientId);
1713
+ if (!client) throw new APIError("NOT_FOUND", {
1714
+ error_description: "client not found",
1715
+ error: "not_found"
1716
+ });
1717
+ if (client.userId) {
1718
+ if (client.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
1719
+ } else if (client.referenceId && opts.clientReference) {
1720
+ if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
1721
+ } else throw new APIError("UNAUTHORIZED");
1722
+ if (client.public || !client.clientSecret) throw new APIError("BAD_REQUEST", {
1723
+ error_description: "public clients cannot be updated",
1724
+ error: "invalid_client"
1725
+ });
1726
+ const clientSecret = opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
1727
+ const storedClientSecret = clientSecret ? await storeClientSecret(ctx, opts, clientSecret) : void 0;
1728
+ const updatedClient = await ctx.context.adapter.update({
1729
+ model: "oauthClient",
1730
+ where: [{
1731
+ field: "clientId",
1732
+ value: clientId
1733
+ }],
1734
+ update: {
1735
+ ...schemaToOAuth(client),
1736
+ clientSecret: storedClientSecret
1737
+ }
1738
+ });
1739
+ if (!updatedClient) throw new APIError("INTERNAL_SERVER_ERROR", {
1740
+ error_description: "unable to update client",
1741
+ error: "invalid_client"
1742
+ });
1743
+ return schemaToOAuth({
1744
+ ...updatedClient,
1745
+ clientSecret: (opts.prefix?.clientSecret ?? "") + clientSecret
1746
+ });
1747
+ }
1748
+
1749
+ //#endregion
1750
+ //#region src/oauthClient/index.ts
1751
+ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/create-client", {
1752
+ method: "POST",
1753
+ body: z.object({
1754
+ redirect_uris: z.array(SafeUrlSchema).min(1),
1755
+ scope: z.string().optional(),
1756
+ client_name: z.string().optional(),
1757
+ client_uri: z.string().optional(),
1758
+ logo_uri: z.string().optional(),
1759
+ contacts: z.array(z.string().min(1)).min(1).optional(),
1760
+ tos_uri: z.string().optional(),
1761
+ policy_uri: z.string().optional(),
1762
+ software_id: z.string().optional(),
1763
+ software_version: z.string().optional(),
1764
+ software_statement: z.string().optional(),
1765
+ post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
1766
+ token_endpoint_auth_method: z.enum([
1767
+ "none",
1768
+ "client_secret_basic",
1769
+ "client_secret_post"
1770
+ ]).default("client_secret_basic").optional(),
1771
+ grant_types: z.array(z.enum([
1772
+ "authorization_code",
1773
+ "client_credentials",
1774
+ "refresh_token"
1775
+ ])).default(["authorization_code"]).optional(),
1776
+ response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
1777
+ type: z.enum([
1778
+ "web",
1779
+ "native",
1780
+ "user-agent-based"
1781
+ ]).optional(),
1782
+ client_secret_expires_at: z.union([z.string(), z.number()]).optional().default(0),
1783
+ skip_consent: z.boolean().optional(),
1784
+ enable_end_session: z.boolean().optional(),
1785
+ metadata: z.record(z.string(), z.unknown()).optional()
1786
+ }),
1787
+ metadata: {
1788
+ SERVER_ONLY: true,
1789
+ openapi: {
1790
+ description: "Register an OAuth2 application",
1791
+ responses: { "200": {
1792
+ description: "OAuth2 application registered successfully",
1793
+ content: { "application/json": { schema: {
1794
+ type: "object",
1795
+ properties: {
1796
+ client_id: {
1797
+ type: "string",
1798
+ description: "Unique identifier for the client"
1799
+ },
1800
+ client_secret: {
1801
+ type: "string",
1802
+ description: "Secret key for the client"
1803
+ },
1804
+ client_secret_expires_at: {
1805
+ type: "number",
1806
+ description: "Time the client secret will expire. If 0, the client secret will never expire."
1807
+ },
1808
+ scope: {
1809
+ type: "string",
1810
+ description: "Space-separated scopes allowed by the client"
1811
+ },
1812
+ user_id: {
1813
+ type: "string",
1814
+ description: "ID of the user who registered the client, null if registered anonymously"
1815
+ },
1816
+ client_id_issued_at: {
1817
+ type: "number",
1818
+ description: "Creation timestamp of this client"
1819
+ },
1820
+ client_name: {
1821
+ type: "string",
1822
+ description: "Name of the OAuth2 application"
1823
+ },
1824
+ client_uri: {
1825
+ type: "string",
1826
+ description: "URI of the OAuth2 application"
1827
+ },
1828
+ logo_uri: {
1829
+ type: "string",
1830
+ description: "Icon URI for the application"
1831
+ },
1832
+ contacts: {
1833
+ type: "array",
1834
+ items: { type: "string" },
1835
+ description: "List representing ways to contact people responsible for this client, typically email addresses"
1836
+ },
1837
+ tos_uri: {
1838
+ type: "string",
1839
+ description: "Client's terms of service uri"
1840
+ },
1841
+ policy_uri: {
1842
+ type: "string",
1843
+ description: "Client's policy uri"
1844
+ },
1845
+ software_id: {
1846
+ type: "string",
1847
+ description: "Unique identifier assigned by the developer to help in the dynamic registration process"
1848
+ },
1849
+ software_version: {
1850
+ type: "string",
1851
+ description: "Version identifier for the software_id"
1852
+ },
1853
+ software_statement: {
1854
+ type: "string",
1855
+ description: "JWT containing metadata values about the client software as claims"
1856
+ },
1857
+ redirect_uris: {
1858
+ type: "array",
1859
+ items: {
1860
+ type: "string",
1861
+ format: "uri"
1862
+ },
1863
+ description: "List of allowed redirect uris"
1864
+ },
1865
+ token_endpoint_auth_method: {
1866
+ type: "string",
1867
+ description: "Requested authentication method for the token endpoint",
1868
+ enum: [
1869
+ "none",
1870
+ "client_secret_basic",
1871
+ "client_secret_post"
1872
+ ]
1873
+ },
1874
+ grant_types: {
1875
+ type: "array",
1876
+ items: {
1877
+ type: "string",
1878
+ enum: [
1879
+ "authorization_code",
1880
+ "client_credentials",
1881
+ "refresh_token"
1882
+ ]
1883
+ },
1884
+ description: "Requested authentication method for the token endpoint"
1885
+ },
1886
+ response_types: {
1887
+ type: "array",
1888
+ items: {
1889
+ type: "string",
1890
+ enum: ["code"]
1891
+ },
1892
+ description: "Requested authentication method for the token endpoint"
1893
+ },
1894
+ public: {
1895
+ type: "boolean",
1896
+ description: "Whether the client is public as determined by the type"
1897
+ },
1898
+ type: {
1899
+ type: "string",
1900
+ description: "Type of the client",
1901
+ enum: [
1902
+ "web",
1903
+ "native",
1904
+ "user-agent-based"
1905
+ ]
1906
+ },
1907
+ disabled: {
1908
+ type: "boolean",
1909
+ description: "Whether the client is disabled"
1910
+ },
1911
+ metadata: {
1912
+ type: "object",
1913
+ additionalProperties: true,
1914
+ nullable: true,
1915
+ description: "Additional metadata for the application"
1916
+ }
1917
+ },
1918
+ required: ["client_id"]
1919
+ } } }
1920
+ } }
1921
+ }
1922
+ }
1923
+ }, async (ctx) => {
1924
+ return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
1925
+ });
1926
+ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client", {
1927
+ method: "POST",
1928
+ use: [sessionMiddleware],
1929
+ body: z.object({
1930
+ redirect_uris: z.array(SafeUrlSchema).min(1),
1931
+ scope: z.string().optional(),
1932
+ client_name: z.string().optional(),
1933
+ client_uri: z.string().optional(),
1934
+ logo_uri: z.string().optional(),
1935
+ contacts: z.array(z.string().min(1)).min(1).optional(),
1936
+ tos_uri: z.string().optional(),
1937
+ policy_uri: z.string().optional(),
1938
+ software_id: z.string().optional(),
1939
+ software_version: z.string().optional(),
1940
+ software_statement: z.string().optional(),
1941
+ post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
1942
+ token_endpoint_auth_method: z.enum([
1943
+ "none",
1944
+ "client_secret_basic",
1945
+ "client_secret_post"
1946
+ ]).default("client_secret_basic").optional(),
1947
+ grant_types: z.array(z.enum([
1948
+ "authorization_code",
1949
+ "client_credentials",
1950
+ "refresh_token"
1951
+ ])).default(["authorization_code"]).optional(),
1952
+ response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
1953
+ type: z.enum([
1954
+ "web",
1955
+ "native",
1956
+ "user-agent-based"
1957
+ ]).optional()
1958
+ }),
1959
+ metadata: { openapi: {
1960
+ description: "Register an OAuth2 application",
1961
+ responses: { "200": {
1962
+ description: "OAuth2 application registered successfully",
1963
+ content: { "application/json": { schema: {
1964
+ type: "object",
1965
+ properties: {
1966
+ client_id: {
1967
+ type: "string",
1968
+ description: "Unique identifier for the client"
1969
+ },
1970
+ client_secret: {
1971
+ type: "string",
1972
+ description: "Secret key for the client"
1973
+ },
1974
+ client_secret_expires_at: {
1975
+ type: "number",
1976
+ description: "Time the client secret will expire. If 0, the client secret will never expire."
1977
+ },
1978
+ scope: {
1979
+ type: "string",
1980
+ description: "Space-separated scopes allowed by the client"
1981
+ },
1982
+ user_id: {
1983
+ type: "string",
1984
+ description: "ID of the user who registered the client, null if registered anonymously"
1985
+ },
1986
+ client_id_issued_at: {
1987
+ type: "number",
1988
+ description: "Creation timestamp of this client"
1989
+ },
1990
+ client_name: {
1991
+ type: "string",
1992
+ description: "Name of the OAuth2 application"
1993
+ },
1994
+ client_uri: {
1995
+ type: "string",
1996
+ description: "URI of the OAuth2 application"
1997
+ },
1998
+ logo_uri: {
1999
+ type: "string",
2000
+ description: "Icon URI for the application"
2001
+ },
2002
+ contacts: {
2003
+ type: "array",
2004
+ items: { type: "string" },
2005
+ description: "List representing ways to contact people responsible for this client, typically email addresses"
2006
+ },
2007
+ tos_uri: {
2008
+ type: "string",
2009
+ description: "Client's terms of service uri"
2010
+ },
2011
+ policy_uri: {
2012
+ type: "string",
2013
+ description: "Client's policy uri"
2014
+ },
2015
+ software_id: {
2016
+ type: "string",
2017
+ description: "Unique identifier assigned by the developer to help in the dynamic registration process"
2018
+ },
2019
+ software_version: {
2020
+ type: "string",
2021
+ description: "Version identifier for the software_id"
2022
+ },
2023
+ software_statement: {
2024
+ type: "string",
2025
+ description: "JWT containing metadata values about the client software as claims"
2026
+ },
2027
+ redirect_uris: {
2028
+ type: "array",
2029
+ items: {
2030
+ type: "string",
2031
+ format: "uri"
2032
+ },
2033
+ description: "List of allowed redirect uris"
2034
+ },
2035
+ token_endpoint_auth_method: {
2036
+ type: "string",
2037
+ description: "Response types the client may use",
2038
+ enum: [
2039
+ "none",
2040
+ "client_secret_basic",
2041
+ "client_secret_post"
2042
+ ]
2043
+ },
2044
+ grant_types: {
2045
+ type: "array",
2046
+ items: {
2047
+ type: "string",
2048
+ enum: [
2049
+ "authorization_code",
2050
+ "client_credentials",
2051
+ "refresh_token"
2052
+ ]
2053
+ },
2054
+ description: "Requested authentication method for the token endpoint"
2055
+ },
2056
+ response_types: {
2057
+ type: "array",
2058
+ items: {
2059
+ type: "string",
2060
+ enum: ["code"]
2061
+ },
2062
+ description: "Requested authentication method for the token endpoint"
2063
+ },
2064
+ public: {
2065
+ type: "boolean",
2066
+ description: "Whether the client is public as determined by the type"
2067
+ },
2068
+ type: {
2069
+ type: "string",
2070
+ description: "Type of the client",
2071
+ enum: [
2072
+ "web",
2073
+ "native",
2074
+ "user-agent-based"
2075
+ ]
2076
+ },
2077
+ disabled: {
2078
+ type: "boolean",
2079
+ description: "Whether the client is disabled"
2080
+ },
2081
+ metadata: {
2082
+ type: "object",
2083
+ additionalProperties: true,
2084
+ nullable: true,
2085
+ description: "Additional metadata for the application"
2086
+ }
2087
+ },
2088
+ required: ["client_id"]
2089
+ } } }
2090
+ } }
2091
+ } }
2092
+ }, async (ctx) => {
2093
+ return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
2094
+ });
2095
+ const getOAuthClient = (opts) => createAuthEndpoint("/oauth2/get-client", {
2096
+ method: "GET",
2097
+ use: [sessionMiddleware],
2098
+ query: z.object({ client_id: z.string() }),
2099
+ metadata: { openapi: { description: "Get OAuth2 formatted client details" } }
2100
+ }, async (ctx) => {
2101
+ return getClientEndpoint(ctx, opts);
2102
+ });
2103
+ const getOAuthClientPublic = (opts) => createAuthEndpoint("/oauth2/public-client", {
2104
+ method: "GET",
2105
+ use: [sessionMiddleware],
2106
+ query: z.object({ client_id: z.string() }),
2107
+ metadata: { openapi: { description: "Gets publically available client fields" } }
2108
+ }, async (ctx) => {
2109
+ return getClientPublicEndpoint(ctx, opts);
2110
+ });
2111
+ const getOAuthClients = (opts) => createAuthEndpoint("/oauth2/get-clients", {
2112
+ method: "GET",
2113
+ use: [sessionMiddleware],
2114
+ metadata: { openapi: { description: "Get OAuth2 formatted client details for a user or organization" } }
2115
+ }, async (ctx) => {
2116
+ return getClientsEndpoint(ctx, opts);
2117
+ });
2118
+ const adminUpdateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/update-client", {
2119
+ method: "PATCH",
2120
+ body: z.object({
2121
+ client_id: z.string(),
2122
+ update: z.object({
2123
+ redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2124
+ scope: z.string().optional(),
2125
+ client_name: z.string().optional(),
2126
+ client_uri: z.string().optional(),
2127
+ logo_uri: z.string().optional(),
2128
+ contacts: z.array(z.string().min(1)).min(1).optional(),
2129
+ tos_uri: z.string().optional(),
2130
+ policy_uri: z.string().optional(),
2131
+ software_id: z.string().optional(),
2132
+ software_version: z.string().optional(),
2133
+ software_statement: z.string().optional(),
2134
+ post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2135
+ grant_types: z.array(z.enum([
2136
+ "authorization_code",
2137
+ "client_credentials",
2138
+ "refresh_token"
2139
+ ])).optional(),
2140
+ response_types: z.array(z.enum(["code"])).optional(),
2141
+ type: z.enum([
2142
+ "web",
2143
+ "native",
2144
+ "user-agent-based"
2145
+ ]).optional(),
2146
+ client_secret_expires_at: z.union([z.string(), z.number()]).optional(),
2147
+ skip_consent: z.boolean().optional(),
2148
+ enable_end_session: z.boolean().optional(),
2149
+ metadata: z.record(z.string(), z.unknown()).optional()
2150
+ })
2151
+ }),
2152
+ metadata: {
2153
+ SERVER_ONLY: true,
2154
+ openapi: { description: "Updates OAuth2 formatted client details." }
2155
+ }
2156
+ }, async (ctx) => {
2157
+ return updateClientEndpoint(ctx, opts);
2158
+ });
2159
+ const updateOAuthClient = (opts) => createAuthEndpoint("/oauth2/update-client", {
2160
+ method: "POST",
2161
+ use: [sessionMiddleware],
2162
+ body: z.object({
2163
+ client_id: z.string(),
2164
+ update: z.object({
2165
+ redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2166
+ scope: z.string().optional(),
2167
+ client_name: z.string().optional(),
2168
+ client_uri: z.string().optional(),
2169
+ logo_uri: z.string().optional(),
2170
+ contacts: z.array(z.string().min(1)).min(1).optional(),
2171
+ tos_uri: z.string().optional(),
2172
+ policy_uri: z.string().optional(),
2173
+ software_id: z.string().optional(),
2174
+ software_version: z.string().optional(),
2175
+ software_statement: z.string().optional(),
2176
+ post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
2177
+ grant_types: z.array(z.enum([
2178
+ "authorization_code",
2179
+ "client_credentials",
2180
+ "refresh_token"
2181
+ ])).optional(),
2182
+ response_types: z.array(z.enum(["code"])).optional(),
2183
+ type: z.enum([
2184
+ "web",
2185
+ "native",
2186
+ "user-agent-based"
2187
+ ]).optional()
2188
+ })
2189
+ }),
2190
+ metadata: { openapi: { description: "Updates OAuth2 formatted client details." } }
2191
+ }, async (ctx) => {
2192
+ return updateClientEndpoint(ctx, opts);
2193
+ });
2194
+ const rotateClientSecret = (opts) => createAuthEndpoint("/oauth2/client/rotate-secret", {
2195
+ method: "POST",
2196
+ use: [sessionMiddleware],
2197
+ body: z.object({ client_id: z.string() }),
2198
+ metadata: { openapi: { description: "Rotates a confidential client's secret" } }
2199
+ }, async (ctx) => {
2200
+ return rotateClientSecretEndpoint(ctx, opts);
2201
+ });
2202
+ const deleteOAuthClient = (opts) => createAuthEndpoint("/oauth2/delete-client", {
2203
+ method: "POST",
2204
+ use: [sessionMiddleware],
2205
+ body: z.object({ client_id: z.string() }),
2206
+ metadata: { openapi: { description: "Deletes an oauth client" } }
2207
+ }, async (ctx) => {
2208
+ return deleteClientEndpoint(ctx, opts);
2209
+ });
2210
+
2211
+ //#endregion
2212
+ //#region src/oauthConsent/endpoints.ts
2213
+ async function getConsent(ctx, opts, id) {
2214
+ return await ctx.context.adapter.findOne({
2215
+ model: "oauthConsent",
2216
+ where: [{
2217
+ field: "id",
2218
+ value: id
2219
+ }]
2220
+ });
2221
+ }
2222
+ async function getConsentEndpoint(ctx, opts) {
2223
+ const session = await getSessionFromCtx(ctx);
2224
+ if (!session) throw new APIError("UNAUTHORIZED");
2225
+ const { id } = ctx.query;
2226
+ if (!id) throw new APIError("NOT_FOUND", {
2227
+ error_description: "missing id parameter",
2228
+ error: "not_found"
2229
+ });
2230
+ const consent = await getConsent(ctx, opts, id);
2231
+ if (!consent) throw new APIError("NOT_FOUND", {
2232
+ error_description: "no consent",
2233
+ error: "not_found"
2234
+ });
2235
+ if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
2236
+ return consent;
2237
+ }
2238
+ async function getConsentsEndpoint(ctx, opts) {
2239
+ const session = await getSessionFromCtx(ctx);
2240
+ if (!session) throw new APIError("UNAUTHORIZED");
2241
+ return await ctx.context.adapter.findMany({
2242
+ model: "oauthConsent",
2243
+ where: [{
2244
+ field: "userId",
2245
+ value: session.user.id
2246
+ }]
2247
+ });
2248
+ }
2249
+ async function deleteConsentEndpoint(ctx, opts) {
2250
+ const session = await getSessionFromCtx(ctx);
2251
+ if (!session) throw new APIError("UNAUTHORIZED");
2252
+ const { id } = ctx.body;
2253
+ if (!id) throw new APIError("NOT_FOUND", {
2254
+ error_description: "missing id parameter",
2255
+ error: "not_found"
2256
+ });
2257
+ const consent = await getConsent(ctx, opts, id);
2258
+ if (!consent) throw new APIError("NOT_FOUND", {
2259
+ error_description: "no consent",
2260
+ error: "not_found"
2261
+ });
2262
+ if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
2263
+ await ctx.context.adapter.delete({
2264
+ model: "oauthConsent",
2265
+ where: [{
2266
+ field: "id",
2267
+ value: id
2268
+ }]
2269
+ });
2270
+ }
2271
+ async function updateConsentEndpoint(ctx, opts) {
2272
+ const session = await getSessionFromCtx(ctx);
2273
+ if (!session) throw new APIError("UNAUTHORIZED");
2274
+ const { id } = ctx.body;
2275
+ if (!id) throw new APIError("NOT_FOUND", {
2276
+ error_description: "missing id parameter",
2277
+ error: "not_found"
2278
+ });
2279
+ const consent = await getConsent(ctx, opts, id);
2280
+ if (!consent) throw new APIError("NOT_FOUND", {
2281
+ error_description: "no consent",
2282
+ error: "not_found"
2283
+ });
2284
+ const client = await getClient(ctx, opts, consent.clientId);
2285
+ if (!consent) throw new APIError("NOT_FOUND", {
2286
+ error_description: "no consent",
2287
+ error: "not_found"
2288
+ });
2289
+ if (consent.userId !== session.user.id) throw new APIError("UNAUTHORIZED");
2290
+ const allowedScopes = client?.scopes ?? opts.scopes ?? [];
2291
+ const updates = ctx.body.update;
2292
+ const scopes = updates.scopes;
2293
+ if (scopes && !scopes.every((val) => allowedScopes?.includes(val))) throw new APIError("BAD_REQUEST", {
2294
+ error_description: `unable to provide scopes to ${client?.referenceId ?? client?.userId}`,
2295
+ error: "invalid_request"
2296
+ });
2297
+ const iat = Math.floor(Date.now() / 1e3);
2298
+ return await ctx.context.adapter.update({
2299
+ model: "oauthConsent",
2300
+ where: [{
2301
+ field: "id",
2302
+ value: id
2303
+ }],
2304
+ update: {
2305
+ ...updates,
2306
+ updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
2307
+ }
2308
+ });
2309
+ }
2310
+
2311
+ //#endregion
2312
+ //#region src/oauthConsent/index.ts
2313
+ const getOAuthConsent = (opts) => createAuthEndpoint("/oauth2/get-consent", {
2314
+ method: "GET",
2315
+ query: z.object({ id: z.string() }),
2316
+ use: [sessionMiddleware],
2317
+ metadata: { openapi: { description: "Gets details of a specific OAuth2 consent for a user" } }
2318
+ }, async (ctx) => {
2319
+ return getConsentEndpoint(ctx, opts);
2320
+ });
2321
+ const getOAuthConsents = (opts) => createAuthEndpoint("/oauth2/get-consents", {
2322
+ method: "GET",
2323
+ use: [sessionMiddleware],
2324
+ metadata: { openapi: { description: "Gets all available OAuth2 consents for a user" } }
2325
+ }, async (ctx) => {
2326
+ return getConsentsEndpoint(ctx, opts);
2327
+ });
2328
+ const updateOAuthConsent = (opts) => createAuthEndpoint("/oauth2/update-consent", {
2329
+ method: "POST",
2330
+ use: [sessionMiddleware],
2331
+ body: z.object({
2332
+ id: z.string(),
2333
+ update: z.object({ scopes: z.array(z.string()) })
2334
+ }),
2335
+ metadata: { openapi: { description: "Updates consent granted to a client." } }
2336
+ }, async (ctx) => {
2337
+ return updateConsentEndpoint(ctx, opts);
2338
+ });
2339
+ const deleteOAuthConsent = (opts) => createAuthEndpoint("/oauth2/delete-consent", {
2340
+ method: "POST",
2341
+ use: [sessionMiddleware],
2342
+ body: z.object({ id: z.string() }),
2343
+ metadata: { openapi: { description: "Deletes consent granted to a client" } }
2344
+ }, async (ctx) => {
2345
+ return deleteConsentEndpoint(ctx, opts);
2346
+ });
2347
+
2348
+ //#endregion
2349
+ //#region src/revoke.ts
2350
+ /**
2351
+ * IMPORTANT NOTES:
2352
+ * Revocation follows RFC7009
2353
+ * https://datatracker.ietf.org/doc/html/rfc7009
2354
+ * - APIError: Continue catches (returnable to client)
2355
+ * - Error: Should immediately stop catches (internal error)
2356
+ */
2357
+ /**
2358
+ * Revokes a JWT access token against the configured JWKs.
2359
+ * (does nothing if successful since a JWT is not stored on the server)
2360
+ */
2361
+ async function revokeJwtAccessToken(ctx, opts, token) {
2362
+ const jwtPlugin = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context);
2363
+ const jwtPluginOptions = jwtPlugin?.options;
2364
+ try {
2365
+ await verifyJwsAccessToken(token, {
2366
+ jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
2367
+ return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
2368
+ },
2369
+ verifyOptions: {
2370
+ audience: opts.validAudiences ?? ctx.context.baseURL,
2371
+ issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL
2372
+ }
2373
+ });
2374
+ } catch (error) {
2375
+ if (error instanceof Error) {
2376
+ if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
2377
+ error_description: "invalid JWT signature",
2378
+ error: "invalid_request"
2379
+ });
2380
+ else if (error.name === "JWTExpired") return null;
2381
+ else if (error.name === "JWTInvalid") return null;
2382
+ throw error;
2383
+ }
2384
+ throw new Error(error);
2385
+ }
2386
+ }
2387
+ /**
2388
+ * Searches for an opaque access token in the database and validates it
2389
+ */
2390
+ async function revokeOpaqueAccessToken(ctx, opts, token, clientId) {
2391
+ let tokenValue = token;
2392
+ if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
2393
+ else throw new APIError$1("BAD_REQUEST", {
2394
+ error_description: "opaque access token not found",
2395
+ error: "invalid_request"
2396
+ });
2397
+ const accessToken = await ctx.context.adapter.findOne({
2398
+ model: "oauthAccessToken",
2399
+ where: [{
2400
+ field: "token",
2401
+ value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
2402
+ }]
2403
+ });
2404
+ if (!accessToken) throw new APIError$1("BAD_REQUEST", {
2405
+ error_description: "opaque access token not found",
2406
+ error: "invalid_request"
2407
+ });
2408
+ if (!accessToken.clientId || accessToken.clientId !== clientId) return null;
2409
+ accessToken.id ? await ctx.context.adapter.delete({
2410
+ model: "oauthAccessToken",
2411
+ where: [{
2412
+ field: "id",
2413
+ value: accessToken.id
2414
+ }]
2415
+ }) : await ctx.context.adapter.delete({
2416
+ model: "oauthAccessToken",
2417
+ where: [{
2418
+ field: "token",
2419
+ value: accessToken.token
2420
+ }]
2421
+ });
2422
+ }
2423
+ /**
2424
+ * Validates a refresh token in the session store.
2425
+ */
2426
+ async function revokeRefreshToken(ctx, opts, token, clientId) {
2427
+ const refreshToken = await ctx.context.adapter.findOne({
2428
+ model: "oauthRefreshToken",
2429
+ where: [{
2430
+ field: "token",
2431
+ value: await getStoredToken(opts.storeTokens, token, "refresh_token")
2432
+ }]
2433
+ });
2434
+ if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
2435
+ error_description: "token not found",
2436
+ error: "invalid_request"
2437
+ });
2438
+ if (refreshToken.revoked) {
2439
+ await ctx.context.adapter.deleteMany({
2440
+ model: "oauthRefreshToken",
2441
+ where: [{
2442
+ field: "clientId",
2443
+ value: clientId
2444
+ }, {
2445
+ field: "userId",
2446
+ value: refreshToken.userId
2447
+ }]
2448
+ });
2449
+ throw new APIError$1("BAD_REQUEST", {
2450
+ error_description: "refresh token revoked",
2451
+ error: "invalid_request"
2452
+ });
2453
+ }
2454
+ if (!refreshToken.clientId || refreshToken.clientId !== clientId) return null;
2455
+ const iat = Math.floor(Date.now() / 1e3);
2456
+ await Promise.allSettled([ctx.context.adapter.deleteMany({
2457
+ model: "oauthAccessToken",
2458
+ where: [{
2459
+ field: "refreshId",
2460
+ value: refreshToken.id
2461
+ }]
2462
+ }), ctx.context.adapter.update({
2463
+ model: "oauthRefreshToken",
2464
+ where: [{
2465
+ field: "id",
2466
+ value: refreshToken.id
2467
+ }],
2468
+ update: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
2469
+ })]);
2470
+ }
2471
+ /**
2472
+ * We don't know the access token format so we try to validate it
2473
+ * as a JWT first, then as an opaque token.
2474
+ */
2475
+ async function revokeAccessToken(ctx, opts, clientId, token) {
2476
+ try {
2477
+ return await revokeJwtAccessToken(ctx, opts, token);
2478
+ } catch (err) {
2479
+ if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
2480
+ else throw new Error(err);
2481
+ }
2482
+ try {
2483
+ return await revokeOpaqueAccessToken(ctx, opts, token, clientId);
2484
+ } catch (err) {
2485
+ if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
2486
+ else throw new Error("Unknown error validating access token");
2487
+ }
2488
+ throw new APIError$1("BAD_REQUEST", {
2489
+ error_description: "Invalid access token",
2490
+ error: "invalid_request"
2491
+ });
2492
+ }
2493
+ async function revokeEndpoint(ctx, opts) {
2494
+ let { client_id, client_secret, token, token_type_hint } = ctx.body;
2495
+ const authorization = ctx.request?.headers.get("authorization") || null;
2496
+ if (authorization?.startsWith("Basic ")) {
2497
+ const res = basicToClientCredentials(authorization);
2498
+ client_id = res?.client_id;
2499
+ client_secret = res?.client_secret;
2500
+ }
2501
+ if (!client_id) throw new APIError$1("UNAUTHORIZED", {
2502
+ error_description: "missing required credentials",
2503
+ error: "invalid_client"
2504
+ });
2505
+ if (typeof token === "string" && token.startsWith("Bearer ")) token = token.replace("Bearer ", "");
2506
+ if (!token?.length) throw new APIError$1("BAD_REQUEST", {
2507
+ error_description: "missing a required token for introspection",
2508
+ error: "invalid_request"
2509
+ });
2510
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
2511
+ try {
2512
+ if (token_type_hint === void 0 || token_type_hint === "access_token") try {
2513
+ return await revokeAccessToken(ctx, opts, client.clientId, token);
2514
+ } catch (error) {
2515
+ if (error instanceof APIError$1) {
2516
+ if (token_type_hint === "access_token") throw error;
2517
+ } else if (error instanceof Error) throw error;
2518
+ else throw new Error(error);
2519
+ }
2520
+ if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
2521
+ return await revokeRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId);
2522
+ } catch (error) {
2523
+ if (error instanceof APIError$1) {
2524
+ if (token_type_hint === "refresh_token") throw error;
2525
+ } else if (error instanceof Error) throw error;
2526
+ else throw new Error(error);
2527
+ }
2528
+ throw new APIError$1("BAD_REQUEST", {
2529
+ error_description: "token not found",
2530
+ error: "invalid_request"
2531
+ });
2532
+ } catch (error) {
2533
+ if (error instanceof APIError$1) {
2534
+ if (error.name === "BAD_REQUEST") return null;
2535
+ throw error;
2536
+ } else if (error instanceof Error) {
2537
+ logger.error("Introspection error:", error.message, error.stack);
2538
+ throw new APIError$1("INTERNAL_SERVER_ERROR");
2539
+ } else {
2540
+ logger.error("Introspection error:", error);
2541
+ throw new APIError$1("INTERNAL_SERVER_ERROR");
2542
+ }
2543
+ }
2544
+ }
2545
+
2546
+ //#endregion
2547
+ //#region src/schema.ts
2548
+ const schema = {
2549
+ oauthClient: {
2550
+ modelName: "oauthClient",
2551
+ fields: {
2552
+ clientId: {
2553
+ type: "string",
2554
+ unique: true,
2555
+ required: true
2556
+ },
2557
+ clientSecret: {
2558
+ type: "string",
2559
+ required: false
2560
+ },
2561
+ disabled: {
2562
+ type: "boolean",
2563
+ defaultValue: false,
2564
+ required: false
2565
+ },
2566
+ skipConsent: {
2567
+ type: "boolean",
2568
+ required: false
2569
+ },
2570
+ enableEndSession: {
2571
+ type: "boolean",
2572
+ required: false
2573
+ },
2574
+ scopes: {
2575
+ type: "string[]",
2576
+ required: false
2577
+ },
2578
+ userId: {
2579
+ type: "string",
2580
+ required: false,
2581
+ references: {
2582
+ model: "user",
2583
+ field: "id"
2584
+ }
2585
+ },
2586
+ createdAt: {
2587
+ type: "date",
2588
+ required: false
2589
+ },
2590
+ updatedAt: {
2591
+ type: "date",
2592
+ required: false
2593
+ },
2594
+ name: {
2595
+ type: "string",
2596
+ required: false
2597
+ },
2598
+ uri: {
2599
+ type: "string",
2600
+ required: false
2601
+ },
2602
+ icon: {
2603
+ type: "string",
2604
+ required: false
2605
+ },
2606
+ contacts: {
2607
+ type: "string[]",
2608
+ required: false
2609
+ },
2610
+ tos: {
2611
+ type: "string",
2612
+ required: false
2613
+ },
2614
+ policy: {
2615
+ type: "string",
2616
+ required: false
2617
+ },
2618
+ softwareId: {
2619
+ type: "string",
2620
+ required: false
2621
+ },
2622
+ softwareVersion: {
2623
+ type: "string",
2624
+ required: false
2625
+ },
2626
+ softwareStatement: {
2627
+ type: "string",
2628
+ required: false
2629
+ },
2630
+ redirectUris: {
2631
+ type: "string[]",
2632
+ required: true
2633
+ },
2634
+ postLogoutRedirectUris: {
2635
+ type: "string[]",
2636
+ required: false
2637
+ },
2638
+ tokenEndpointAuthMethod: {
2639
+ type: "string",
2640
+ required: false
2641
+ },
2642
+ grantTypes: {
2643
+ type: "string[]",
2644
+ required: false
2645
+ },
2646
+ responseTypes: {
2647
+ type: "string[]",
2648
+ required: false
2649
+ },
2650
+ public: {
2651
+ type: "boolean",
2652
+ required: false
2653
+ },
2654
+ type: {
2655
+ type: "string",
2656
+ required: false
2657
+ },
2658
+ referenceId: {
2659
+ type: "string",
2660
+ required: false
2661
+ },
2662
+ metadata: {
2663
+ type: "json",
2664
+ required: false
2665
+ }
2666
+ }
2667
+ },
2668
+ oauthRefreshToken: { fields: {
2669
+ token: {
2670
+ type: "string",
2671
+ required: true
2672
+ },
2673
+ clientId: {
2674
+ type: "string",
2675
+ required: true,
2676
+ references: {
2677
+ model: "oauthClient",
2678
+ field: "clientId"
2679
+ }
2680
+ },
2681
+ sessionId: {
2682
+ type: "string",
2683
+ required: false,
2684
+ references: {
2685
+ model: "session",
2686
+ field: "id",
2687
+ onDelete: "set null"
2688
+ }
2689
+ },
2690
+ userId: {
2691
+ type: "string",
2692
+ required: true,
2693
+ references: {
2694
+ model: "user",
2695
+ field: "id"
2696
+ }
2697
+ },
2698
+ referenceId: {
2699
+ type: "string",
2700
+ required: false
2701
+ },
2702
+ expiresAt: { type: "date" },
2703
+ createdAt: { type: "date" },
2704
+ revoked: {
2705
+ type: "date",
2706
+ required: false
2707
+ },
2708
+ scopes: {
2709
+ type: "string[]",
2710
+ required: true
2711
+ }
2712
+ } },
2713
+ oauthAccessToken: {
2714
+ modelName: "oauthAccessToken",
2715
+ fields: {
2716
+ token: {
2717
+ type: "string",
2718
+ unique: true
2719
+ },
2720
+ clientId: {
2721
+ type: "string",
2722
+ required: true,
2723
+ references: {
2724
+ model: "oauthClient",
2725
+ field: "clientId"
2726
+ }
2727
+ },
2728
+ sessionId: {
2729
+ type: "string",
2730
+ required: false,
2731
+ references: {
2732
+ model: "session",
2733
+ field: "id",
2734
+ onDelete: "set null"
2735
+ }
2736
+ },
2737
+ userId: {
2738
+ type: "string",
2739
+ required: false,
2740
+ references: {
2741
+ model: "user",
2742
+ field: "id"
2743
+ }
2744
+ },
2745
+ referenceId: {
2746
+ type: "string",
2747
+ required: false
2748
+ },
2749
+ refreshId: {
2750
+ type: "string",
2751
+ required: false,
2752
+ references: {
2753
+ model: "oauthRefreshToken",
2754
+ field: "id"
2755
+ }
2756
+ },
2757
+ expiresAt: { type: "date" },
2758
+ createdAt: { type: "date" },
2759
+ scopes: {
2760
+ type: "string[]",
2761
+ required: true
2762
+ }
2763
+ }
2764
+ },
2765
+ oauthConsent: {
2766
+ modelName: "oauthConsent",
2767
+ fields: {
2768
+ clientId: {
2769
+ type: "string",
2770
+ required: true,
2771
+ references: {
2772
+ model: "oauthClient",
2773
+ field: "clientId"
2774
+ }
2775
+ },
2776
+ userId: {
2777
+ type: "string",
2778
+ required: false,
2779
+ references: {
2780
+ model: "user",
2781
+ field: "id"
2782
+ }
2783
+ },
2784
+ referenceId: {
2785
+ type: "string",
2786
+ required: false
2787
+ },
2788
+ scopes: {
2789
+ type: "string[]",
2790
+ required: true
2791
+ },
2792
+ createdAt: { type: "date" },
2793
+ updatedAt: { type: "date" }
2794
+ }
2795
+ }
2796
+ };
2797
+
2798
+ //#endregion
2799
+ //#region src/oauth.ts
2800
+ const oAuthState = defineRequestState(() => null);
2801
+ /**
2802
+ * oAuth 2.1 provider plugin for Better Auth.
2803
+ *
2804
+ * @see https://better-auth.com/docs/plugins/oauth-provider
2805
+ * @param options - The options for the oAuth Provider plugin.
2806
+ * @returns A Better Auth plugin.
2807
+ */
2808
+ const oauthProvider = (options) => {
2809
+ let clientRegistrationAllowedScopes = options.clientRegistrationAllowedScopes;
2810
+ if (options.clientRegistrationDefaultScopes) {
2811
+ const _allowedScopes = clientRegistrationAllowedScopes ? new Set([...clientRegistrationAllowedScopes, ...options.clientRegistrationDefaultScopes]) : new Set([...options.clientRegistrationDefaultScopes]);
2812
+ clientRegistrationAllowedScopes = Array.from(_allowedScopes);
2813
+ }
2814
+ const scopes = new Set((options.scopes ?? [
2815
+ "openid",
2816
+ "profile",
2817
+ "email",
2818
+ "offline_access"
2819
+ ]).filter((val) => val.length));
2820
+ if (clientRegistrationAllowedScopes) {
2821
+ for (const sc of clientRegistrationAllowedScopes) if (!scopes.has(sc)) throw new BetterAuthError(`clientRegistrationAllowedScope ${sc} not found in scopes`);
2822
+ }
2823
+ for (const sc of options.advertisedMetadata?.scopes_supported ?? []) if (!scopes?.has(sc)) throw new BetterAuthError(`advertisedMetadata.scopes_supported ${sc} not found in scopes`);
2824
+ const claims = new Set([
2825
+ "sub",
2826
+ "iss",
2827
+ "aud",
2828
+ "exp",
2829
+ "iat",
2830
+ "sid",
2831
+ "scope",
2832
+ "azp",
2833
+ ...scopes.has("email") ? ["email", "email_verified"] : [],
2834
+ ...scopes.has("profile") ? [
2835
+ "name",
2836
+ "picture",
2837
+ "family_name",
2838
+ "given_name"
2839
+ ] : []
2840
+ ]);
2841
+ const opts = {
2842
+ codeExpiresIn: 600,
2843
+ accessTokenExpiresIn: 3600,
2844
+ m2mAccessTokenExpiresIn: 3600,
2845
+ refreshTokenExpiresIn: 2592e3,
2846
+ allowUnauthenticatedClientRegistration: false,
2847
+ allowDynamicClientRegistration: false,
2848
+ disableJwtPlugin: false,
2849
+ storeClientSecret: options.disableJwtPlugin ? "encrypted" : "hashed",
2850
+ storeTokens: "hashed",
2851
+ grantTypes: [
2852
+ "authorization_code",
2853
+ "client_credentials",
2854
+ "refresh_token"
2855
+ ],
2856
+ ...options,
2857
+ scopes: Array.from(scopes),
2858
+ claims: Array.from(claims),
2859
+ clientRegistrationAllowedScopes
2860
+ };
2861
+ if (opts.grantTypes && opts.grantTypes.includes("refresh_token") && !opts.grantTypes.includes("authorization_code")) throw new BetterAuthError("refresh_token grant requires authorization_code grant");
2862
+ 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");
2863
+ if (!opts.disableJwtPlugin && (opts.storeClientSecret === "encrypted" || typeof opts.storeClientSecret === "object" && ("encrypt" in opts.storeClientSecret || "decrypt" in opts.storeClientSecret))) throw new BetterAuthError("encryption method not recommended, please use 'hashed' or the 'hash' function");
2864
+ return {
2865
+ id: "oauth-provider",
2866
+ options: opts,
2867
+ init: (ctx) => {
2868
+ if (ctx.options.session && !ctx.options.session.storeSessionInDatabase) throw new BetterAuthError("OAuth Provider requires `session.storeSessionInDatabase: true` when using secondaryStorage");
2869
+ if (!opts.disableJwtPlugin) {
2870
+ const issuer = (getJwtPlugin(ctx)?.options)?.jwt?.issuer ?? ctx.baseURL;
2871
+ const issuerPath = new URL(issuer).pathname;
2872
+ if (!opts.silenceWarnings?.oauthAuthServerConfig && !(ctx.options.basePath === "/" && issuerPath === "/")) logger.warn(`Please ensure '/.well-known/oauth-authorization-server${issuerPath === "/" ? "" : issuerPath}' exists. Upon completion, clear with silenceWarnings.oauthAuthServerConfig.`);
2873
+ if (!opts.silenceWarnings?.openidConfig && ctx.options.basePath !== issuerPath && opts.scopes?.includes("openid")) logger.warn(`Please ensure '${issuerPath}${issuerPath.endsWith("/") ? "" : "/"}.well-known/openid-configuration' exists. Upon completion, clear with silenceWarnings.openidConfig.`);
2874
+ }
2875
+ },
2876
+ hooks: {
2877
+ before: [{
2878
+ matcher(ctx) {
2879
+ return ctx.body?.oauth_query;
2880
+ },
2881
+ handler: createAuthMiddleware(async (ctx) => {
2882
+ const query = ctx.body.oauth_query;
2883
+ let queryParams = new URLSearchParams(query);
2884
+ const sig = queryParams.get("sig");
2885
+ const exp = Number(queryParams.get("exp"));
2886
+ queryParams.delete("sig");
2887
+ queryParams = new URLSearchParams(queryParams);
2888
+ const verifySig = await makeSignature(queryParams.toString(), ctx.context.secret);
2889
+ if (!sig || !constantTimeEqual(sig, verifySig) || /* @__PURE__ */ new Date(exp * 1e3) < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", { error: "invalid_signature" });
2890
+ queryParams.delete("exp");
2891
+ await oAuthState.set({ query: new URLSearchParams(queryParams).toString() });
2892
+ if (ctx.path === "/sign-in/social" || ctx.path === "/sign-in/oauth2") {
2893
+ if (ctx.body.additionalData?.query) return;
2894
+ if (!ctx.body.additionalData) ctx.body.additionalData = {};
2895
+ ctx.body.additionalData.query = queryParams.toString();
2896
+ }
2897
+ })
2898
+ }],
2899
+ after: [{
2900
+ matcher(ctx) {
2901
+ return parseSetCookieHeader(ctx.context.responseHeaders?.get("set-cookie") || "").has(ctx.context.authCookies.sessionToken.name);
2902
+ },
2903
+ handler: createAuthMiddleware(async (ctx) => {
2904
+ const sessionToken = parseSetCookieHeader(ctx.context.responseHeaders?.get("set-cookie") || "").get(ctx.context.authCookies.sessionToken.name)?.value.split(".")[0];
2905
+ if (!sessionToken) return;
2906
+ const _query = (await oAuthState.get())?.query ?? (await getOAuthState())?.query;
2907
+ if (!_query) return;
2908
+ const query = new URLSearchParams(_query);
2909
+ const session = await ctx.context.internalAdapter.findSession(sessionToken);
2910
+ if (!session) return;
2911
+ ctx.context.session = session;
2912
+ ctx.query = deleteFromPrompt(query, "login");
2913
+ return await authorizeEndpoint(ctx, opts);
2914
+ })
2915
+ }]
2916
+ },
2917
+ endpoints: {
2918
+ getOAuthServerConfig: createAuthEndpoint("/.well-known/oauth-authorization-server", {
2919
+ method: "GET",
2920
+ metadata: { SERVER_ONLY: true }
2921
+ }, async (ctx) => {
2922
+ if (opts.scopes && opts.scopes.includes("openid")) return oidcServerMetadata(ctx, opts);
2923
+ else return authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, { scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes });
2924
+ }),
2925
+ getOpenIdConfig: createAuthEndpoint("/.well-known/openid-configuration", {
2926
+ method: "GET",
2927
+ metadata: { SERVER_ONLY: true }
2928
+ }, async (ctx) => {
2929
+ if (opts.scopes && !opts.scopes.includes("openid")) throw new APIError("NOT_FOUND");
2930
+ return oidcServerMetadata(ctx, opts);
2931
+ }),
2932
+ oauth2Authorize: createAuthEndpoint("/oauth2/authorize", {
2933
+ method: "GET",
2934
+ query: z.object({
2935
+ response_type: z.enum(["code"]),
2936
+ client_id: z.string(),
2937
+ redirect_uri: SafeUrlSchema.optional(),
2938
+ scope: z.string().optional(),
2939
+ state: z.string().optional(),
2940
+ code_challenge: z.string().optional(),
2941
+ code_challenge_method: z.enum(["S256"]).optional(),
2942
+ nonce: z.string().optional(),
2943
+ prompt: z.enum([
2944
+ "consent",
2945
+ "login",
2946
+ "create",
2947
+ "select_account",
2948
+ "login consent",
2949
+ "select_account consent"
2950
+ ]).optional()
2951
+ }),
2952
+ metadata: { openapi: {
2953
+ description: "Authorize an OAuth2 request",
2954
+ parameters: [
2955
+ {
2956
+ name: "response_type",
2957
+ in: "query",
2958
+ required: true,
2959
+ schema: { type: "string" },
2960
+ description: "OAuth2 response type (e.g., 'code')"
2961
+ },
2962
+ {
2963
+ name: "client_id",
2964
+ in: "query",
2965
+ required: true,
2966
+ schema: { type: "string" },
2967
+ description: "OAuth2 client ID"
2968
+ },
2969
+ {
2970
+ name: "redirect_uri",
2971
+ in: "query",
2972
+ required: false,
2973
+ schema: {
2974
+ type: "string",
2975
+ format: "uri"
2976
+ },
2977
+ description: "OAuth2 redirect URI"
2978
+ },
2979
+ {
2980
+ name: "scope",
2981
+ in: "query",
2982
+ required: false,
2983
+ schema: { type: "string" },
2984
+ description: "OAuth2 scopes (space-separated)"
2985
+ },
2986
+ {
2987
+ name: "state",
2988
+ in: "query",
2989
+ required: false,
2990
+ schema: { type: "string" },
2991
+ description: "OAuth2 state parameter"
2992
+ },
2993
+ {
2994
+ name: "code_challenge",
2995
+ in: "query",
2996
+ required: false,
2997
+ schema: { type: "string" },
2998
+ description: "PKCE code challenge"
2999
+ },
3000
+ {
3001
+ name: "code_challenge_method",
3002
+ in: "query",
3003
+ required: false,
3004
+ schema: { type: "string" },
3005
+ description: "PKCE code challenge method"
3006
+ },
3007
+ {
3008
+ name: "nonce",
3009
+ in: "query",
3010
+ required: false,
3011
+ schema: { type: "string" },
3012
+ description: "OpenID Connect nonce"
3013
+ },
3014
+ {
3015
+ name: "prompt",
3016
+ in: "query",
3017
+ required: false,
3018
+ schema: { type: "string" },
3019
+ description: "OAuth2 prompt parameter"
3020
+ }
3021
+ ],
3022
+ responses: {
3023
+ "302": {
3024
+ description: "Redirect to client with code or error",
3025
+ headers: { Location: {
3026
+ description: "Redirect URI with code or error",
3027
+ schema: {
3028
+ type: "string",
3029
+ format: "uri"
3030
+ }
3031
+ } }
3032
+ },
3033
+ "400": {
3034
+ description: "Invalid request",
3035
+ content: { "application/json": { schema: {
3036
+ type: "object",
3037
+ properties: {
3038
+ error: { type: "string" },
3039
+ error_description: { type: "string" },
3040
+ state: { type: "string" }
3041
+ },
3042
+ required: ["error"]
3043
+ } } }
3044
+ }
3045
+ }
3046
+ } }
3047
+ }, async (ctx) => {
3048
+ return authorizeEndpoint(ctx, opts, { isAuthorize: true });
3049
+ }),
3050
+ oauth2Consent: createAuthEndpoint("/oauth2/consent", {
3051
+ method: "POST",
3052
+ body: z.object({
3053
+ accept: z.boolean().meta({ description: "Accept or deny user consent for a set of scopes" }),
3054
+ scope: z.string().optional().meta({ description: "List of accept of accepted space-separated scopes. If none is provided, then all originally requested scopes are accepted." }),
3055
+ oauth_query: z.string().optional().meta({ description: "The redirected page's query parameters" })
3056
+ }),
3057
+ use: [sessionMiddleware],
3058
+ metadata: { openapi: {
3059
+ description: "Handle OAuth2 consent",
3060
+ responses: { "200": {
3061
+ description: "Consent processed successfully",
3062
+ content: { "application/json": { schema: {
3063
+ type: "object",
3064
+ properties: { redirect_uri: {
3065
+ type: "string",
3066
+ format: "uri",
3067
+ description: "The URI to redirect to, either with an authorization code or an error"
3068
+ } },
3069
+ required: ["redirect_uri"]
3070
+ } } }
3071
+ } }
3072
+ } }
3073
+ }, async (ctx) => {
3074
+ return consentEndpoint(ctx, opts);
3075
+ }),
3076
+ oauth2Continue: createAuthEndpoint("/oauth2/continue", {
3077
+ method: "POST",
3078
+ body: z.object({
3079
+ selected: z.boolean().optional().meta({ description: "Confirms an account has been selected and authorization can proceed." }),
3080
+ created: z.boolean().optional().meta({ description: "Confirms an account was registered" }),
3081
+ postLogin: z.boolean().optional().meta({ description: "Confirms organization and/or team selection." }),
3082
+ oauth_query: z.string().optional().meta({ description: "The redirected page's query parameters" })
3083
+ }),
3084
+ use: [sessionMiddleware],
3085
+ metadata: { openapi: {
3086
+ description: "Continues OAuth2 authorization flow",
3087
+ responses: { "200": {
3088
+ description: "Consent processed successfully",
3089
+ content: { "application/json": { schema: {
3090
+ type: "object",
3091
+ properties: { redirect_uri: {
3092
+ type: "string",
3093
+ format: "uri",
3094
+ description: "The URI to redirect to, either with an authorization code or an error"
3095
+ } },
3096
+ required: ["redirect_uri"]
3097
+ } } }
3098
+ } }
3099
+ } }
3100
+ }, async (ctx) => {
3101
+ return continueEndpoint(ctx, opts);
3102
+ }),
3103
+ oauth2Token: createAuthEndpoint("/oauth2/token", {
3104
+ method: "POST",
3105
+ body: z.object({
3106
+ grant_type: z.enum([
3107
+ "authorization_code",
3108
+ "client_credentials",
3109
+ "refresh_token"
3110
+ ]),
3111
+ client_id: z.string().optional(),
3112
+ client_secret: z.string().optional(),
3113
+ code: z.string().optional(),
3114
+ code_verifier: z.string().optional(),
3115
+ redirect_uri: SafeUrlSchema.optional(),
3116
+ refresh_token: z.string().optional(),
3117
+ resource: z.string().optional(),
3118
+ scope: z.string().optional()
3119
+ }),
3120
+ metadata: {
3121
+ allowedMediaTypes: ["application/x-www-form-urlencoded"],
3122
+ openapi: {
3123
+ description: "Obtain an OAuth2.1 access token",
3124
+ requestBody: {
3125
+ required: true,
3126
+ content: { "application/json": { schema: {
3127
+ type: "object",
3128
+ properties: {
3129
+ grant_type: {
3130
+ type: "string",
3131
+ enum: [
3132
+ "authorization_code",
3133
+ "client_credentials",
3134
+ "refresh_token"
3135
+ ],
3136
+ description: "OAuth2 grant type"
3137
+ },
3138
+ client_id: {
3139
+ type: "string",
3140
+ description: "OAuth2 client ID"
3141
+ },
3142
+ client_secret: {
3143
+ type: "string",
3144
+ description: "OAuth2 client secret"
3145
+ },
3146
+ code: {
3147
+ type: "string",
3148
+ description: "Authorization code (for authorization_code grant)"
3149
+ },
3150
+ code_verifier: {
3151
+ type: "string",
3152
+ description: "PKCE code verifier (for authorization_code grant)"
3153
+ },
3154
+ redirect_uri: {
3155
+ type: "string",
3156
+ format: "uri",
3157
+ description: "Redirect URI (for authorization_code grant)"
3158
+ },
3159
+ refresh_token: {
3160
+ type: "string",
3161
+ description: "Refresh token (for refresh_token grant)"
3162
+ },
3163
+ resource: {
3164
+ type: "string",
3165
+ description: "Requested token resource (ie audience) to obtain a JWT formatted access token"
3166
+ },
3167
+ scope: {
3168
+ type: "string",
3169
+ description: "Requested scopes (for client_credentials grant)"
3170
+ }
3171
+ },
3172
+ required: ["grant_type"]
3173
+ } } }
3174
+ },
3175
+ responses: {
3176
+ "200": {
3177
+ description: "Access token response",
3178
+ content: { "application/json": { schema: {
3179
+ type: "object",
3180
+ properties: {
3181
+ access_token: {
3182
+ type: "string",
3183
+ description: "The access token issued by the authorization server"
3184
+ },
3185
+ token_type: {
3186
+ type: "string",
3187
+ description: "The type of the token issued",
3188
+ enum: ["Bearer"]
3189
+ },
3190
+ expires_in: {
3191
+ type: "number",
3192
+ description: "Lifetime in seconds of the access token"
3193
+ },
3194
+ refresh_token: {
3195
+ type: "string",
3196
+ description: "Refresh token, if issued"
3197
+ },
3198
+ scope: {
3199
+ type: "string",
3200
+ description: "Scopes granted by the access token"
3201
+ },
3202
+ id_token: {
3203
+ type: "string",
3204
+ description: "ID Token (if OpenID Connect)"
3205
+ }
3206
+ },
3207
+ required: [
3208
+ "access_token",
3209
+ "token_type",
3210
+ "expires_in"
3211
+ ]
3212
+ } } }
3213
+ },
3214
+ "400": {
3215
+ description: "Invalid request or error response",
3216
+ content: { "application/json": { schema: {
3217
+ type: "object",
3218
+ properties: {
3219
+ error: { type: "string" },
3220
+ error_description: { type: "string" },
3221
+ error_uri: { type: "string" }
3222
+ },
3223
+ required: ["error"]
3224
+ } } }
3225
+ }
3226
+ }
3227
+ }
3228
+ }
3229
+ }, async (ctx) => {
3230
+ return tokenEndpoint(ctx, opts);
3231
+ }),
3232
+ oauth2Introspect: createAuthEndpoint("/oauth2/introspect", {
3233
+ method: "POST",
3234
+ body: z.object({
3235
+ client_id: z.string().optional(),
3236
+ client_secret: z.string().optional(),
3237
+ token: z.string(),
3238
+ token_type_hint: z.enum(["access_token", "refresh_token"]).optional()
3239
+ }),
3240
+ metadata: {
3241
+ allowedMediaTypes: ["application/x-www-form-urlencoded"],
3242
+ openapi: {
3243
+ description: "Introspect an OAuth2 access or refresh token",
3244
+ requestBody: {
3245
+ required: true,
3246
+ content: { "application/json": { schema: {
3247
+ type: "object",
3248
+ properties: {
3249
+ client_id: {
3250
+ type: "string",
3251
+ description: "OAuth2 client ID"
3252
+ },
3253
+ client_secret: {
3254
+ type: "string",
3255
+ description: "OAuth2 client secret"
3256
+ },
3257
+ token: {
3258
+ type: "string",
3259
+ description: "The token to introspect (access or refresh token)"
3260
+ },
3261
+ token_type_hint: {
3262
+ type: "string",
3263
+ enum: ["access_token", "refresh_token"],
3264
+ description: "Hint about the type of the token submitted for introspection"
3265
+ },
3266
+ resource: {
3267
+ type: "string",
3268
+ description: "Introspects a token for a specific resource."
3269
+ }
3270
+ },
3271
+ required: ["token"]
3272
+ } } }
3273
+ },
3274
+ responses: {
3275
+ "200": {
3276
+ description: "Token introspection response",
3277
+ content: { "application/json": { schema: {
3278
+ type: "object",
3279
+ properties: {
3280
+ active: {
3281
+ type: "boolean",
3282
+ description: "Whether the token is active"
3283
+ },
3284
+ scope: {
3285
+ type: "string",
3286
+ description: "Scopes associated with the token"
3287
+ },
3288
+ client_id: {
3289
+ type: "string",
3290
+ description: "Client ID associated with the token"
3291
+ },
3292
+ username: {
3293
+ type: "string",
3294
+ description: "Username associated with the token"
3295
+ },
3296
+ token_type: {
3297
+ type: "string",
3298
+ description: "Type of the token"
3299
+ },
3300
+ exp: {
3301
+ type: "number",
3302
+ description: "Expiration time of the token (seconds since epoch)"
3303
+ },
3304
+ iat: {
3305
+ type: "number",
3306
+ description: "Issued at time (seconds since epoch)"
3307
+ },
3308
+ nbf: {
3309
+ type: "number",
3310
+ description: "Not before time (seconds since epoch)"
3311
+ },
3312
+ sub: {
3313
+ type: "string",
3314
+ description: "Subject of the token"
3315
+ },
3316
+ aud: {
3317
+ type: "string",
3318
+ description: "Audience of the token"
3319
+ },
3320
+ iss: {
3321
+ type: "string",
3322
+ description: "Issuer of the token"
3323
+ },
3324
+ jti: {
3325
+ type: "string",
3326
+ description: "JWT ID"
3327
+ }
3328
+ },
3329
+ required: ["active"]
3330
+ } } }
3331
+ },
3332
+ "400": {
3333
+ description: "Invalid request or error response",
3334
+ content: { "application/json": { schema: {
3335
+ type: "object",
3336
+ properties: {
3337
+ error: { type: "string" },
3338
+ error_description: { type: "string" },
3339
+ error_uri: { type: "string" }
3340
+ },
3341
+ required: ["error"]
3342
+ } } }
3343
+ }
3344
+ }
3345
+ }
3346
+ }
3347
+ }, async (ctx) => {
3348
+ return introspectEndpoint(ctx, opts);
3349
+ }),
3350
+ oauth2Revoke: createAuthEndpoint("/oauth2/revoke", {
3351
+ method: "POST",
3352
+ body: z.object({
3353
+ client_id: z.string().optional(),
3354
+ client_secret: z.string().optional(),
3355
+ token: z.string(),
3356
+ token_type_hint: z.enum(["access_token", "refresh_token"]).optional()
3357
+ }),
3358
+ metadata: {
3359
+ allowedMediaTypes: ["application/x-www-form-urlencoded"],
3360
+ openapi: {
3361
+ description: "Revoke an OAuth2 access or refresh token",
3362
+ requestBody: {
3363
+ required: true,
3364
+ content: { "application/json": { schema: {
3365
+ type: "object",
3366
+ properties: {
3367
+ client_id: {
3368
+ type: "string",
3369
+ description: "OAuth2 client ID"
3370
+ },
3371
+ client_secret: {
3372
+ type: "string",
3373
+ description: "OAuth2 client secret"
3374
+ },
3375
+ token: {
3376
+ type: "string",
3377
+ description: "The token to revoke (access or refresh token)"
3378
+ },
3379
+ token_type_hint: {
3380
+ type: "string",
3381
+ enum: ["access_token", "refresh_token"],
3382
+ description: "Hint about the type of the token submitted for revocation"
3383
+ }
3384
+ },
3385
+ required: ["token"]
3386
+ } } }
3387
+ },
3388
+ responses: {
3389
+ "200": {
3390
+ description: "Token revoked successfully. The response body is empty.",
3391
+ content: { "application/json": { schema: {
3392
+ type: "object",
3393
+ description: "Empty object on success"
3394
+ } } }
3395
+ },
3396
+ "400": {
3397
+ description: "Invalid request or error response",
3398
+ content: { "application/json": { schema: {
3399
+ type: "object",
3400
+ properties: {
3401
+ error: { type: "string" },
3402
+ error_description: { type: "string" },
3403
+ error_uri: { type: "string" }
3404
+ },
3405
+ required: ["error"]
3406
+ } } }
3407
+ }
3408
+ }
3409
+ }
3410
+ }
3411
+ }, async (ctx) => {
3412
+ return revokeEndpoint(ctx, opts);
3413
+ }),
3414
+ oauth2UserInfo: createAuthEndpoint("/oauth2/userinfo", {
3415
+ method: "GET",
3416
+ metadata: { openapi: {
3417
+ description: "Get OpenID Connect user information (UserInfo endpoint)",
3418
+ security: [{ bearerAuth: [] }, { OAuth2: [
3419
+ "openid",
3420
+ "profile",
3421
+ "email"
3422
+ ] }],
3423
+ parameters: [{
3424
+ name: "Authorization",
3425
+ in: "header",
3426
+ required: false,
3427
+ schema: { type: "string" },
3428
+ description: "Bearer access token"
3429
+ }],
3430
+ responses: {
3431
+ "200": {
3432
+ description: "User information retrieved successfully",
3433
+ content: { "application/json": { schema: {
3434
+ type: "object",
3435
+ properties: {
3436
+ sub: {
3437
+ type: "string",
3438
+ description: "Subject identifier (user ID)"
3439
+ },
3440
+ email: {
3441
+ type: "string",
3442
+ format: "email",
3443
+ nullable: true,
3444
+ description: "User's email address, included if 'email' scope is granted"
3445
+ },
3446
+ name: {
3447
+ type: "string",
3448
+ nullable: true,
3449
+ description: "User's full name, included if 'profile' scope is granted"
3450
+ },
3451
+ picture: {
3452
+ type: "string",
3453
+ format: "uri",
3454
+ nullable: true,
3455
+ description: "User's profile picture URL, included if 'profile' scope is granted"
3456
+ },
3457
+ given_name: {
3458
+ type: "string",
3459
+ nullable: true,
3460
+ description: "User's given name, included if 'profile' scope is granted"
3461
+ },
3462
+ family_name: {
3463
+ type: "string",
3464
+ nullable: true,
3465
+ description: "User's family name, included if 'profile' scope is granted"
3466
+ },
3467
+ email_verified: {
3468
+ type: "boolean",
3469
+ nullable: true,
3470
+ description: "Whether the email is verified, included if 'email' scope is granted"
3471
+ }
3472
+ },
3473
+ required: ["sub"]
3474
+ } } }
3475
+ },
3476
+ "401": {
3477
+ description: "Unauthorized - invalid or missing access token",
3478
+ content: { "application/json": { schema: {
3479
+ type: "object",
3480
+ properties: {
3481
+ error: { type: "string" },
3482
+ error_description: { type: "string" }
3483
+ },
3484
+ required: ["error"]
3485
+ } } }
3486
+ },
3487
+ "403": {
3488
+ description: "Forbidden - insufficient scope",
3489
+ content: { "application/json": { schema: {
3490
+ type: "object",
3491
+ properties: {
3492
+ error: { type: "string" },
3493
+ error_description: { type: "string" }
3494
+ },
3495
+ required: ["error"]
3496
+ } } }
3497
+ }
3498
+ }
3499
+ } }
3500
+ }, async (ctx) => {
3501
+ return userInfoEndpoint(ctx, opts);
3502
+ }),
3503
+ oauth2EndSession: createAuthEndpoint("/oauth2/end-session", {
3504
+ method: "GET",
3505
+ query: z.object({
3506
+ id_token_hint: z.string(),
3507
+ client_id: z.string().optional(),
3508
+ post_logout_redirect_uri: SafeUrlSchema.optional(),
3509
+ state: z.string().optional()
3510
+ }),
3511
+ metadata: { openapi: {
3512
+ description: "RP-Initiated Logout endpoint. Allows clients to notify the OP that the End-User has logged out.",
3513
+ responses: { "200": {
3514
+ description: "Logout successful. May include redirect_uri if post_logout_redirect_uri was provided.",
3515
+ content: { "application/json": { schema: {
3516
+ type: "object",
3517
+ properties: {
3518
+ redirect_uri: {
3519
+ type: "string",
3520
+ format: "uri",
3521
+ description: "URI to redirect to after logout (if post_logout_redirect_uri was provided)"
3522
+ },
3523
+ message: {
3524
+ type: "string",
3525
+ description: "Success message"
3526
+ }
3527
+ }
3528
+ } } }
3529
+ } }
3530
+ } }
3531
+ }, async (ctx) => {
3532
+ return rpInitiatedLogoutEndpoint(ctx, opts);
3533
+ }),
3534
+ registerOAuthClient: createAuthEndpoint("/oauth2/register", {
3535
+ method: "POST",
3536
+ body: z.object({
3537
+ redirect_uris: z.array(SafeUrlSchema).min(1).min(1),
3538
+ scope: z.string().optional(),
3539
+ client_name: z.string().optional(),
3540
+ client_uri: z.string().optional(),
3541
+ logo_uri: z.string().optional(),
3542
+ contacts: z.array(z.string().min(1)).min(1).optional(),
3543
+ tos_uri: z.string().optional(),
3544
+ policy_uri: z.string().optional(),
3545
+ software_id: z.string().optional(),
3546
+ software_version: z.string().optional(),
3547
+ software_statement: z.string().optional(),
3548
+ post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
3549
+ token_endpoint_auth_method: z.enum([
3550
+ "none",
3551
+ "client_secret_basic",
3552
+ "client_secret_post"
3553
+ ]).default("client_secret_basic").optional(),
3554
+ grant_types: z.array(z.enum([
3555
+ "authorization_code",
3556
+ "client_credentials",
3557
+ "refresh_token"
3558
+ ])).default(["authorization_code"]).optional(),
3559
+ response_types: z.array(z.enum(["code"])).default(["code"]).optional(),
3560
+ type: z.enum([
3561
+ "web",
3562
+ "native",
3563
+ "user-agent-based"
3564
+ ]).optional()
3565
+ }),
3566
+ metadata: { openapi: {
3567
+ description: "Register an OAuth2 application",
3568
+ responses: { "200": {
3569
+ description: "OAuth2 application registered successfully",
3570
+ content: { "application/json": { schema: {
3571
+ type: "object",
3572
+ properties: {
3573
+ client_id: {
3574
+ type: "string",
3575
+ description: "Unique identifier for the client"
3576
+ },
3577
+ client_secret: {
3578
+ type: "string",
3579
+ description: "Secret key for the client"
3580
+ },
3581
+ client_secret_expires_at: {
3582
+ type: "number",
3583
+ description: "Time the client secret will expire. If 0, the client secret will never expire."
3584
+ },
3585
+ scope: {
3586
+ type: "string",
3587
+ description: "Space-separated scopes allowed by the client"
3588
+ },
3589
+ user_id: {
3590
+ type: "string",
3591
+ description: "ID of the user who registered the client, null if registered anonymously"
3592
+ },
3593
+ client_id_issued_at: {
3594
+ type: "number",
3595
+ description: "Creation timestamp of this client"
3596
+ },
3597
+ client_name: {
3598
+ type: "string",
3599
+ description: "Name of the OAuth2 application"
3600
+ },
3601
+ client_uri: {
3602
+ type: "string",
3603
+ description: "Name of the OAuth2 application"
3604
+ },
3605
+ logo_uri: {
3606
+ type: "string",
3607
+ description: "Icon URL for the application"
3608
+ },
3609
+ contacts: {
3610
+ type: "array",
3611
+ items: { type: "string" },
3612
+ description: "List representing ways to contact people responsible for this client, typically email addresses"
3613
+ },
3614
+ tos_uri: {
3615
+ type: "string",
3616
+ description: "Client's terms of service uri"
3617
+ },
3618
+ policy_uri: {
3619
+ type: "string",
3620
+ description: "Client's policy uri"
3621
+ },
3622
+ software_id: {
3623
+ type: "string",
3624
+ description: "Unique identifier assigned by the developer to help in the dynamic registration process"
3625
+ },
3626
+ software_version: {
3627
+ type: "string",
3628
+ description: "Version identifier for the software_id"
3629
+ },
3630
+ software_statement: {
3631
+ type: "string",
3632
+ description: "JWT containing metadata values about the client software as claims"
3633
+ },
3634
+ redirect_uris: {
3635
+ type: "array",
3636
+ items: {
3637
+ type: "string",
3638
+ format: "uri"
3639
+ },
3640
+ description: "List of allowed redirect uris"
3641
+ },
3642
+ post_logout_redirect_uris: {
3643
+ type: "array",
3644
+ items: {
3645
+ type: "string",
3646
+ format: "uri"
3647
+ },
3648
+ description: "List of allowed logout redirect uris"
3649
+ },
3650
+ token_endpoint_auth_method: {
3651
+ type: "string",
3652
+ description: "Requested authentication method for the token endpoint",
3653
+ enum: [
3654
+ "none",
3655
+ "client_secret_basic",
3656
+ "client_secret_post"
3657
+ ]
3658
+ },
3659
+ grant_types: {
3660
+ type: "array",
3661
+ items: {
3662
+ type: "string",
3663
+ enum: [
3664
+ "authorization_code",
3665
+ "client_credentials",
3666
+ "refresh_token"
3667
+ ]
3668
+ },
3669
+ description: "Requested authentication method for the token endpoint"
3670
+ },
3671
+ response_types: {
3672
+ type: "array",
3673
+ items: {
3674
+ type: "string",
3675
+ enum: ["code"]
3676
+ },
3677
+ description: "Requested authentication method for the token endpoint"
3678
+ },
3679
+ public: {
3680
+ type: "boolean",
3681
+ description: "Whether the client is public as determined by the type"
3682
+ },
3683
+ type: {
3684
+ type: "string",
3685
+ description: "Type of the client",
3686
+ enum: [
3687
+ "web",
3688
+ "native",
3689
+ "user-agent-based"
3690
+ ]
3691
+ },
3692
+ disabled: {
3693
+ type: "boolean",
3694
+ description: "Whether the client is disabled"
3695
+ }
3696
+ },
3697
+ required: ["client_id"]
3698
+ } } }
3699
+ } }
3700
+ } }
3701
+ }, async (ctx) => {
3702
+ return registerEndpoint(ctx, opts);
3703
+ }),
3704
+ adminCreateOAuthClient: adminCreateOAuthClient(opts),
3705
+ createOAuthClient: createOAuthClient(opts),
3706
+ getOAuthClient: getOAuthClient(opts),
3707
+ getOAuthClientPublic: getOAuthClientPublic(opts),
3708
+ getOAuthClients: getOAuthClients(opts),
3709
+ adminUpdateOAuthClient: adminUpdateOAuthClient(opts),
3710
+ updateOAuthClient: updateOAuthClient(opts),
3711
+ rotateClientSecret: rotateClientSecret(opts),
3712
+ deleteOAuthClient: deleteOAuthClient(opts),
3713
+ getOAuthConsent: getOAuthConsent(opts),
3714
+ getOAuthConsents: getOAuthConsents(opts),
3715
+ updateOAuthConsent: updateOAuthConsent(opts),
3716
+ deleteOAuthConsent: deleteOAuthConsent(opts)
3717
+ },
3718
+ schema: mergeSchema(schema, opts?.schema)
3719
+ };
3720
+ };
3721
+
3722
+ //#endregion
3723
+ export { authServerMetadata, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
3724
+ //# sourceMappingURL=index.mjs.map