better-auth
Version:
The most comprehensive authentication framework for TypeScript.
1 lines • 84.5 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":["consentCode: string | null","accessToken","idToken: string","client: Client","validatedClientId: string | null","validatedUserId: string | null"],"sources":["../../../src/plugins/oidc-provider/index.ts"],"sourcesContent":["import type {\n\tBetterAuthPlugin,\n\tGenericEndpointContext,\n} from \"@better-auth/core\";\nimport {\n\tcreateAuthEndpoint,\n\tcreateAuthMiddleware,\n} from \"@better-auth/core/api\";\nimport { getCurrentAuthContext } from \"@better-auth/core/context\";\nimport { base64 } from \"@better-auth/utils/base64\";\nimport { createHash } from \"@better-auth/utils/hash\";\nimport type { OpenAPIParameter } from \"better-call\";\nimport { jwtVerify, SignJWT } from \"jose\";\nimport * as z from \"zod\";\nimport { APIError, getSessionFromCtx, sessionMiddleware } from \"../../api\";\nimport { parseSetCookieHeader } from \"../../cookies\";\nimport {\n\tgenerateRandomString,\n\tsymmetricDecrypt,\n\tsymmetricEncrypt,\n} from \"../../crypto\";\nimport { mergeSchema } from \"../../db\";\nimport { HIDE_METADATA } from \"../../utils\";\nimport type { jwt } from \"../jwt\";\nimport { getJwtToken, verifyJWT } from \"../jwt\";\nimport { authorize } from \"./authorize\";\nimport type { OAuthApplication } from \"./schema\";\nimport { schema } from \"./schema\";\nimport type {\n\tClient,\n\tCodeVerificationValue,\n\tOAuthAccessToken,\n\tOIDCMetadata,\n\tOIDCOptions,\n} from \"./types\";\nimport { defaultClientSecretHasher } from \"./utils\";\nimport { parsePrompt } from \"./utils/prompt\";\n\nconst getJwtPlugin = (ctx: GenericEndpointContext) => {\n\treturn ctx.context.options.plugins?.find(\n\t\t(plugin) => plugin.id === \"jwt\",\n\t) as ReturnType<typeof jwt>;\n};\n\n/**\n * Get a client by ID, checking trusted clients first, then database\n */\nexport async function getClient(\n\tclientId: string,\n\ttrustedClients: (Client & { skipConsent?: boolean | undefined })[] = [],\n): Promise<(Client & { skipConsent?: boolean | undefined }) | null> {\n\tconst {\n\t\tcontext: { adapter },\n\t} = await getCurrentAuthContext();\n\tconst trustedClient = trustedClients.find(\n\t\t(client) => client.clientId === clientId,\n\t);\n\tif (trustedClient) {\n\t\treturn trustedClient;\n\t}\n\treturn adapter\n\t\t.findOne<OAuthApplication>({\n\t\t\tmodel: \"oauthApplication\",\n\t\t\twhere: [{ field: \"clientId\", value: clientId }],\n\t\t})\n\t\t.then((res) => {\n\t\t\tif (!res) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\t// omit sensitive fields\n\t\t\treturn {\n\t\t\t\tclientId: res.clientId,\n\t\t\t\tclientSecret: res.clientSecret,\n\t\t\t\ttype: res.type,\n\t\t\t\tname: res.name,\n\t\t\t\ticon: res.icon,\n\t\t\t\tdisabled: res.disabled,\n\t\t\t\tredirectUrls: (res.redirectUrls ?? \"\").split(\",\"),\n\t\t\t\tmetadata: res.metadata ? JSON.parse(res.metadata) : {},\n\t\t\t} satisfies Client;\n\t\t});\n}\n\nexport const getMetadata = (\n\tctx: GenericEndpointContext,\n\toptions?: OIDCOptions | undefined,\n): OIDCMetadata => {\n\tconst jwtPlugin = getJwtPlugin(ctx);\n\tconst issuer =\n\t\tjwtPlugin && jwtPlugin.options?.jwt && jwtPlugin.options.jwt.issuer\n\t\t\t? jwtPlugin.options.jwt.issuer\n\t\t\t: (ctx.context.options.baseURL as string);\n\tconst baseURL = ctx.context.baseURL;\n\tconst supportedAlgs = options?.useJWTPlugin\n\t\t? [\"RS256\", \"EdDSA\", \"none\"]\n\t\t: [\"HS256\", \"none\"];\n\treturn {\n\t\tissuer,\n\t\tauthorization_endpoint: `${baseURL}/oauth2/authorize`,\n\t\ttoken_endpoint: `${baseURL}/oauth2/token`,\n\t\tuserinfo_endpoint: `${baseURL}/oauth2/userinfo`,\n\t\tjwks_uri: `${baseURL}/jwks`,\n\t\tregistration_endpoint: `${baseURL}/oauth2/register`,\n\t\tend_session_endpoint: `${baseURL}/oauth2/endsession`,\n\t\tscopes_supported: [\"openid\", \"profile\", \"email\", \"offline_access\"],\n\t\tresponse_types_supported: [\"code\"],\n\t\tresponse_modes_supported: [\"query\"],\n\t\tgrant_types_supported: [\"authorization_code\", \"refresh_token\"],\n\t\tacr_values_supported: [\n\t\t\t\"urn:mace:incommon:iap:silver\",\n\t\t\t\"urn:mace:incommon:iap:bronze\",\n\t\t],\n\t\tsubject_types_supported: [\"public\"],\n\t\tid_token_signing_alg_values_supported: supportedAlgs,\n\t\ttoken_endpoint_auth_methods_supported: [\n\t\t\t\"client_secret_basic\",\n\t\t\t\"client_secret_post\",\n\t\t\t\"none\",\n\t\t],\n\t\tcode_challenge_methods_supported: [\"S256\"],\n\t\tclaims_supported: [\n\t\t\t\"sub\",\n\t\t\t\"iss\",\n\t\t\t\"aud\",\n\t\t\t\"exp\",\n\t\t\t\"nbf\",\n\t\t\t\"iat\",\n\t\t\t\"jti\",\n\t\t\t\"email\",\n\t\t\t\"email_verified\",\n\t\t\t\"name\",\n\t\t],\n\t\t...options?.metadata,\n\t};\n};\n\nconst oAuthConsentBodySchema = z.object({\n\taccept: z.boolean(),\n\tconsent_code: z.string().optional().nullish(),\n});\n\nconst oAuth2TokenBodySchema = z.record(z.any(), z.any());\n\nconst registerOAuthApplicationBodySchema = z.object({\n\tredirect_uris: z.array(z.string()).meta({\n\t\tdescription:\n\t\t\t'A list of redirect URIs. Eg: [\"https://client.example.com/callback\"]',\n\t}),\n\ttoken_endpoint_auth_method: z\n\t\t.enum([\"none\", \"client_secret_basic\", \"client_secret_post\"])\n\t\t.meta({\n\t\t\tdescription:\n\t\t\t\t'The authentication method for the token endpoint. Eg: \"client_secret_basic\"',\n\t\t})\n\t\t.default(\"client_secret_basic\")\n\t\t.optional(),\n\tgrant_types: z\n\t\t.array(\n\t\t\tz.enum([\n\t\t\t\t\"authorization_code\",\n\t\t\t\t\"implicit\",\n\t\t\t\t\"password\",\n\t\t\t\t\"client_credentials\",\n\t\t\t\t\"refresh_token\",\n\t\t\t\t\"urn:ietf:params:oauth:grant-type:jwt-bearer\",\n\t\t\t\t\"urn:ietf:params:oauth:grant-type:saml2-bearer\",\n\t\t\t]),\n\t\t)\n\t\t.meta({\n\t\t\tdescription:\n\t\t\t\t'The grant types supported by the application. Eg: [\"authorization_code\"]',\n\t\t})\n\t\t.default([\"authorization_code\"])\n\t\t.optional(),\n\tresponse_types: z\n\t\t.array(z.enum([\"code\", \"token\"]))\n\t\t.meta({\n\t\t\tdescription:\n\t\t\t\t'The response types supported by the application. Eg: [\"code\"]',\n\t\t})\n\t\t.default([\"code\"])\n\t\t.optional(),\n\tclient_name: z\n\t\t.string()\n\t\t.meta({\n\t\t\tdescription: 'The name of the application. Eg: \"My App\"',\n\t\t})\n\t\t.optional(),\n\tclient_uri: z\n\t\t.string()\n\t\t.meta({\n\t\t\tdescription:\n\t\t\t\t'The URI of the application. Eg: \"https://client.example.com\"',\n\t\t})\n\t\t.optional(),\n\tlogo_uri: z\n\t\t.string()\n\t\t.meta({\n\t\t\tdescription:\n\t\t\t\t'The URI of the application logo. Eg: \"https://client.example.com/logo.png\"',\n\t\t})\n\t\t.optional(),\n\tscope: z\n\t\t.string()\n\t\t.meta({\n\t\t\tdescription:\n\t\t\t\t'The scopes supported by the application. Separated by spaces. Eg: \"profile email\"',\n\t\t})\n\t\t.optional(),\n\tcontacts: z\n\t\t.array(z.string())\n\t\t.meta({\n\t\t\tdescription:\n\t\t\t\t'The contact information for the application. Eg: [\"admin@example.com\"]',\n\t\t})\n\t\t.optional(),\n\ttos_uri: z\n\t\t.string()\n\t\t.meta({\n\t\t\tdescription:\n\t\t\t\t'The URI of the application terms of service. Eg: \"https://client.example.com/tos\"',\n\t\t})\n\t\t.optional(),\n\tpolicy_uri: z\n\t\t.string()\n\t\t.meta({\n\t\t\tdescription:\n\t\t\t\t'The URI of the application privacy policy. Eg: \"https://client.example.com/policy\"',\n\t\t})\n\t\t.optional(),\n\tjwks_uri: z\n\t\t.string()\n\t\t.meta({\n\t\t\tdescription:\n\t\t\t\t'The URI of the application JWKS. Eg: \"https://client.example.com/jwks\"',\n\t\t})\n\t\t.optional(),\n\tjwks: z\n\t\t.record(z.any(), z.any())\n\t\t.meta({\n\t\t\tdescription:\n\t\t\t\t'The JWKS of the application. Eg: {\"keys\": [{\"kty\": \"RSA\", \"alg\": \"RS256\", \"use\": \"sig\", \"n\": \"...\", \"e\": \"...\"}]}',\n\t\t})\n\t\t.optional(),\n\tmetadata: z\n\t\t.record(z.any(), z.any())\n\t\t.meta({\n\t\t\tdescription: 'The metadata of the application. Eg: {\"key\": \"value\"}',\n\t\t})\n\t\t.optional(),\n\tsoftware_id: z\n\t\t.string()\n\t\t.meta({\n\t\t\tdescription: 'The software ID of the application. Eg: \"my-software\"',\n\t\t})\n\t\t.optional(),\n\tsoftware_version: z\n\t\t.string()\n\t\t.meta({\n\t\t\tdescription: 'The software version of the application. Eg: \"1.0.0\"',\n\t\t})\n\t\t.optional(),\n\tsoftware_statement: z\n\t\t.string()\n\t\t.meta({\n\t\t\tdescription: \"The software statement of the application.\",\n\t\t})\n\t\t.optional(),\n});\n\nconst DEFAULT_CODE_EXPIRES_IN = 600;\nconst DEFAULT_ACCESS_TOKEN_EXPIRES_IN = 3600;\nconst DEFAULT_REFRESH_TOKEN_EXPIRES_IN = 604800;\n\n/**\n * OpenID Connect (OIDC) plugin for Better Auth. This plugin implements the\n * authorization code flow and the token exchange flow. It also implements the\n * userinfo endpoint.\n *\n * @param options - The options for the OIDC plugin.\n * @returns A Better Auth plugin.\n */\nexport const oidcProvider = (options: OIDCOptions) => {\n\tconst modelName = {\n\t\toauthClient: \"oauthApplication\",\n\t\toauthAccessToken: \"oauthAccessToken\",\n\t\toauthConsent: \"oauthConsent\",\n\t};\n\n\tconst opts = {\n\t\tcodeExpiresIn: DEFAULT_CODE_EXPIRES_IN,\n\t\tdefaultScope: \"openid\",\n\t\taccessTokenExpiresIn: DEFAULT_ACCESS_TOKEN_EXPIRES_IN,\n\t\trefreshTokenExpiresIn: DEFAULT_REFRESH_TOKEN_EXPIRES_IN,\n\t\tallowPlainCodeChallengeMethod: true,\n\t\tstoreClientSecret: \"plain\" as const,\n\t\t...options,\n\t\tscopes: [\n\t\t\t\"openid\",\n\t\t\t\"profile\",\n\t\t\t\"email\",\n\t\t\t\"offline_access\",\n\t\t\t...(options?.scopes || []),\n\t\t],\n\t};\n\n\tconst trustedClients = options.trustedClients || [];\n\n\t/**\n\t * Store client secret according to the configured storage method\n\t */\n\tasync function storeClientSecret(\n\t\tctx: GenericEndpointContext,\n\t\tclientSecret: string,\n\t) {\n\t\tif (opts.storeClientSecret === \"encrypted\") {\n\t\t\treturn await symmetricEncrypt({\n\t\t\t\tkey: ctx.context.secret,\n\t\t\t\tdata: clientSecret,\n\t\t\t});\n\t\t}\n\t\tif (opts.storeClientSecret === \"hashed\") {\n\t\t\treturn await defaultClientSecretHasher(clientSecret);\n\t\t}\n\t\tif (\n\t\t\ttypeof opts.storeClientSecret === \"object\" &&\n\t\t\t\"hash\" in opts.storeClientSecret\n\t\t) {\n\t\t\treturn await opts.storeClientSecret.hash(clientSecret);\n\t\t}\n\t\tif (\n\t\t\ttypeof opts.storeClientSecret === \"object\" &&\n\t\t\t\"encrypt\" in opts.storeClientSecret\n\t\t) {\n\t\t\treturn await opts.storeClientSecret.encrypt(clientSecret);\n\t\t}\n\n\t\treturn clientSecret;\n\t}\n\n\t/**\n\t * Verify stored client secret against provided client secret\n\t */\n\tasync function verifyStoredClientSecret(\n\t\tctx: GenericEndpointContext,\n\t\tstoredClientSecret: string,\n\t\tclientSecret: string,\n\t): Promise<boolean> {\n\t\tif (opts.storeClientSecret === \"encrypted\") {\n\t\t\treturn (\n\t\t\t\t(await symmetricDecrypt({\n\t\t\t\t\tkey: ctx.context.secret,\n\t\t\t\t\tdata: storedClientSecret,\n\t\t\t\t})) === clientSecret\n\t\t\t);\n\t\t}\n\t\tif (opts.storeClientSecret === \"hashed\") {\n\t\t\tconst hashedClientSecret = await defaultClientSecretHasher(clientSecret);\n\t\t\treturn hashedClientSecret === storedClientSecret;\n\t\t}\n\t\tif (\n\t\t\ttypeof opts.storeClientSecret === \"object\" &&\n\t\t\t\"hash\" in opts.storeClientSecret\n\t\t) {\n\t\t\tconst hashedClientSecret =\n\t\t\t\tawait opts.storeClientSecret.hash(clientSecret);\n\t\t\treturn hashedClientSecret === storedClientSecret;\n\t\t}\n\t\tif (\n\t\t\ttypeof opts.storeClientSecret === \"object\" &&\n\t\t\t\"decrypt\" in opts.storeClientSecret\n\t\t) {\n\t\t\tconst decryptedClientSecret =\n\t\t\t\tawait opts.storeClientSecret.decrypt(storedClientSecret);\n\t\t\treturn decryptedClientSecret === clientSecret;\n\t\t}\n\n\t\treturn clientSecret === storedClientSecret;\n\t}\n\n\treturn {\n\t\tid: \"oidc\",\n\t\thooks: {\n\t\t\tafter: [\n\t\t\t\t{\n\t\t\t\t\tmatcher() {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t},\n\t\t\t\t\thandler: createAuthMiddleware(async (ctx) => {\n\t\t\t\t\t\tconst loginPromptCookie = await ctx.getSignedCookie(\n\t\t\t\t\t\t\t\"oidc_login_prompt\",\n\t\t\t\t\t\t\tctx.context.secret,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst cookieName = ctx.context.authCookies.sessionToken.name;\n\t\t\t\t\t\tconst parsedSetCookieHeader = parseSetCookieHeader(\n\t\t\t\t\t\t\tctx.context.responseHeaders?.get(\"set-cookie\") || \"\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst hasSessionToken = parsedSetCookieHeader.has(cookieName);\n\t\t\t\t\t\tif (!loginPromptCookie || !hasSessionToken) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tctx.setCookie(\"oidc_login_prompt\", \"\", {\n\t\t\t\t\t\t\tmaxAge: 0,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tconst sessionCookie = parsedSetCookieHeader.get(cookieName)?.value;\n\t\t\t\t\t\tconst sessionToken = sessionCookie?.split(\".\")[0]!;\n\t\t\t\t\t\tif (!sessionToken) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst session =\n\t\t\t\t\t\t\t(await ctx.context.internalAdapter.findSession(sessionToken)) ||\n\t\t\t\t\t\t\tctx.context.newSession;\n\t\t\t\t\t\tif (!session) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tctx.query = JSON.parse(loginPromptCookie);\n\n\t\t\t\t\t\t// Remove \"login\" from prompt since user just logged in\n\t\t\t\t\t\tconst promptSet = parsePrompt(String(ctx.query?.prompt));\n\t\t\t\t\t\tif (promptSet.has(\"login\")) {\n\t\t\t\t\t\t\tconst newPromptSet = new Set(promptSet);\n\t\t\t\t\t\t\tnewPromptSet.delete(\"login\");\n\t\t\t\t\t\t\tctx.query = {\n\t\t\t\t\t\t\t\t...ctx.query,\n\t\t\t\t\t\t\t\tprompt: Array.from(newPromptSet).join(\" \"),\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tctx.context.session = session;\n\t\t\t\t\t\tconst response = await authorize(ctx, opts);\n\t\t\t\t\t\treturn response;\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tendpoints: {\n\t\t\tgetOpenIdConfig: createAuthEndpoint(\n\t\t\t\t\"/.well-known/openid-configuration\",\n\t\t\t\t{\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\toperationId: \"getOpenIdConfig\",\n\t\t\t\t\tmetadata: HIDE_METADATA,\n\t\t\t\t},\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst metadata = getMetadata(ctx, options);\n\t\t\t\t\treturn ctx.json(metadata);\n\t\t\t\t},\n\t\t\t),\n\t\t\toAuth2authorize: createAuthEndpoint(\n\t\t\t\t\"/oauth2/authorize\",\n\t\t\t\t{\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\toperationId: \"oauth2Authorize\",\n\t\t\t\t\tquery: z.record(z.string(), z.any()),\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\topenapi: {\n\t\t\t\t\t\t\tdescription: \"Authorize an OAuth2 request\",\n\t\t\t\t\t\t\tresponses: {\n\t\t\t\t\t\t\t\t\"200\": {\n\t\t\t\t\t\t\t\t\tdescription: \"Authorization response generated successfully\",\n\t\t\t\t\t\t\t\t\tcontent: {\n\t\t\t\t\t\t\t\t\t\t\"application/json\": {\n\t\t\t\t\t\t\t\t\t\t\tschema: {\n\t\t\t\t\t\t\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"Authorization response, contents depend on the authorize function implementation\",\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\treturn authorize(ctx, opts);\n\t\t\t\t},\n\t\t\t),\n\t\t\toAuthConsent: createAuthEndpoint(\n\t\t\t\t\"/oauth2/consent\",\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\toperationId: \"oauth2Consent\",\n\t\t\t\t\tbody: oAuthConsentBodySchema,\n\t\t\t\t\tuse: [sessionMiddleware],\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\topenapi: {\n\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\"Handle OAuth2 consent. Supports both URL parameter-based flows (consent_code in body) and cookie-based flows (signed cookie).\",\n\t\t\t\t\t\t\trequestBody: {\n\t\t\t\t\t\t\t\trequired: true,\n\t\t\t\t\t\t\t\tcontent: {\n\t\t\t\t\t\t\t\t\t\"application/json\": {\n\t\t\t\t\t\t\t\t\t\tschema: {\n\t\t\t\t\t\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\t\t\taccept: {\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"boolean\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Whether the user accepts or denies the consent request\",\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\tconsent_code: {\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"The consent code from the authorization request. Optional if using cookie-based flow.\",\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\trequired: [\"accept\"],\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tresponses: {\n\t\t\t\t\t\t\t\t\"200\": {\n\t\t\t\t\t\t\t\t\tdescription: \"Consent processed successfully\",\n\t\t\t\t\t\t\t\t\tcontent: {\n\t\t\t\t\t\t\t\t\t\t\"application/json\": {\n\t\t\t\t\t\t\t\t\t\t\tschema: {\n\t\t\t\t\t\t\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\t\t\t\tredirectURI: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tformat: \"uri\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"The URI to redirect to, either with an authorization code or an error\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\trequired: [\"redirectURI\"],\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\t// Support both consent flow methods:\n\t\t\t\t\t// 1. URL parameter-based: consent_code in request body (standard OAuth2 pattern)\n\t\t\t\t\t// 2. Cookie-based: using signed cookie for stateful consent flows\n\t\t\t\t\tlet consentCode: string | null = ctx.body.consent_code || null;\n\n\t\t\t\t\tif (!consentCode) {\n\t\t\t\t\t\t// Check for cookie-based consent flow\n\t\t\t\t\t\tconst cookieValue = await ctx.getSignedCookie(\n\t\t\t\t\t\t\t\"oidc_consent_prompt\",\n\t\t\t\t\t\t\tctx.context.secret,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (cookieValue) {\n\t\t\t\t\t\t\tconsentCode = cookieValue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!consentCode) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description:\n\t\t\t\t\t\t\t\t\"consent_code is required (either in body or cookie)\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tconst verification =\n\t\t\t\t\t\tawait ctx.context.internalAdapter.findVerificationValue(\n\t\t\t\t\t\t\tconsentCode,\n\t\t\t\t\t\t);\n\t\t\t\t\tif (!verification) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"Invalid code\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (verification.expiresAt < new Date()) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"Code expired\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Clear the cookie\n\t\t\t\t\tctx.setCookie(\"oidc_consent_prompt\", \"\", {\n\t\t\t\t\t\tmaxAge: 0,\n\t\t\t\t\t});\n\n\t\t\t\t\tconst value = JSON.parse(verification.value) as CodeVerificationValue;\n\t\t\t\t\tif (!value.requireConsent) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"Consent not required\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!ctx.body.accept) {\n\t\t\t\t\t\tawait ctx.context.internalAdapter.deleteVerificationValue(\n\t\t\t\t\t\t\tverification.id,\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn ctx.json({\n\t\t\t\t\t\t\tredirectURI: `${value.redirectURI}?error=access_denied&error_description=User denied access`,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tconst code = generateRandomString(32, \"a-z\", \"A-Z\", \"0-9\");\n\t\t\t\t\tconst codeExpiresInMs =\n\t\t\t\t\t\t(opts?.codeExpiresIn ?? DEFAULT_CODE_EXPIRES_IN) * 1000;\n\t\t\t\t\tconst expiresAt = new Date(Date.now() + codeExpiresInMs);\n\t\t\t\t\tawait ctx.context.internalAdapter.updateVerificationValue(\n\t\t\t\t\t\tverification.id,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: JSON.stringify({\n\t\t\t\t\t\t\t\t...value,\n\t\t\t\t\t\t\t\trequireConsent: false,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tidentifier: code,\n\t\t\t\t\t\t\texpiresAt,\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t\tawait ctx.context.adapter.create({\n\t\t\t\t\t\tmodel: modelName.oauthConsent,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\tclientId: value.clientId,\n\t\t\t\t\t\t\tuserId: value.userId,\n\t\t\t\t\t\t\tscopes: value.scope.join(\" \"),\n\t\t\t\t\t\t\tconsentGiven: true,\n\t\t\t\t\t\t\tcreatedAt: new Date(),\n\t\t\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tconst redirectURI = new URL(value.redirectURI);\n\t\t\t\t\tredirectURI.searchParams.set(\"code\", code);\n\t\t\t\t\tif (value.state) redirectURI.searchParams.set(\"state\", value.state);\n\t\t\t\t\treturn ctx.json({\n\t\t\t\t\t\tredirectURI: redirectURI.toString(),\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t),\n\t\t\toAuth2token: createAuthEndpoint(\n\t\t\t\t\"/oauth2/token\",\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\toperationId: \"oauth2Token\",\n\t\t\t\t\tbody: oAuth2TokenBodySchema,\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\t...HIDE_METADATA,\n\t\t\t\t\t\tallowedMediaTypes: [\n\t\t\t\t\t\t\t\"application/x-www-form-urlencoded\",\n\t\t\t\t\t\t\t\"application/json\",\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tlet { body } = ctx;\n\t\t\t\t\tif (!body) {\n\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\terror_description: \"request body not found\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (body instanceof FormData) {\n\t\t\t\t\t\tbody = Object.fromEntries(body.entries());\n\t\t\t\t\t}\n\t\t\t\t\tif (!(body instanceof Object)) {\n\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\terror_description: \"request body is not an object\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tlet { client_id, client_secret } = body;\n\t\t\t\t\tconst authorization =\n\t\t\t\t\t\tctx.request?.headers.get(\"authorization\") || null;\n\t\t\t\t\tif (\n\t\t\t\t\t\tauthorization &&\n\t\t\t\t\t\t!client_id &&\n\t\t\t\t\t\t!client_secret &&\n\t\t\t\t\t\tauthorization.startsWith(\"Basic \")\n\t\t\t\t\t) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst encoded = authorization.replace(\"Basic \", \"\");\n\t\t\t\t\t\t\tconst decoded = new TextDecoder().decode(base64.decode(encoded));\n\t\t\t\t\t\t\tif (!decoded.includes(\":\")) {\n\t\t\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\t\t\terror_description: \"invalid authorization header format\",\n\t\t\t\t\t\t\t\t\terror: \"invalid_client\",\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst [id, secret] = decoded.split(\":\");\n\t\t\t\t\t\t\tif (!id || !secret) {\n\t\t\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\t\t\terror_description: \"invalid authorization header format\",\n\t\t\t\t\t\t\t\t\terror: \"invalid_client\",\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tclient_id = id;\n\t\t\t\t\t\t\tclient_secret = secret;\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\t\terror_description: \"invalid authorization header format\",\n\t\t\t\t\t\t\t\terror: \"invalid_client\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst now = Date.now();\n\t\t\t\t\tconst iat = Math.floor(now / 1000);\n\t\t\t\t\tconst exp = iat + (opts.accessTokenExpiresIn ?? 3600);\n\n\t\t\t\t\tconst accessTokenExpiresAt = new Date(exp * 1000);\n\t\t\t\t\tconst refreshTokenExpiresAt = new Date(\n\t\t\t\t\t\t(iat + (opts.refreshTokenExpiresIn ?? 604800)) * 1000,\n\t\t\t\t\t);\n\n\t\t\t\t\tconst {\n\t\t\t\t\t\tgrant_type,\n\t\t\t\t\t\tcode,\n\t\t\t\t\t\tredirect_uri,\n\t\t\t\t\t\trefresh_token,\n\t\t\t\t\t\tcode_verifier,\n\t\t\t\t\t} = body;\n\t\t\t\t\tif (grant_type === \"refresh_token\") {\n\t\t\t\t\t\tif (!refresh_token) {\n\t\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\t\terror_description: \"refresh_token is required\",\n\t\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst token = await ctx.context.adapter.findOne<OAuthAccessToken>({\n\t\t\t\t\t\t\tmodel: modelName.oauthAccessToken,\n\t\t\t\t\t\t\twhere: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tfield: \"refreshToken\",\n\t\t\t\t\t\t\t\t\tvalue: refresh_token.toString(),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (!token) {\n\t\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\t\terror_description: \"invalid refresh token\",\n\t\t\t\t\t\t\t\terror: \"invalid_grant\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (token.clientId !== client_id?.toString()) {\n\t\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\t\terror_description: \"invalid client_id\",\n\t\t\t\t\t\t\t\terror: \"invalid_client\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (token.refreshTokenExpiresAt < new Date()) {\n\t\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\t\terror_description: \"refresh token expired\",\n\t\t\t\t\t\t\t\terror: \"invalid_grant\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst accessToken = generateRandomString(32, \"a-z\", \"A-Z\");\n\t\t\t\t\t\tconst newRefreshToken = generateRandomString(32, \"a-z\", \"A-Z\");\n\n\t\t\t\t\t\tawait ctx.context.adapter.create({\n\t\t\t\t\t\t\tmodel: modelName.oauthAccessToken,\n\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\taccessToken,\n\t\t\t\t\t\t\t\trefreshToken: newRefreshToken,\n\t\t\t\t\t\t\t\taccessTokenExpiresAt,\n\t\t\t\t\t\t\t\trefreshTokenExpiresAt,\n\t\t\t\t\t\t\t\tclientId: client_id.toString(),\n\t\t\t\t\t\t\t\tuserId: token.userId,\n\t\t\t\t\t\t\t\tscopes: token.scopes,\n\t\t\t\t\t\t\t\tcreatedAt: new Date(iat * 1000),\n\t\t\t\t\t\t\t\tupdatedAt: new Date(iat * 1000),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn ctx.json({\n\t\t\t\t\t\t\taccess_token: accessToken,\n\t\t\t\t\t\t\ttoken_type: \"Bearer\",\n\t\t\t\t\t\t\texpires_in: opts.accessTokenExpiresIn,\n\t\t\t\t\t\t\trefresh_token: newRefreshToken,\n\t\t\t\t\t\t\tscope: token.scopes,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!code) {\n\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\terror_description: \"code is required\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (options.requirePKCE && !code_verifier) {\n\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\terror_description: \"code verifier is missing\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t/**\n\t\t\t\t\t * We need to check if the code is valid before we can proceed\n\t\t\t\t\t * with the rest of the request.\n\t\t\t\t\t */\n\t\t\t\t\tconst verificationValue =\n\t\t\t\t\t\tawait ctx.context.internalAdapter.findVerificationValue(\n\t\t\t\t\t\t\tcode.toString(),\n\t\t\t\t\t\t);\n\t\t\t\t\tif (!verificationValue) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"invalid code\",\n\t\t\t\t\t\t\terror: \"invalid_grant\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (verificationValue.expiresAt < new Date()) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"code expired\",\n\t\t\t\t\t\t\terror: \"invalid_grant\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tawait ctx.context.internalAdapter.deleteVerificationValue(\n\t\t\t\t\t\tverificationValue.id,\n\t\t\t\t\t);\n\t\t\t\t\tif (!client_id) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"client_id is required\",\n\t\t\t\t\t\t\terror: \"invalid_client\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (!grant_type) {\n\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\terror_description: \"grant_type is required\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (grant_type !== \"authorization_code\") {\n\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\terror_description: \"grant_type must be 'authorization_code'\",\n\t\t\t\t\t\t\terror: \"unsupported_grant_type\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!redirect_uri) {\n\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\terror_description: \"redirect_uri is required\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tconst client = await getClient(client_id.toString(), trustedClients);\n\t\t\t\t\tif (!client) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"invalid client_id\",\n\t\t\t\t\t\t\terror: \"invalid_client\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (client.disabled) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"client is disabled\",\n\t\t\t\t\t\t\terror: \"invalid_client\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tconst value = JSON.parse(\n\t\t\t\t\t\tverificationValue.value,\n\t\t\t\t\t) as CodeVerificationValue;\n\t\t\t\t\tif (value.clientId !== client_id.toString()) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"invalid client_id\",\n\t\t\t\t\t\t\terror: \"invalid_client\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (value.redirectURI !== redirect_uri.toString()) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"invalid redirect_uri\",\n\t\t\t\t\t\t\terror: \"invalid_client\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (value.codeChallenge && !code_verifier) {\n\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\terror_description: \"code verifier is missing\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (client.type === \"public\") {\n\t\t\t\t\t\t// For public clients (type: 'public'), validate PKCE instead of client_secret\n\t\t\t\t\t\tif (!code_verifier) {\n\t\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\t\terror_description:\n\t\t\t\t\t\t\t\t\t\"code verifier is required for public clients\",\n\t\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// PKCE validation happens later in the flow, so we skip client_secret validation\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (!client.clientSecret || !client_secret) {\n\t\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\t\terror_description:\n\t\t\t\t\t\t\t\t\t\"client_secret is required for confidential clients\",\n\t\t\t\t\t\t\t\terror: \"invalid_client\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst isValidSecret = await verifyStoredClientSecret(\n\t\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\tclient.clientSecret,\n\t\t\t\t\t\t\tclient_secret.toString(),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (!isValidSecret) {\n\t\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\t\terror_description: \"invalid client_secret\",\n\t\t\t\t\t\t\t\terror: \"invalid_client\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst challenge =\n\t\t\t\t\t\tvalue.codeChallengeMethod === \"plain\"\n\t\t\t\t\t\t\t? code_verifier\n\t\t\t\t\t\t\t: await createHash(\"SHA-256\", \"base64urlnopad\").digest(\n\t\t\t\t\t\t\t\t\tcode_verifier,\n\t\t\t\t\t\t\t\t);\n\n\t\t\t\t\tif (challenge !== value.codeChallenge) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"code verification failed\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tconst requestedScopes = value.scope;\n\t\t\t\t\tawait ctx.context.internalAdapter.deleteVerificationValue(\n\t\t\t\t\t\tverificationValue.id,\n\t\t\t\t\t);\n\t\t\t\t\tconst accessToken = generateRandomString(32, \"a-z\", \"A-Z\");\n\t\t\t\t\tconst refreshToken = generateRandomString(32, \"A-Z\", \"a-z\");\n\t\t\t\t\tawait ctx.context.adapter.create({\n\t\t\t\t\t\tmodel: modelName.oauthAccessToken,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\taccessToken,\n\t\t\t\t\t\t\trefreshToken,\n\t\t\t\t\t\t\taccessTokenExpiresAt,\n\t\t\t\t\t\t\trefreshTokenExpiresAt,\n\t\t\t\t\t\t\tclientId: client_id.toString(),\n\t\t\t\t\t\t\tuserId: value.userId,\n\t\t\t\t\t\t\tscopes: requestedScopes.join(\" \"),\n\t\t\t\t\t\t\tcreatedAt: new Date(iat * 1000),\n\t\t\t\t\t\t\tupdatedAt: new Date(iat * 1000),\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tconst user = await ctx.context.internalAdapter.findUserById(\n\t\t\t\t\t\tvalue.userId,\n\t\t\t\t\t);\n\t\t\t\t\tif (!user) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"user not found\",\n\t\t\t\t\t\t\terror: \"invalid_grant\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tconst profile = {\n\t\t\t\t\t\tgiven_name: user.name.split(\" \")[0]!,\n\t\t\t\t\t\tfamily_name: user.name.split(\" \")[1]!,\n\t\t\t\t\t\tname: user.name,\n\t\t\t\t\t\tprofile: user.image,\n\t\t\t\t\t\tupdated_at: new Date(user.updatedAt).toISOString(),\n\t\t\t\t\t};\n\t\t\t\t\tconst email = {\n\t\t\t\t\t\temail: user.email,\n\t\t\t\t\t\temail_verified: user.emailVerified,\n\t\t\t\t\t};\n\t\t\t\t\tconst userClaims = {\n\t\t\t\t\t\t...(requestedScopes.includes(\"profile\") ? profile : {}),\n\t\t\t\t\t\t...(requestedScopes.includes(\"email\") ? email : {}),\n\t\t\t\t\t};\n\n\t\t\t\t\tconst additionalUserClaims = options.getAdditionalUserInfoClaim\n\t\t\t\t\t\t? await options.getAdditionalUserInfoClaim(\n\t\t\t\t\t\t\t\tuser,\n\t\t\t\t\t\t\t\trequestedScopes,\n\t\t\t\t\t\t\t\tclient,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: {};\n\n\t\t\t\t\tconst payload = {\n\t\t\t\t\t\tsub: user.id,\n\t\t\t\t\t\taud: client_id.toString(),\n\t\t\t\t\t\tiat: iat,\n\t\t\t\t\t\tauth_time: ctx.context.session\n\t\t\t\t\t\t\t? new Date(ctx.context.session.session.createdAt).getTime()\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\tnonce: value.nonce,\n\t\t\t\t\t\tacr: \"urn:mace:incommon:iap:silver\", // default to silver - ⚠︎ this should be configurable and should be validated against the client's metadata\n\t\t\t\t\t\t...userClaims,\n\t\t\t\t\t\t...additionalUserClaims,\n\t\t\t\t\t};\n\t\t\t\t\tconst expirationTime =\n\t\t\t\t\t\tMath.floor(Date.now() / 1000) +\n\t\t\t\t\t\t(opts?.accessTokenExpiresIn ?? DEFAULT_ACCESS_TOKEN_EXPIRES_IN);\n\n\t\t\t\t\tlet idToken: string;\n\n\t\t\t\t\t// The JWT plugin is enabled, so we use the JWKS keys to sign\n\t\t\t\t\tif (options.useJWTPlugin) {\n\t\t\t\t\t\tconst jwtPlugin = getJwtPlugin(ctx);\n\t\t\t\t\t\tif (!jwtPlugin) {\n\t\t\t\t\t\t\tctx.context.logger.error(\n\t\t\t\t\t\t\t\t\"OIDC: `useJWTPlugin` is enabled but the JWT plugin is not available. Make sure you have the JWT Plugin in your plugins array or set `useJWTPlugin` to false.\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tthrow new APIError(\"INTERNAL_SERVER_ERROR\", {\n\t\t\t\t\t\t\t\terror_description: \"JWT plugin is not enabled\",\n\t\t\t\t\t\t\t\terror: \"internal_server_error\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tidToken = await getJwtToken(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...ctx,\n\t\t\t\t\t\t\t\tcontext: {\n\t\t\t\t\t\t\t\t\t...ctx.context,\n\t\t\t\t\t\t\t\t\tsession: {\n\t\t\t\t\t\t\t\t\t\tsession: {\n\t\t\t\t\t\t\t\t\t\t\tid: generateRandomString(32, \"a-z\", \"A-Z\"),\n\t\t\t\t\t\t\t\t\t\t\tcreatedAt: new Date(iat * 1000),\n\t\t\t\t\t\t\t\t\t\t\tupdatedAt: new Date(iat * 1000),\n\t\t\t\t\t\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\t\t\t\t\t\texpiresAt: accessTokenExpiresAt,\n\t\t\t\t\t\t\t\t\t\t\ttoken: accessToken,\n\t\t\t\t\t\t\t\t\t\t\tipAddress: ctx.request?.headers.get(\"x-forwarded-for\"),\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tuser,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...jwtPlugin.options,\n\t\t\t\t\t\t\t\tjwt: {\n\t\t\t\t\t\t\t\t\t...jwtPlugin.options?.jwt,\n\t\t\t\t\t\t\t\t\tgetSubject: () => user.id,\n\t\t\t\t\t\t\t\t\taudience: client_id.toString(),\n\t\t\t\t\t\t\t\t\tissuer:\n\t\t\t\t\t\t\t\t\t\tjwtPlugin.options?.jwt?.issuer ??\n\t\t\t\t\t\t\t\t\t\tctx.context.options.baseURL,\n\t\t\t\t\t\t\t\t\texpirationTime,\n\t\t\t\t\t\t\t\t\tdefinePayload: () => payload,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// If the JWT token is not enabled, create a key and use it to sign\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidToken = await new SignJWT(payload)\n\t\t\t\t\t\t\t.setProtectedHeader({ alg: \"HS256\" })\n\t\t\t\t\t\t\t.setIssuedAt(iat)\n\t\t\t\t\t\t\t.setExpirationTime(accessTokenExpiresAt)\n\t\t\t\t\t\t\t.sign(new TextEncoder().encode(client.clientSecret));\n\t\t\t\t\t}\n\n\t\t\t\t\treturn ctx.json(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\taccess_token: accessToken,\n\t\t\t\t\t\t\ttoken_type: \"Bearer\",\n\t\t\t\t\t\t\texpires_in: opts.accessTokenExpiresIn,\n\t\t\t\t\t\t\trefresh_token: requestedScopes.includes(\"offline_access\")\n\t\t\t\t\t\t\t\t? refreshToken\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\tscope: requestedScopes.join(\" \"),\n\t\t\t\t\t\t\tid_token: requestedScopes.includes(\"openid\")\n\t\t\t\t\t\t\t\t? idToken\n\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\"Cache-Control\": \"no-store\",\n\t\t\t\t\t\t\t\tPragma: \"no-cache\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t),\n\t\t\toAuth2userInfo: createAuthEndpoint(\n\t\t\t\t\"/oauth2/userinfo\",\n\t\t\t\t{\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\toperationId: \"oauth2Userinfo\",\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\t...HIDE_METADATA,\n\t\t\t\t\t\topenapi: {\n\t\t\t\t\t\t\tdescription: \"Get OAuth2 user information\",\n\t\t\t\t\t\t\tresponses: {\n\t\t\t\t\t\t\t\t\"200\": {\n\t\t\t\t\t\t\t\t\tdescription: \"User information retrieved successfully\",\n\t\t\t\t\t\t\t\t\tcontent: {\n\t\t\t\t\t\t\t\t\t\t\"application/json\": {\n\t\t\t\t\t\t\t\t\t\t\tschema: {\n\t\t\t\t\t\t\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\t\t\t\tsub: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription: \"Subject identifier (user ID)\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\temail: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tformat: \"email\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"User's email address, included if 'email' scope is granted\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tname: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"User's full name, included if 'profile' scope is granted\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tpicture: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tformat: \"uri\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"User's profile picture URL, included if 'profile' scope is granted\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tgiven_name: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"User's given name, included if 'profile' scope is granted\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tfamily_name: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"User's family name, included if 'profile' scope is granted\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\temail_verified: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"boolean\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Whether the email is verified, included if 'email' scope is granted\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\trequired: [\"sub\"],\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tif (!ctx.request) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"request not found\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tconst authorization = ctx.request.headers.get(\"authorization\");\n\t\t\t\t\tif (!authorization) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"authorization header not found\",\n\t\t\t\t\t\t\terror: \"invalid_request\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tconst token = authorization.replace(\"Bearer \", \"\");\n\t\t\t\t\tconst accessToken =\n\t\t\t\t\t\tawait ctx.context.adapter.findOne<OAuthAccessToken>({\n\t\t\t\t\t\t\tmodel: modelName.oauthAccessToken,\n\t\t\t\t\t\t\twhere: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tfield: \"accessToken\",\n\t\t\t\t\t\t\t\t\tvalue: token,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t});\n\t\t\t\t\tif (!accessToken) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"invalid access token\",\n\t\t\t\t\t\t\terror: \"invalid_token\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (accessToken.accessTokenExpiresAt < new Date()) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"The Access Token expired\",\n\t\t\t\t\t\t\terror: \"invalid_token\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tconst client = await getClient(accessToken.clientId, trustedClients);\n\t\t\t\t\tif (!client) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"client not found\",\n\t\t\t\t\t\t\terror: \"invalid_token\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tconst user = await ctx.context.internalAdapter.findUserById(\n\t\t\t\t\t\taccessToken.userId,\n\t\t\t\t\t);\n\t\t\t\t\tif (!user) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror_description: \"user not found\",\n\t\t\t\t\t\t\terror: \"invalid_token\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tconst requestedScopes = accessToken.scopes.split(\" \");\n\t\t\t\t\tconst baseUserClaims = {\n\t\t\t\t\t\tsub: user.id,\n\t\t\t\t\t\temail: requestedScopes.includes(\"email\") ? user.email : undefined,\n\t\t\t\t\t\tname: requestedScopes.includes(\"profile\") ? user.name : undefined,\n\t\t\t\t\t\tpicture: requestedScopes.includes(\"profile\")\n\t\t\t\t\t\t\t? user.image\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\tgiven_name: requestedScopes.includes(\"profile\")\n\t\t\t\t\t\t\t? user.name.split(\" \")[0]!\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\tfamily_name: requestedScopes.includes(\"profile\")\n\t\t\t\t\t\t\t? user.name.split(\" \")[1]!\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\temail_verified: requestedScopes.includes(\"email\")\n\t\t\t\t\t\t\t? user.emailVerified\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t};\n\t\t\t\t\tconst userClaims = options.getAdditionalUserInfoClaim\n\t\t\t\t\t\t? await options.getAdditionalUserInfoClaim(\n\t\t\t\t\t\t\t\tuser,\n\t\t\t\t\t\t\t\trequestedScopes,\n\t\t\t\t\t\t\t\tclient,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: baseUserClaims;\n\t\t\t\t\treturn ctx.json({\n\t\t\t\t\t\t...baseUserClaims,\n\t\t\t\t\t\t...userClaims,\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t),\n\t\t\t/**\n\t\t\t * ### Endpoint\n\t\t\t *\n\t\t\t * POST `/oauth2/register`\n\t\t\t *\n\t\t\t * ### API Methods\n\t\t\t *\n\t\t\t * **server:**\n\t\t\t * `auth.api.registerOAuthApplication`\n\t\t\t *\n\t\t\t * **client:**\n\t\t\t * `authClient.oauth2.register`\n\t\t\t *\n\t\t\t * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/oidc-provider#api-method-oauth2-register)\n\t\t\t */\n\t\t\tregisterOAuthApplication: createAuthEndpoint(\n\t\t\t\t\"/oauth2/register\",\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: registerOAuthApplicationBodySchema,\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\topenapi: {\n\t\t\t\t\t\t\tdescription: \"Register an OAuth2 application\",\n\t\t\t\t\t\t\tresponses: {\n\t\t\t\t\t\t\t\t\"200\": {\n\t\t\t\t\t\t\t\t\tdescription: \"OAuth2 application registered successfully\",\n\t\t\t\t\t\t\t\t\tcontent: {\n\t\t\t\t\t\t\t\t\t\t\"application/json\": {\n\t\t\t\t\t\t\t\t\t\t\tschema: {\n\t\t\t\t\t\t\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\t\t\t\tname: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription: \"Name of the OAuth2 application\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\ticon: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription: \"Icon URL for the application\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tmetadata: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"object\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Additional metadata for the application\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tclientId: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription: \"Unique identifier for the client\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tclientSecret: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription: \"Secret key for the client\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tredirectURLs: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"array\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titems: { type: \"string\", format: \"uri\" },\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription: \"List of allowed redirect URLs\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\ttype: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription: \"Type of the client\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tenum: [\"web\"],\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tauthenticationScheme: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Authentication scheme used by the client\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tenum: [\"client_secret\"],\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"boolean\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription: \"Whether the client is disabled\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tenum: [false],\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tuserId: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tnullable: true,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"ID of the user who registered the client, null if registered anonymously\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tcreatedAt: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tformat: \"date-time\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription: \"Creation timestamp\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\tupdatedAt: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: \"string\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tformat: \"date-time\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdescription: \"Last update timestamp\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\trequired: [\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"name\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"clientId\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"clientSecret\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"redirectURLs\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"type\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"authenticationScheme\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"disabled\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"createdAt\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"updatedAt\",\n\t\t\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst body = ctx.body;\n\t\t\t\t\tconst session = await getSessionFromCtx(ctx);\n\n\t\t\t\t\t// Check authorization\n\t\t\t\t\tif (!session && !options.allowDynamicClientRegistration) {\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\terror: \"invalid_token\",\n\t\t\t\t\t\t\terror_description:\n\t\t\t\t\t\t\t\t\"Authentication required for client registration\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Validate redirect URIs for redirect-based flows\n\t\t\t\t\tif (\n\t\t\t\t\t\t(!body.grant_types ||\n\t\t\t\t\t\t\tbody.grant_types.includes(\"authorization_code\") ||\n\t\t\t\t\t\t\tbody.grant_types.includes(\"implicit\")) &&\n\t\t\t\t\t\t(!body.redirect_uris || body.redirect_uris.length === 0)\n\t\t\t\t\t) {\n\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\terror: \"invalid_redirect_uri\",\n\t\t\t\t\t\t\terror_description:\n\t\t\t\t\t\t\t\t\"Redirect URIs are required for authorization_code and implicit grant types\",\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Validate correlation between grant_types and response_types\n\t\t\t\t\tif (body.grant_types && body.response_types) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tbody.grant_types.includes(\"authorization_code\") &&\n\t\t\t\t\t\t\t!body.response_types.includes(\"code\")\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\t\terror: \"invalid_client_metadata\",\n\t\t\t\t\t\t\t\terror_description:\n\t\t\t\t\t\t\t\t\t\"When 'authorization_code' grant type is used, 'code' response type must be included\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tbody.grant_types.includes(\"implicit\") &&\n\t\t\t\t\t\t\t!body.response_types.includes(\"token\")\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\t\terror: \"invalid_client_metadata\",\n\t\t\t\t\t\t\t\terror_description:\n\t\t\t\t\t\t\t\t\t\"When 'implicit' grant type is used, 'token' response type must be included\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst clientId =\n\t\t\t\t\t\toptions.generateClientId?.() ||\n\t\t\t\t\t\tgenerateRandomString(32, \"a-z\", \"A-Z\");\n\t\t\t\t\tconst clien