@better-auth/oauth-provider 1.7.0-beta.8 → 1.7.0-rc.0

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-DvHp2a64.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-nSGiIxZx.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,16 +2810,37 @@ 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: {
2791
2829
  type: "date",
2792
2830
  required: false
2793
2831
  },
2832
+ rotatedAt: {
2833
+ type: "date",
2834
+ required: false
2835
+ },
2836
+ rotationReplayResponse: {
2837
+ type: "string",
2838
+ required: false
2839
+ },
2840
+ rotationReplayExpiresAt: {
2841
+ type: "date",
2842
+ required: false
2843
+ },
2794
2844
  authTime: {
2795
2845
  type: "date",
2796
2846
  required: false
@@ -2843,10 +2893,19 @@ const schema = {
2843
2893
  type: "string",
2844
2894
  required: false
2845
2895
  },
2896
+ authorizationCodeId: {
2897
+ type: "string",
2898
+ required: false,
2899
+ index: true
2900
+ },
2846
2901
  resources: {
2847
2902
  type: "string[]",
2848
2903
  required: false
2849
2904
  },
2905
+ requestedUserInfoClaims: {
2906
+ type: "string[]",
2907
+ required: false
2908
+ },
2850
2909
  refreshId: {
2851
2910
  type: "string",
2852
2911
  required: false,
@@ -2901,6 +2960,10 @@ const schema = {
2901
2960
  type: "string[]",
2902
2961
  required: false
2903
2962
  },
2963
+ requestedUserInfoClaims: {
2964
+ type: "string[]",
2965
+ required: false
2966
+ },
2904
2967
  scopes: {
2905
2968
  type: "string[]",
2906
2969
  required: true
@@ -2965,19 +3028,14 @@ const oauthProvider = (options) => {
2965
3028
  "sid",
2966
3029
  "scope",
2967
3030
  "azp",
2968
- ...scopes.has("email") ? ["email", "email_verified"] : [],
2969
- ...scopes.has("profile") ? [
2970
- "name",
2971
- "picture",
2972
- "family_name",
2973
- "given_name"
2974
- ] : []
3031
+ ...STANDARD_CLAIM_NAMES.filter((name) => scopes.has(STANDARD_CLAIMS[name].scope))
2975
3032
  ]);
2976
3033
  const opts = {
2977
3034
  codeExpiresIn: 600,
2978
3035
  accessTokenExpiresIn: 3600,
2979
3036
  m2mAccessTokenExpiresIn: 3600,
2980
3037
  refreshTokenExpiresIn: 2592e3,
3038
+ refreshTokenReuseInterval: 0,
2981
3039
  allowUnauthenticatedClientRegistration: false,
2982
3040
  allowDynamicClientRegistration: false,
2983
3041
  disableJwtPlugin: false,
@@ -3034,135 +3092,134 @@ const oauthProvider = (options) => {
3034
3092
  if (isOpenIdConfigRequest) return { response: createMetadataResponse(oidcServerMetadata(endpointCtx, opts)) };
3035
3093
  };
3036
3094
  const oauth2AuthorizeEndpoint = createOAuthEndpoint("/oauth2/authorize", {
3037
- method: "GET",
3038
- query: authorizationQuerySchema,
3095
+ method: ["GET", "POST"],
3096
+ body: z.object({}).passthrough(),
3039
3097
  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"
3098
+ metadata: {
3099
+ allowedMediaTypes: ["application/x-www-form-urlencoded"],
3100
+ openapi: {
3101
+ description: "Authorize an OAuth 2.1 request from query parameters or an application/x-www-form-urlencoded POST body",
3102
+ parameters: [
3103
+ {
3104
+ name: "response_type",
3105
+ in: "query",
3106
+ required: false,
3107
+ schema: { type: "string" },
3108
+ description: "OAuth 2.1 response type (e.g., 'code')"
3068
3109
  },
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
3110
+ {
3111
+ name: "client_id",
3112
+ in: "query",
3113
+ required: true,
3114
+ schema: { type: "string" },
3115
+ description: "OAuth 2.1 client ID"
3120
3116
  },
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",
3117
+ {
3118
+ name: "redirect_uri",
3119
+ in: "query",
3120
+ required: false,
3146
3121
  schema: {
3147
3122
  type: "string",
3148
3123
  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
3124
  },
3161
- required: ["error"]
3162
- } } }
3125
+ description: "OAuth 2.1 redirect URI"
3126
+ },
3127
+ {
3128
+ name: "scope",
3129
+ in: "query",
3130
+ required: false,
3131
+ schema: { type: "string" },
3132
+ description: "OAuth 2.1 scopes (space-separated)"
3133
+ },
3134
+ {
3135
+ name: "state",
3136
+ in: "query",
3137
+ required: false,
3138
+ schema: { type: "string" },
3139
+ description: "OAuth 2.1 state parameter"
3140
+ },
3141
+ {
3142
+ name: "request_uri",
3143
+ in: "query",
3144
+ required: false,
3145
+ schema: { type: "string" },
3146
+ description: "Pushed Authorization Request URI referencing stored parameters"
3147
+ },
3148
+ {
3149
+ name: "code_challenge",
3150
+ in: "query",
3151
+ required: false,
3152
+ schema: { type: "string" },
3153
+ description: "PKCE code challenge"
3154
+ },
3155
+ {
3156
+ name: "code_challenge_method",
3157
+ in: "query",
3158
+ required: false,
3159
+ schema: { type: "string" },
3160
+ description: "PKCE code challenge method"
3161
+ },
3162
+ {
3163
+ name: "nonce",
3164
+ in: "query",
3165
+ required: false,
3166
+ schema: { type: "string" },
3167
+ description: "OpenID Connect nonce"
3168
+ },
3169
+ {
3170
+ name: "max_age",
3171
+ in: "query",
3172
+ required: false,
3173
+ schema: {
3174
+ type: "integer",
3175
+ minimum: 0
3176
+ },
3177
+ description: "Maximum authentication age in seconds; forces re-authentication when exceeded"
3178
+ },
3179
+ {
3180
+ name: "resource",
3181
+ in: "query",
3182
+ required: false,
3183
+ schema: {
3184
+ type: "array",
3185
+ items: { type: "string" }
3186
+ },
3187
+ 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."
3188
+ },
3189
+ {
3190
+ name: "prompt",
3191
+ in: "query",
3192
+ required: false,
3193
+ schema: { type: "string" },
3194
+ description: "OAuth2 prompt parameter"
3195
+ }
3196
+ ],
3197
+ responses: {
3198
+ "302": {
3199
+ description: "Redirect to client with code or error",
3200
+ headers: { Location: {
3201
+ description: "Redirect URI with code or error",
3202
+ schema: {
3203
+ type: "string",
3204
+ format: "uri"
3205
+ }
3206
+ } }
3207
+ },
3208
+ "400": {
3209
+ description: "Invalid request",
3210
+ content: { "application/json": { schema: {
3211
+ type: "object",
3212
+ properties: {
3213
+ error: { type: "string" },
3214
+ error_description: { type: "string" },
3215
+ state: { type: "string" }
3216
+ },
3217
+ required: ["error"]
3218
+ } } }
3219
+ }
3163
3220
  }
3164
3221
  }
3165
- } }
3222
+ }
3166
3223
  }, async (ctx) => {
3167
3224
  return authorizeEndpoint(ctx, opts, ctx.authorizeSettings ?? { isAuthorize: true });
3168
3225
  });
@@ -3278,6 +3335,7 @@ const oauthProvider = (options) => {
3278
3335
  body: z.object({
3279
3336
  accept: z.boolean().meta({ description: "Accept or deny user consent for a set of scopes" }),
3280
3337
  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." }),
3338
+ claims: claimsRequestParameterSchema.optional().meta({ description: "Accepted OIDC claims request object. If none is provided, then all originally requested claims are accepted." }),
3281
3339
  oauth_query: z.string().optional().meta({ description: "The redirected page's query parameters" })
3282
3340
  }),
3283
3341
  use: [sessionMiddleware],
@@ -3658,8 +3716,10 @@ const oauthProvider = (options) => {
3658
3716
  }),
3659
3717
  oauth2UserInfo: createAuthEndpoint("/oauth2/userinfo", {
3660
3718
  method: ["GET", "POST"],
3719
+ body: z.object({ access_token: z.string().optional() }).passthrough().optional(),
3661
3720
  metadata: {
3662
3721
  noStore: true,
3722
+ allowedMediaTypes: ["application/x-www-form-urlencoded"],
3663
3723
  openapi: {
3664
3724
  description: "Get OpenID Connect user information (UserInfo endpoint)",
3665
3725
  security: [{ bearerAuth: [] }, { OAuth2: [
@@ -4037,6 +4097,24 @@ function deriveResponseMode(raw) {
4037
4097
  if (responseType && /\b(token|id_token)\b/.test(responseType)) return "fragment";
4038
4098
  return "query";
4039
4099
  }
4100
+ const authorizationQueryErrorCodesByField = {
4101
+ response_type: { invalid: "unsupported_response_type" },
4102
+ resource: { invalid: "invalid_target" }
4103
+ };
4104
+ function isRecord(value) {
4105
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4106
+ }
4107
+ function getStringParameter(value) {
4108
+ return typeof value === "string" ? value : void 0;
4109
+ }
4110
+ function getAuthorizationRequestParameters(ctx, settings) {
4111
+ const source = ctx.method === "POST" && settings?.isAuthorize === true ? ctx.body : ctx.query;
4112
+ return { ...isRecord(source) ? source : {} };
4113
+ }
4114
+ function isAcrValuesRequestSupported(acrValues) {
4115
+ if (acrValues === void 0) return true;
4116
+ return acrValues.split(" ").filter(Boolean).includes("0");
4117
+ }
4040
4118
  const handleRedirect = (ctx, uri) => {
4041
4119
  const fromFetch = isBrowserFetchRequest(ctx.request?.headers);
4042
4120
  const acceptJson = ctx.headers?.get("accept")?.includes("application/json");
@@ -4153,31 +4231,68 @@ async function authorizeEndpoint(ctx, opts, settings) {
4153
4231
  error: "invalid_request"
4154
4232
  });
4155
4233
  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"));
4234
+ let query = getAuthorizationRequestParameters(ctx, settings);
4235
+ ctx.query = query;
4236
+ const requestObject = getStringParameter(query.request);
4237
+ const requestUri = getStringParameter(query.request_uri);
4238
+ if (requestObject !== void 0 && requestUri !== void 0) return authorizeRedirectOnError(opts)({
4239
+ error: "invalid_request",
4240
+ error_description: "request and request_uri cannot be used together",
4241
+ ctx
4242
+ });
4243
+ if (requestObject !== void 0) return authorizeRedirectOnError(opts)({
4244
+ error: "request_not_supported",
4245
+ error_description: "request object not supported",
4246
+ ctx
4247
+ });
4248
+ if (requestUri !== void 0) {
4249
+ const clientId = getStringParameter(query.client_id);
4250
+ if (!clientId) return authorizeRedirectOnError(opts)({
4251
+ error: "invalid_request",
4252
+ error_description: "client_id is required",
4253
+ ctx
4254
+ });
4255
+ if (!opts.requestUriResolver) return authorizeRedirectOnError(opts)({
4256
+ error: "request_uri_not_supported",
4257
+ error_description: "request_uri not supported",
4258
+ ctx
4259
+ });
4159
4260
  const resolvedParams = await opts.requestUriResolver({
4160
- requestUri: query.request_uri,
4161
- clientId: query.client_id ?? "",
4261
+ requestUri,
4262
+ clientId,
4263
+ ctx
4264
+ });
4265
+ if (!resolvedParams) return authorizeRedirectOnError(opts)({
4266
+ error: "invalid_request_uri",
4267
+ error_description: "request_uri is invalid or expired",
4162
4268
  ctx
4163
4269
  });
4164
- if (!resolvedParams) return handleRedirect(ctx, getErrorURL(ctx, "invalid_request_uri", "request_uri is invalid or expired"));
4165
4270
  const urlClientId = query.client_id;
4166
4271
  query = resolvedParams;
4167
4272
  if (urlClientId) query.client_id = urlClientId;
4168
4273
  }
4169
4274
  ctx.query = query;
4170
4275
  const parsedQuery = authorizationQuerySchema.safeParse(query);
4171
- if (!parsedQuery.success) return authorizeRedirectOnError(opts)({
4276
+ if (!parsedQuery.success) {
4277
+ const mappedError = mapIssuesToOAuthError(parsedQuery.error.issues, authorizationQueryErrorCodesByField);
4278
+ return authorizeRedirectOnError(opts)({
4279
+ ...mappedError,
4280
+ ctx
4281
+ });
4282
+ }
4283
+ query = parsedQuery.data;
4284
+ ctx.query = query;
4285
+ if (!isAcrValuesRequestSupported(query.acr_values)) return authorizeRedirectOnError(opts)({
4172
4286
  error: "invalid_request",
4173
- error_description: "invalid authorization request",
4287
+ error_description: "unsupported acr_values",
4174
4288
  ctx
4175
4289
  });
4176
- query = parsedQuery.data;
4177
- ctx.query = query;
4178
4290
  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"));
4291
+ if (!query.response_type) return authorizeRedirectOnError(opts)({
4292
+ error: "invalid_request",
4293
+ error_description: "response_type is required",
4294
+ ctx
4295
+ });
4181
4296
  const promptSet = ctx.query?.prompt ? parsePrompt(ctx.query?.prompt) : void 0;
4182
4297
  const promptNone = promptSet?.has("none") ?? false;
4183
4298
  if (promptSet?.has("select_account") && !opts.selectAccount?.page) return handleRedirect(ctx, getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
@@ -4199,6 +4314,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
4199
4314
  requestedScopes = client.scopes ?? opts.scopes ?? [];
4200
4315
  query.scope = requestedScopes.join(" ");
4201
4316
  }
4317
+ const requestedUserInfoClaims = getRequestedUserInfoClaims(query.claims, getSupportedClaims(opts));
4202
4318
  if (query.resource !== void 0) try {
4203
4319
  await resolveResourcePolicy(ctx, opts, {
4204
4320
  resource: query.resource,
@@ -4213,7 +4329,10 @@ async function authorizeEndpoint(ctx, opts, settings) {
4213
4329
  }
4214
4330
  throw err;
4215
4331
  }
4216
- const pkceRequired = isPKCERequired(client, requestedScopes);
4332
+ const pkceRequired = isPKCERequired(client, {
4333
+ scopes: requestedScopes,
4334
+ nonce: query.nonce
4335
+ });
4217
4336
  if (pkceRequired) {
4218
4337
  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
4338
  }
@@ -4300,7 +4419,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
4300
4419
  }] : []
4301
4420
  ]
4302
4421
  });
4303
- if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val))) {
4422
+ if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val)) || !requestedUserInfoClaims.every((claim) => (consent.requestedUserInfoClaims ?? []).includes(claim))) {
4304
4423
  if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "consent_required", "End-User consent is required");
4305
4424
  return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
4306
4425
  }
@@ -4324,6 +4443,7 @@ function serializeAuthorizationQuery(query) {
4324
4443
  for (const [key, value] of Object.entries(query)) {
4325
4444
  if (value == null) continue;
4326
4445
  if (Array.isArray(value)) for (const v of value) params.append(key, String(v));
4446
+ else if (key === "claims" && typeof value === "object") params.set(key, JSON.stringify(value));
4327
4447
  else params.set(key, String(value));
4328
4448
  }
4329
4449
  return params;