@better-auth/oauth-provider 1.7.0-beta.8 → 1.7.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 CHANGED
@@ -1,20 +1,20 @@
1
- import { D as applyOAuthProviderMetadataExtensions, E as verifyOAuthQueryParams, F as getSupportedGrantTypes, L as isExtensionTokenEndpointAuthMethod, M as getClientDiscoveries, P as getSupportedAuthMethods, R as validateOAuthProviderExtensions, S as storeToken, T as validateClientCredentials, _ as removePromptFromQuery, a as getClient, b as searchParamsToQuery, c as getStoredToken, d as mergeDiscoveryMetadata, g as removeMaxAgeFromQuery, h as parsePrompt, i as extractClientCredentials, j as extendOAuthProvider, l as isPKCERequired, m as parseClientMetadata, n as decryptStoredClientSecret, o as getJwtPlugin, p as parseBearerToken, r as destructureCredentials, t as clientAllowsGrant, u as isSessionFreshForSignedQuery, w as toResourceList, x as storeClientSecret, y as resolveSubjectIdentifier } from "./utils-Baq6atYN.mjs";
2
- import { a as setSignedOAuthQueryParameterNames, i as postLoginClearedParam, n as canonicalizeOAuthQueryParams, o as signedQueryIssuedAtParam, r as getSignedQueryIssuedAt } from "./signed-query-CFv2jNMT.mjs";
3
- import { _ as invalidateResourceCache, a as invalidateRefreshFamily, b as resolveResourcePolicy, c as ResourceUriSchema, d as clientRegistrationRequestSchema, f as JWS_ALGORITHMS, g as getResource, h as extractRepeatedResourceFromForm, i as getOAuthProviderApi, l as SafeUrlSchema, m as buildClientResourceLinkId, o as tokenEndpoint, p as assertIdentifierValid, r as decodeRefreshToken, s as userInfoEndpoint, t as introspectEndpoint, u as authorizationQuerySchema, v as isAudienceClaimAllowed, x as seedResources, y as logEnforcePerClientResourcesResolution } from "./introspect-BXNvkz8S.mjs";
4
- import { n as consumeClientAssertion, r as isPrivateHostname } from "./client-assertion-CctbJywV.mjs";
5
- import { t as PACKAGE_VERSION } from "./version-bmpg6tAD.mjs";
1
+ import { C as STANDARD_CLAIM_NAMES, D as getRequestedUserInfoClaims, E as filterClaimsRequestUserInfoClaims, S as STANDARD_CLAIMS, T as claimsRequestParameterSchema, _ as invalidateResourceCache, a as invalidateRefreshFamily, b as resolveResourcePolicy, c as ResourceUriSchema, d as clientRegistrationRequestSchema, f as JWS_ALGORITHMS, g as getResource, h as extractRepeatedResourceFromForm, i as getOAuthProviderApi, l as SafeUrlSchema, m as buildClientResourceLinkId, o as tokenEndpoint, p as assertIdentifierValid, r as decodeRefreshToken, s as userInfoEndpoint, t as introspectEndpoint, u as authorizationQuerySchema, v as isAudienceClaimAllowed, w as getSupportedClaims, x as seedResources, y as logEnforcePerClientResourcesResolution } from "./introspect-Bzbar3iY.mjs";
2
+ import { D as applyOAuthProviderMetadataExtensions, E as verifyOAuthQueryParams, F as getSupportedGrantTypes, L as isExtensionTokenEndpointAuthMethod, M as getClientDiscoveries, P as getSupportedAuthMethods, R as validateOAuthProviderExtensions, S as storeToken, T as validateClientCredentials, _ as removePromptFromQuery, a as getClient, b as searchParamsToQuery, c as getStoredToken, d as mergeDiscoveryMetadata, g as removeMaxAgeFromQuery, h as parsePrompt, i as extractClientCredentials, j as extendOAuthProvider, l as isPKCERequired, m as parseClientMetadata, n as decryptStoredClientSecret, o as getJwtPlugin, p as parseBearerToken, r as destructureCredentials, t as clientAllowsGrant, u as isSessionFreshForSignedQuery, w as toResourceList, x as storeClientSecret, y as resolveSubjectIdentifier } from "./utils-DO8lmoDw.mjs";
3
+ import { a as setSignedOAuthQueryParameterNames, i as postLoginClearedParam, n as canonicalizeOAuthQueryParams, o as signedQueryIssuedAtParam, r as getSignedQueryIssuedAt } from "./signed-query-Df1MNiSH.mjs";
4
+ import { n as consumeClientAssertion, r as isPrivateHostname } from "./client-assertion-D-tAYsKC.mjs";
5
+ import { t as PACKAGE_VERSION } from "./version-M87fotrf.mjs";
6
6
  import { t as raiseResourceServerChallenge } from "./resource-challenge-B-cqv4ur.mjs";
7
7
  import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
8
8
  import { isLoopbackHost, isLoopbackIP } from "@better-auth/core/utils/host";
9
9
  import { APIError, NO_STORE_HEADERS, addOAuthServerContext, createAuthEndpoint, createAuthMiddleware, dispatchAuthEndpoint, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
10
10
  import { generateRandomString, makeSignature } from "better-auth/crypto";
11
11
  import { APIError as APIError$1 } from "better-call";
12
- import { defineRequestState, runWithTransaction } from "@better-auth/core/context";
13
12
  import { logger } from "@better-auth/core/env";
13
+ import * as z from "zod";
14
+ import { defineRequestState, runWithTransaction } from "@better-auth/core/context";
14
15
  import { BetterAuthError } from "@better-auth/core/error";
15
16
  import { parseSetCookieHeader } from "better-auth/cookies";
16
17
  import { mergeSchema } from "better-auth/db";
17
- import * as z from "zod";
18
18
  import { DPOP_SIGNING_ALGORITHMS, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
19
19
  import { getJwks, stripAccessTokenAuthorizationScheme } from "better-auth/oauth2";
20
20
  import { compactVerify, createLocalJWKSet, decodeJwt, jwtVerify } from "jose";
@@ -29,6 +29,8 @@ async function consentEndpoint(ctx, opts, authorize) {
29
29
  });
30
30
  const query = new URLSearchParams(_query);
31
31
  const originalRequestedScopes = query.get("scope")?.split(" ") ?? [];
32
+ const supportedClaims = getSupportedClaims(opts);
33
+ const originalRequestedUserInfoClaims = getRequestedUserInfoClaims(query.get("claims"), supportedClaims);
32
34
  const clientId = query.get("client_id");
33
35
  if (!clientId) throw new APIError("BAD_REQUEST", {
34
36
  error_description: "client_id is required",
@@ -41,6 +43,12 @@ async function consentEndpoint(ctx, opts, authorize) {
41
43
  error: "invalid_request"
42
44
  });
43
45
  }
46
+ const acceptedClaims = ctx.body.claims;
47
+ const acceptedUserInfoClaims = acceptedClaims !== void 0 ? getRequestedUserInfoClaims(acceptedClaims, supportedClaims) : originalRequestedUserInfoClaims;
48
+ if (acceptedClaims !== void 0 && !acceptedUserInfoClaims.every((claim) => originalRequestedUserInfoClaims.includes(claim))) throw new APIError("BAD_REQUEST", {
49
+ error_description: "Claim not originally requested",
50
+ error: "invalid_request"
51
+ });
44
52
  if (!(ctx.body.accept === true)) return {
45
53
  redirect: true,
46
54
  url: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0, getIssuer(ctx, opts))
@@ -81,6 +89,7 @@ async function consentEndpoint(ctx, opts, authorize) {
81
89
  clientId,
82
90
  userId: session?.user.id,
83
91
  scopes: requestedScopes ?? originalRequestedScopes,
92
+ requestedUserInfoClaims: acceptedUserInfoClaims,
84
93
  createdAt: /* @__PURE__ */ new Date(iat * 1e3),
85
94
  updatedAt: /* @__PURE__ */ new Date(iat * 1e3),
86
95
  resources: resource.length ? resource : void 0,
@@ -95,6 +104,7 @@ async function consentEndpoint(ctx, opts, authorize) {
95
104
  update: {
96
105
  resources: consent.resources,
97
106
  scopes: consent.scopes,
107
+ requestedUserInfoClaims: consent.requestedUserInfoClaims,
98
108
  updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
99
109
  }
100
110
  }) : await ctx.context.adapter.create({
@@ -105,6 +115,11 @@ async function consentEndpoint(ctx, opts, authorize) {
105
115
  }
106
116
  });
107
117
  if (requestedScopes) query.set("scope", consent.scopes.join(" "));
118
+ if (acceptedClaims !== void 0) {
119
+ const claimsRequest = filterClaimsRequestUserInfoClaims(query.get("claims"), acceptedUserInfoClaims);
120
+ if (claimsRequest) query.set("claims", JSON.stringify(claimsRequest));
121
+ else query.delete("claims");
122
+ }
108
123
  ctx?.headers?.set("accept", "application/json");
109
124
  let authorizationQuery = removePromptFromQuery(query, "consent");
110
125
  if (hasSatisfiedLoginPrompt) {
@@ -510,9 +525,11 @@ function oidcServerMetadata(ctx, opts) {
510
525
  const { jwtPluginOptions, clientDiscoveries, authMetadata } = buildAuthServerMetadata(ctx, opts);
511
526
  const metadata = {
512
527
  ...authMetadata,
513
- claims_supported: opts?.advertisedMetadata?.claims_supported ?? opts?.claims ?? [],
528
+ claims_supported: getSupportedClaims(opts),
529
+ claims_parameter_supported: true,
514
530
  userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
515
531
  subject_types_supported: opts.pairwiseSecret ? ["public", "pairwise"] : ["public"],
532
+ acr_values_supported: ["0"],
516
533
  id_token_signing_alg_values_supported: (() => {
517
534
  if (opts.disableJwtPlugin) return ["HS256"];
518
535
  const primary = jwtPluginOptions?.jwks?.keyPairConfig?.alg ?? "EdDSA";
@@ -520,7 +537,8 @@ function oidcServerMetadata(ctx, opts) {
520
537
  return Array.from(new Set([primary, ...extras]));
521
538
  })(),
522
539
  end_session_endpoint: `${baseURL}/oauth2/end-session`,
523
- acr_values_supported: ["urn:mace:incommon:iap:bronze"],
540
+ request_parameter_supported: false,
541
+ request_uri_parameter_supported: false,
524
542
  prompt_values_supported: [
525
543
  "login",
526
544
  "consent",
@@ -768,22 +786,27 @@ async function authorizeInitialAccessToken(ctx, opts, clientMetadata) {
768
786
  }
769
787
  //#endregion
770
788
  //#region src/register.ts
771
- /**
772
- * Resolves the auth method and type for unauthenticated DCR.
773
- * Overrides confidential methods to "none" per RFC 7591 Section 3.2.1.
774
- * When overriding, clears type "web" since it is only valid for confidential clients.
775
- */
776
- function resolveUnauthenticatedAuth(body) {
777
- if (body.token_endpoint_auth_method === "none") return {
778
- tokenEndpointAuthMethod: "none",
779
- type: body.type
780
- };
781
- return {
782
- tokenEndpointAuthMethod: "none",
783
- type: body.type === "web" ? void 0 : body.type
784
- };
785
- }
786
789
  const DEFAULT_REGISTRATION_GRANT_TYPES = ["authorization_code"];
790
+ const PRIVATE_JWK_MEMBER_NAMES = [
791
+ "d",
792
+ "p",
793
+ "q",
794
+ "dp",
795
+ "dq",
796
+ "qi",
797
+ "oth"
798
+ ];
799
+ function hasStringJwkMember(key, memberName) {
800
+ return typeof key[memberName] === "string" && key[memberName].length > 0;
801
+ }
802
+ function isSupportedPublicJwk(key) {
803
+ switch (key.kty) {
804
+ case "RSA": return hasStringJwkMember(key, "n") && hasStringJwkMember(key, "e");
805
+ case "EC": return hasStringJwkMember(key, "crv") && hasStringJwkMember(key, "x") && hasStringJwkMember(key, "y");
806
+ case "OKP": return hasStringJwkMember(key, "crv") && hasStringJwkMember(key, "x");
807
+ default: return false;
808
+ }
809
+ }
787
810
  function resolveRegistrationGrantTypes(client) {
788
811
  const grantTypes = client.grant_types ?? [...DEFAULT_REGISTRATION_GRANT_TYPES];
789
812
  if (grantTypes.length > 0) return grantTypes;
@@ -805,6 +828,23 @@ function applyOAuthClientRegistrationDefaults(client) {
805
828
  response_types: resolveRegistrationResponseTypes(client, grantTypes)
806
829
  };
807
830
  }
831
+ function validatePublicJwks(jwks) {
832
+ const keys = Array.isArray(jwks) ? jwks : jwks.keys;
833
+ if (!Array.isArray(keys) || keys.length === 0) throw new APIError("BAD_REQUEST", {
834
+ error: "invalid_client_metadata",
835
+ error_description: "jwks must be a non-empty array of JWK objects or a JWKS document {keys:[...]}"
836
+ });
837
+ for (const key of keys) {
838
+ if (key.kty === "oct" || "k" in key || PRIVATE_JWK_MEMBER_NAMES.some((name) => name in key)) throw new APIError("BAD_REQUEST", {
839
+ error: "invalid_client_metadata",
840
+ error_description: "jwks must contain only public asymmetric keys"
841
+ });
842
+ if (!isSupportedPublicJwk(key)) throw new APIError("BAD_REQUEST", {
843
+ error: "invalid_client_metadata",
844
+ error_description: "jwks keys must be supported public JWKs with required key parameters"
845
+ });
846
+ }
847
+ }
808
848
  async function registerEndpoint(ctx, opts) {
809
849
  const body = ctx.body;
810
850
  if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
@@ -823,9 +863,6 @@ async function registerEndpoint(ctx, opts) {
823
863
  error: "invalid_client_metadata",
824
864
  error_description: "client_credentials grant requires authenticated registration"
825
865
  });
826
- const resolved = resolveUnauthenticatedAuth(body);
827
- body.token_endpoint_auth_method = resolved.tokenEndpointAuthMethod;
828
- body.type = resolved.type;
829
866
  }
830
867
  if (!body.scope) body.scope = (opts.clientRegistrationDefaultScopes ?? opts.scopes)?.join(" ");
831
868
  const requestedResources = Array.isArray(body.resources) ? [...new Set(body.resources.filter((resource) => typeof resource === "string" && resource.length > 0))] : [];
@@ -917,12 +954,7 @@ async function checkOAuthClient(client, opts, settings) {
917
954
  error: "invalid_client_metadata",
918
955
  error_description: `pkce is required for registered clients.`
919
956
  });
920
- const usesAssertionKeyMaterial = tokenEndpointAuthMethod === "private_key_jwt" || isExtensionTokenEndpointAuthMethod(opts, tokenEndpointAuthMethod);
921
957
  if (clientWithDefaults.jwks || clientWithDefaults.jwks_uri) {
922
- if (!usesAssertionKeyMaterial) throw new APIError("BAD_REQUEST", {
923
- error: "invalid_client_metadata",
924
- error_description: "jwks and jwks_uri are only allowed with private_key_jwt or an assertion-based authentication method"
925
- });
926
958
  if (clientWithDefaults.jwks && clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
927
959
  error: "invalid_client_metadata",
928
960
  error_description: "jwks and jwks_uri are mutually exclusive"
@@ -948,13 +980,7 @@ async function checkOAuthClient(client, opts, settings) {
948
980
  error_description: "jwks_uri must be a valid URL"
949
981
  });
950
982
  }
951
- if (clientWithDefaults.jwks) {
952
- const keys = Array.isArray(clientWithDefaults.jwks) ? clientWithDefaults.jwks : clientWithDefaults.jwks.keys;
953
- if (!Array.isArray(keys) || keys.length === 0) throw new APIError("BAD_REQUEST", {
954
- error: "invalid_client_metadata",
955
- error_description: "jwks must be a non-empty array of JWK objects or a JWKS document {keys:[...]}"
956
- });
957
- }
983
+ if (clientWithDefaults.jwks) validatePublicJwks(clientWithDefaults.jwks);
958
984
  }
959
985
  if (tokenEndpointAuthMethod === "private_key_jwt" && !clientWithDefaults.jwks && !clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
960
986
  error: "invalid_client_metadata",
@@ -1007,6 +1033,8 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1007
1033
  const clientId = opts.generateClientId?.() || generateRandomString(32, "a-z", "A-Z");
1008
1034
  const clientSecret = isPublic || isPrivateKeyJwt || isExtensionAuthMethod ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
1009
1035
  const storedClientSecret = clientSecret ? await storeClientSecret(ctx, opts, clientSecret) : void 0;
1036
+ const isPKCEOptionalForRegisteredClient = settings.isRegister && !isPublic && opts.clientRegistrationRequirePKCE === false;
1037
+ const requirePKCE = body.require_pkce ?? (isPKCEOptionalForRegisteredClient ? false : void 0);
1010
1038
  const iat = Math.floor(Date.now() / 1e3);
1011
1039
  const referenceId = settings.referenceId ?? (session && opts.clientReference ? await opts.clientReference({
1012
1040
  user: session.user,
@@ -1020,6 +1048,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1020
1048
  client_id: clientId,
1021
1049
  client_secret: storedClientSecret,
1022
1050
  client_id_issued_at: iat,
1051
+ require_pkce: requirePKCE,
1023
1052
  public: isPublic,
1024
1053
  user_id: referenceId ? void 0 : session?.session.userId,
1025
1054
  reference_id: referenceId
@@ -1135,7 +1164,7 @@ function schemaToOAuth(input) {
1135
1164
  contacts: contacts ?? void 0,
1136
1165
  tos_uri: tos ?? void 0,
1137
1166
  policy_uri: policy ?? void 0,
1138
- jwks: jwks ? JSON.parse(jwks).keys : void 0,
1167
+ jwks: jwks ? JSON.parse(jwks) : void 0,
1139
1168
  jwks_uri: jwksUri ?? void 0,
1140
1169
  software_id: softwareId ?? void 0,
1141
1170
  software_version: softwareVersion ?? void 0,
@@ -2781,10 +2810,19 @@ const schema = {
2781
2810
  type: "string",
2782
2811
  required: false
2783
2812
  },
2813
+ authorizationCodeId: {
2814
+ type: "string",
2815
+ required: false,
2816
+ index: true
2817
+ },
2784
2818
  resources: {
2785
2819
  type: "string[]",
2786
2820
  required: false
2787
2821
  },
2822
+ requestedUserInfoClaims: {
2823
+ type: "string[]",
2824
+ required: false
2825
+ },
2788
2826
  expiresAt: { type: "date" },
2789
2827
  createdAt: { type: "date" },
2790
2828
  revoked: {
@@ -2843,10 +2881,19 @@ const schema = {
2843
2881
  type: "string",
2844
2882
  required: false
2845
2883
  },
2884
+ authorizationCodeId: {
2885
+ type: "string",
2886
+ required: false,
2887
+ index: true
2888
+ },
2846
2889
  resources: {
2847
2890
  type: "string[]",
2848
2891
  required: false
2849
2892
  },
2893
+ requestedUserInfoClaims: {
2894
+ type: "string[]",
2895
+ required: false
2896
+ },
2850
2897
  refreshId: {
2851
2898
  type: "string",
2852
2899
  required: false,
@@ -2901,6 +2948,10 @@ const schema = {
2901
2948
  type: "string[]",
2902
2949
  required: false
2903
2950
  },
2951
+ requestedUserInfoClaims: {
2952
+ type: "string[]",
2953
+ required: false
2954
+ },
2904
2955
  scopes: {
2905
2956
  type: "string[]",
2906
2957
  required: true
@@ -2965,13 +3016,7 @@ const oauthProvider = (options) => {
2965
3016
  "sid",
2966
3017
  "scope",
2967
3018
  "azp",
2968
- ...scopes.has("email") ? ["email", "email_verified"] : [],
2969
- ...scopes.has("profile") ? [
2970
- "name",
2971
- "picture",
2972
- "family_name",
2973
- "given_name"
2974
- ] : []
3019
+ ...STANDARD_CLAIM_NAMES.filter((name) => scopes.has(STANDARD_CLAIMS[name].scope))
2975
3020
  ]);
2976
3021
  const opts = {
2977
3022
  codeExpiresIn: 600,
@@ -3034,135 +3079,134 @@ const oauthProvider = (options) => {
3034
3079
  if (isOpenIdConfigRequest) return { response: createMetadataResponse(oidcServerMetadata(endpointCtx, opts)) };
3035
3080
  };
3036
3081
  const oauth2AuthorizeEndpoint = createOAuthEndpoint("/oauth2/authorize", {
3037
- method: "GET",
3038
- query: authorizationQuerySchema,
3082
+ method: ["GET", "POST"],
3083
+ body: z.object({}).passthrough(),
3039
3084
  redirectOnError: authorizeRedirectOnError(opts),
3040
- errorCodesByField: {
3041
- response_type: { invalid: "unsupported_response_type" },
3042
- resource: { invalid: "invalid_target" }
3043
- },
3044
- metadata: { openapi: {
3045
- description: "Authorize an OAuth2 request",
3046
- parameters: [
3047
- {
3048
- name: "response_type",
3049
- in: "query",
3050
- required: false,
3051
- schema: { type: "string" },
3052
- description: "OAuth2 response type (e.g., 'code')"
3053
- },
3054
- {
3055
- name: "client_id",
3056
- in: "query",
3057
- required: true,
3058
- schema: { type: "string" },
3059
- description: "OAuth2 client ID"
3060
- },
3061
- {
3062
- name: "redirect_uri",
3063
- in: "query",
3064
- required: false,
3065
- schema: {
3066
- type: "string",
3067
- format: "uri"
3085
+ metadata: {
3086
+ allowedMediaTypes: ["application/x-www-form-urlencoded"],
3087
+ openapi: {
3088
+ description: "Authorize an OAuth 2.1 request from query parameters or an application/x-www-form-urlencoded POST body",
3089
+ parameters: [
3090
+ {
3091
+ name: "response_type",
3092
+ in: "query",
3093
+ required: false,
3094
+ schema: { type: "string" },
3095
+ description: "OAuth 2.1 response type (e.g., 'code')"
3068
3096
  },
3069
- description: "OAuth2 redirect URI"
3070
- },
3071
- {
3072
- name: "scope",
3073
- in: "query",
3074
- required: false,
3075
- schema: { type: "string" },
3076
- description: "OAuth2 scopes (space-separated)"
3077
- },
3078
- {
3079
- name: "state",
3080
- in: "query",
3081
- required: false,
3082
- schema: { type: "string" },
3083
- description: "OAuth2 state parameter"
3084
- },
3085
- {
3086
- name: "request_uri",
3087
- in: "query",
3088
- required: false,
3089
- schema: { type: "string" },
3090
- description: "Pushed Authorization Request URI referencing stored parameters"
3091
- },
3092
- {
3093
- name: "code_challenge",
3094
- in: "query",
3095
- required: false,
3096
- schema: { type: "string" },
3097
- description: "PKCE code challenge"
3098
- },
3099
- {
3100
- name: "code_challenge_method",
3101
- in: "query",
3102
- required: false,
3103
- schema: { type: "string" },
3104
- description: "PKCE code challenge method"
3105
- },
3106
- {
3107
- name: "nonce",
3108
- in: "query",
3109
- required: false,
3110
- schema: { type: "string" },
3111
- description: "OpenID Connect nonce"
3112
- },
3113
- {
3114
- name: "max_age",
3115
- in: "query",
3116
- required: false,
3117
- schema: {
3118
- type: "integer",
3119
- minimum: 0
3097
+ {
3098
+ name: "client_id",
3099
+ in: "query",
3100
+ required: true,
3101
+ schema: { type: "string" },
3102
+ description: "OAuth 2.1 client ID"
3120
3103
  },
3121
- description: "Maximum authentication age in seconds; forces re-authentication when exceeded"
3122
- },
3123
- {
3124
- name: "resource",
3125
- in: "query",
3126
- required: false,
3127
- schema: {
3128
- type: "array",
3129
- items: { type: "string" }
3130
- },
3131
- description: "Requested protected resource(s) for the access token. May be supplied multiple times as repeated 'resource' query parameters (RFC 8707) or as an array of strings."
3132
- },
3133
- {
3134
- name: "prompt",
3135
- in: "query",
3136
- required: false,
3137
- schema: { type: "string" },
3138
- description: "OAuth2 prompt parameter"
3139
- }
3140
- ],
3141
- responses: {
3142
- "302": {
3143
- description: "Redirect to client with code or error",
3144
- headers: { Location: {
3145
- description: "Redirect URI with code or error",
3104
+ {
3105
+ name: "redirect_uri",
3106
+ in: "query",
3107
+ required: false,
3146
3108
  schema: {
3147
3109
  type: "string",
3148
3110
  format: "uri"
3149
- }
3150
- } }
3151
- },
3152
- "400": {
3153
- description: "Invalid request",
3154
- content: { "application/json": { schema: {
3155
- type: "object",
3156
- properties: {
3157
- error: { type: "string" },
3158
- error_description: { type: "string" },
3159
- state: { type: "string" }
3160
3111
  },
3161
- required: ["error"]
3162
- } } }
3112
+ description: "OAuth 2.1 redirect URI"
3113
+ },
3114
+ {
3115
+ name: "scope",
3116
+ in: "query",
3117
+ required: false,
3118
+ schema: { type: "string" },
3119
+ description: "OAuth 2.1 scopes (space-separated)"
3120
+ },
3121
+ {
3122
+ name: "state",
3123
+ in: "query",
3124
+ required: false,
3125
+ schema: { type: "string" },
3126
+ description: "OAuth 2.1 state parameter"
3127
+ },
3128
+ {
3129
+ name: "request_uri",
3130
+ in: "query",
3131
+ required: false,
3132
+ schema: { type: "string" },
3133
+ description: "Pushed Authorization Request URI referencing stored parameters"
3134
+ },
3135
+ {
3136
+ name: "code_challenge",
3137
+ in: "query",
3138
+ required: false,
3139
+ schema: { type: "string" },
3140
+ description: "PKCE code challenge"
3141
+ },
3142
+ {
3143
+ name: "code_challenge_method",
3144
+ in: "query",
3145
+ required: false,
3146
+ schema: { type: "string" },
3147
+ description: "PKCE code challenge method"
3148
+ },
3149
+ {
3150
+ name: "nonce",
3151
+ in: "query",
3152
+ required: false,
3153
+ schema: { type: "string" },
3154
+ description: "OpenID Connect nonce"
3155
+ },
3156
+ {
3157
+ name: "max_age",
3158
+ in: "query",
3159
+ required: false,
3160
+ schema: {
3161
+ type: "integer",
3162
+ minimum: 0
3163
+ },
3164
+ description: "Maximum authentication age in seconds; forces re-authentication when exceeded"
3165
+ },
3166
+ {
3167
+ name: "resource",
3168
+ in: "query",
3169
+ required: false,
3170
+ schema: {
3171
+ type: "array",
3172
+ items: { type: "string" }
3173
+ },
3174
+ description: "Requested protected resource(s) for the access token. May be supplied multiple times as repeated 'resource' query parameters (RFC 8707) or as an array of strings."
3175
+ },
3176
+ {
3177
+ name: "prompt",
3178
+ in: "query",
3179
+ required: false,
3180
+ schema: { type: "string" },
3181
+ description: "OAuth2 prompt parameter"
3182
+ }
3183
+ ],
3184
+ responses: {
3185
+ "302": {
3186
+ description: "Redirect to client with code or error",
3187
+ headers: { Location: {
3188
+ description: "Redirect URI with code or error",
3189
+ schema: {
3190
+ type: "string",
3191
+ format: "uri"
3192
+ }
3193
+ } }
3194
+ },
3195
+ "400": {
3196
+ description: "Invalid request",
3197
+ content: { "application/json": { schema: {
3198
+ type: "object",
3199
+ properties: {
3200
+ error: { type: "string" },
3201
+ error_description: { type: "string" },
3202
+ state: { type: "string" }
3203
+ },
3204
+ required: ["error"]
3205
+ } } }
3206
+ }
3163
3207
  }
3164
3208
  }
3165
- } }
3209
+ }
3166
3210
  }, async (ctx) => {
3167
3211
  return authorizeEndpoint(ctx, opts, ctx.authorizeSettings ?? { isAuthorize: true });
3168
3212
  });
@@ -3278,6 +3322,7 @@ const oauthProvider = (options) => {
3278
3322
  body: z.object({
3279
3323
  accept: z.boolean().meta({ description: "Accept or deny user consent for a set of scopes" }),
3280
3324
  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." }),
3325
+ claims: claimsRequestParameterSchema.optional().meta({ description: "Accepted OIDC claims request object. If none is provided, then all originally requested claims are accepted." }),
3281
3326
  oauth_query: z.string().optional().meta({ description: "The redirected page's query parameters" })
3282
3327
  }),
3283
3328
  use: [sessionMiddleware],
@@ -3658,8 +3703,10 @@ const oauthProvider = (options) => {
3658
3703
  }),
3659
3704
  oauth2UserInfo: createAuthEndpoint("/oauth2/userinfo", {
3660
3705
  method: ["GET", "POST"],
3706
+ body: z.object({ access_token: z.string().optional() }).passthrough().optional(),
3661
3707
  metadata: {
3662
3708
  noStore: true,
3709
+ allowedMediaTypes: ["application/x-www-form-urlencoded"],
3663
3710
  openapi: {
3664
3711
  description: "Get OpenID Connect user information (UserInfo endpoint)",
3665
3712
  security: [{ bearerAuth: [] }, { OAuth2: [
@@ -4037,6 +4084,24 @@ function deriveResponseMode(raw) {
4037
4084
  if (responseType && /\b(token|id_token)\b/.test(responseType)) return "fragment";
4038
4085
  return "query";
4039
4086
  }
4087
+ const authorizationQueryErrorCodesByField = {
4088
+ response_type: { invalid: "unsupported_response_type" },
4089
+ resource: { invalid: "invalid_target" }
4090
+ };
4091
+ function isRecord(value) {
4092
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4093
+ }
4094
+ function getStringParameter(value) {
4095
+ return typeof value === "string" ? value : void 0;
4096
+ }
4097
+ function getAuthorizationRequestParameters(ctx, settings) {
4098
+ const source = ctx.method === "POST" && settings?.isAuthorize === true ? ctx.body : ctx.query;
4099
+ return { ...isRecord(source) ? source : {} };
4100
+ }
4101
+ function isAcrValuesRequestSupported(acrValues) {
4102
+ if (acrValues === void 0) return true;
4103
+ return acrValues.split(" ").filter(Boolean).includes("0");
4104
+ }
4040
4105
  const handleRedirect = (ctx, uri) => {
4041
4106
  const fromFetch = isBrowserFetchRequest(ctx.request?.headers);
4042
4107
  const acceptJson = ctx.headers?.get("accept")?.includes("application/json");
@@ -4153,31 +4218,68 @@ async function authorizeEndpoint(ctx, opts, settings) {
4153
4218
  error: "invalid_request"
4154
4219
  });
4155
4220
  const request = ctx.request;
4156
- let query = ctx.query;
4157
- if (query.request_uri) {
4158
- if (!opts.requestUriResolver) return handleRedirect(ctx, getErrorURL(ctx, "invalid_request_uri", "request_uri not supported"));
4221
+ let query = getAuthorizationRequestParameters(ctx, settings);
4222
+ ctx.query = query;
4223
+ const requestObject = getStringParameter(query.request);
4224
+ const requestUri = getStringParameter(query.request_uri);
4225
+ if (requestObject !== void 0 && requestUri !== void 0) return authorizeRedirectOnError(opts)({
4226
+ error: "invalid_request",
4227
+ error_description: "request and request_uri cannot be used together",
4228
+ ctx
4229
+ });
4230
+ if (requestObject !== void 0) return authorizeRedirectOnError(opts)({
4231
+ error: "request_not_supported",
4232
+ error_description: "request object not supported",
4233
+ ctx
4234
+ });
4235
+ if (requestUri !== void 0) {
4236
+ const clientId = getStringParameter(query.client_id);
4237
+ if (!clientId) return authorizeRedirectOnError(opts)({
4238
+ error: "invalid_request",
4239
+ error_description: "client_id is required",
4240
+ ctx
4241
+ });
4242
+ if (!opts.requestUriResolver) return authorizeRedirectOnError(opts)({
4243
+ error: "request_uri_not_supported",
4244
+ error_description: "request_uri not supported",
4245
+ ctx
4246
+ });
4159
4247
  const resolvedParams = await opts.requestUriResolver({
4160
- requestUri: query.request_uri,
4161
- clientId: query.client_id ?? "",
4248
+ requestUri,
4249
+ clientId,
4250
+ ctx
4251
+ });
4252
+ if (!resolvedParams) return authorizeRedirectOnError(opts)({
4253
+ error: "invalid_request_uri",
4254
+ error_description: "request_uri is invalid or expired",
4162
4255
  ctx
4163
4256
  });
4164
- if (!resolvedParams) return handleRedirect(ctx, getErrorURL(ctx, "invalid_request_uri", "request_uri is invalid or expired"));
4165
4257
  const urlClientId = query.client_id;
4166
4258
  query = resolvedParams;
4167
4259
  if (urlClientId) query.client_id = urlClientId;
4168
4260
  }
4169
4261
  ctx.query = query;
4170
4262
  const parsedQuery = authorizationQuerySchema.safeParse(query);
4171
- if (!parsedQuery.success) return authorizeRedirectOnError(opts)({
4263
+ if (!parsedQuery.success) {
4264
+ const mappedError = mapIssuesToOAuthError(parsedQuery.error.issues, authorizationQueryErrorCodesByField);
4265
+ return authorizeRedirectOnError(opts)({
4266
+ ...mappedError,
4267
+ ctx
4268
+ });
4269
+ }
4270
+ query = parsedQuery.data;
4271
+ ctx.query = query;
4272
+ if (!isAcrValuesRequestSupported(query.acr_values)) return authorizeRedirectOnError(opts)({
4172
4273
  error: "invalid_request",
4173
- error_description: "invalid authorization request",
4274
+ error_description: "unsupported acr_values",
4174
4275
  ctx
4175
4276
  });
4176
- query = parsedQuery.data;
4177
- ctx.query = query;
4178
4277
  await oAuthState.set({ query: serializeAuthorizationQuery(query).toString() });
4179
- if (!query.client_id) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "client_id is required"));
4180
- if (!query.response_type) return handleRedirect(ctx, getErrorURL(ctx, "invalid_request", "response_type is required"));
4278
+ if (!query.response_type) return authorizeRedirectOnError(opts)({
4279
+ error: "invalid_request",
4280
+ error_description: "response_type is required",
4281
+ ctx
4282
+ });
4181
4283
  const promptSet = ctx.query?.prompt ? parsePrompt(ctx.query?.prompt) : void 0;
4182
4284
  const promptNone = promptSet?.has("none") ?? false;
4183
4285
  if (promptSet?.has("select_account") && !opts.selectAccount?.page) return handleRedirect(ctx, getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
@@ -4199,6 +4301,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
4199
4301
  requestedScopes = client.scopes ?? opts.scopes ?? [];
4200
4302
  query.scope = requestedScopes.join(" ");
4201
4303
  }
4304
+ const requestedUserInfoClaims = getRequestedUserInfoClaims(query.claims, getSupportedClaims(opts));
4202
4305
  if (query.resource !== void 0) try {
4203
4306
  await resolveResourcePolicy(ctx, opts, {
4204
4307
  resource: query.resource,
@@ -4213,7 +4316,10 @@ async function authorizeEndpoint(ctx, opts, settings) {
4213
4316
  }
4214
4317
  throw err;
4215
4318
  }
4216
- const pkceRequired = isPKCERequired(client, requestedScopes);
4319
+ const pkceRequired = isPKCERequired(client, {
4320
+ scopes: requestedScopes,
4321
+ nonce: query.nonce
4322
+ });
4217
4323
  if (pkceRequired) {
4218
4324
  if (!query.code_challenge || !query.code_challenge_method) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", pkceRequired.valueOf(), query.state, getIssuer(ctx, opts)));
4219
4325
  }
@@ -4300,7 +4406,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
4300
4406
  }] : []
4301
4407
  ]
4302
4408
  });
4303
- if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val))) {
4409
+ if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val)) || !requestedUserInfoClaims.every((claim) => (consent.requestedUserInfoClaims ?? []).includes(claim))) {
4304
4410
  if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "consent_required", "End-User consent is required");
4305
4411
  return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
4306
4412
  }
@@ -4324,6 +4430,7 @@ function serializeAuthorizationQuery(query) {
4324
4430
  for (const [key, value] of Object.entries(query)) {
4325
4431
  if (value == null) continue;
4326
4432
  if (Array.isArray(value)) for (const v of value) params.append(key, String(v));
4433
+ else if (key === "claims" && typeof value === "object") params.set(key, JSON.stringify(value));
4327
4434
  else params.set(key, String(value));
4328
4435
  }
4329
4436
  return params;