@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/{client-assertion-CctbJywV.mjs → client-assertion-D-tAYsKC.mjs} +1 -1
- package/dist/client-resource.d.mts +1 -1
- package/dist/client-resource.mjs +2 -2
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +2 -2
- package/dist/index.d.mts +7 -4
- package/dist/index.mjs +288 -181
- package/dist/{introspect-BXNvkz8S.mjs → introspect-Bzbar3iY.mjs} +328 -90
- package/dist/{oauth-CqOygaZd.d.mts → oauth-C7baUP-L.d.mts} +36 -42
- package/dist/{oauth-CPWY2Few.d.mts → oauth-Di-k6QeJ.d.mts} +150 -20
- package/dist/{utils-Baq6atYN.mjs → utils-DO8lmoDw.mjs} +13 -8
- package/dist/{version-bmpg6tAD.mjs → version-M87fotrf.mjs} +1 -1
- package/package.json +5 -5
- /package/dist/{signed-query-CFv2jNMT.mjs → signed-query-Df1MNiSH.mjs} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
4
|
-
import { n as consumeClientAssertion, r as isPrivateHostname } from "./client-assertion-
|
|
5
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
...
|
|
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
|
-
|
|
3082
|
+
method: ["GET", "POST"],
|
|
3083
|
+
body: z.object({}).passthrough(),
|
|
3039
3084
|
redirectOnError: authorizeRedirectOnError(opts),
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
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
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
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
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
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
|
-
|
|
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
|
|
4157
|
-
|
|
4158
|
-
|
|
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
|
|
4161
|
-
clientId
|
|
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)
|
|
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: "
|
|
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.
|
|
4180
|
-
|
|
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,
|
|
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;
|