@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/{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 +301 -181
- package/dist/{introspect-BXNvkz8S.mjs → introspect-DvHp2a64.mjs} +489 -101
- package/dist/{oauth-CqOygaZd.d.mts → oauth-BrNRbP2A.d.mts} +57 -43
- package/dist/{oauth-CPWY2Few.d.mts → oauth-ScTJEcFV.d.mts} +191 -21
- package/dist/{utils-Baq6atYN.mjs → utils-DO8lmoDw.mjs} +13 -8
- package/dist/{version-bmpg6tAD.mjs → version-nSGiIxZx.mjs} +1 -1
- package/package.json +7 -7
- /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-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
|
|
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,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
|
-
...
|
|
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
|
-
|
|
3095
|
+
method: ["GET", "POST"],
|
|
3096
|
+
body: z.object({}).passthrough(),
|
|
3039
3097
|
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"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
|
4157
|
-
|
|
4158
|
-
|
|
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
|
|
4161
|
-
clientId
|
|
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)
|
|
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: "
|
|
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.
|
|
4180
|
-
|
|
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,
|
|
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;
|