@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
-
import { A as collectExtensionUserInfoClaims, C as toAudienceClaim, F as getSupportedGrantTypes, I as hasUserInfoClaimExtension, N as getExtensionGrantHandler, O as collectExtensionAccessTokenClaims, S as storeToken, T as validateClientCredentials, a as getClient, c as getStoredToken, f as normalizeTimestampValue, i as extractClientCredentials, k as collectExtensionIdTokenClaims, l as isPKCERequired, m as parseClientMetadata, n as decryptStoredClientSecret, o as getJwtPlugin, r as destructureCredentials, t as clientAllowsGrant, v as resolveSessionAuthTime, w as toResourceList, y as resolveSubjectIdentifier } from "./utils-
|
|
2
|
+
import { A as collectExtensionUserInfoClaims, C as toAudienceClaim, F as getSupportedGrantTypes, I as hasUserInfoClaimExtension, N as getExtensionGrantHandler, O as collectExtensionAccessTokenClaims, S as storeToken, T as validateClientCredentials, a as getClient, c as getStoredToken, f as normalizeTimestampValue, i as extractClientCredentials, k as collectExtensionIdTokenClaims, l as isPKCERequired, m as parseClientMetadata, n as decryptStoredClientSecret, o as getJwtPlugin, r as destructureCredentials, t as clientAllowsGrant, v as resolveSessionAuthTime, w as toResourceList, y as resolveSubjectIdentifier } from "./utils-DO8lmoDw.mjs";
|
|
3
3
|
import { APIError } from "better-auth/api";
|
|
4
|
-
import { generateRandomString } from "better-auth/crypto";
|
|
4
|
+
import { generateRandomString, symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
|
|
5
5
|
import { APIError as APIError$1 } from "better-call";
|
|
6
6
|
import { logger } from "@better-auth/core/env";
|
|
7
7
|
import * as z from "zod";
|
|
@@ -9,6 +9,156 @@ import { createDpopReplayStore, enforceDpopBinding, generateCodeChallenge, getCo
|
|
|
9
9
|
import { SignJWT, base64url, createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
|
|
10
10
|
import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
|
|
11
11
|
import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
|
|
12
|
+
//#region src/authentication-context.ts
|
|
13
|
+
const RESERVED_ID_TOKEN_CLAIMS = new Set([
|
|
14
|
+
"iss",
|
|
15
|
+
"sub",
|
|
16
|
+
"aud",
|
|
17
|
+
"exp",
|
|
18
|
+
"nbf",
|
|
19
|
+
"iat",
|
|
20
|
+
"jti",
|
|
21
|
+
"auth_time",
|
|
22
|
+
"nonce",
|
|
23
|
+
"acr",
|
|
24
|
+
"amr",
|
|
25
|
+
"azp",
|
|
26
|
+
"sid",
|
|
27
|
+
"at_hash",
|
|
28
|
+
"c_hash",
|
|
29
|
+
"s_hash"
|
|
30
|
+
]);
|
|
31
|
+
/**
|
|
32
|
+
* Removes provider-owned ID token claim names from custom claims.
|
|
33
|
+
*
|
|
34
|
+
* `customIdTokenClaims` is an extension point for additional claims. It must not
|
|
35
|
+
* replace the issuer, subject, audience, lifetime, nonce, authorized party,
|
|
36
|
+
* token-binding hashes, session binding, or authentication context that the
|
|
37
|
+
* provider owns.
|
|
38
|
+
*/
|
|
39
|
+
function stripReservedIdTokenClaims(claims) {
|
|
40
|
+
if (!claims) return {};
|
|
41
|
+
const stripped = [];
|
|
42
|
+
const safeClaims = Object.create(null);
|
|
43
|
+
for (const [key, value] of Object.entries(claims)) {
|
|
44
|
+
if (RESERVED_ID_TOKEN_CLAIMS.has(key)) {
|
|
45
|
+
stripped.push(key);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
safeClaims[key] = value;
|
|
49
|
+
}
|
|
50
|
+
if (stripped.length > 0) logger.warn(`oauth-provider: stripped reserved id-token claim name(s): ${stripped.join(", ")}. The AS owns these claim values.`);
|
|
51
|
+
return safeClaims;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/claims-request.ts
|
|
55
|
+
const claimRequestMemberSchema = z.union([z.null(), z.record(z.string(), z.unknown())]);
|
|
56
|
+
const claimsRequestObjectSchema = z.object({
|
|
57
|
+
userinfo: z.record(z.string(), claimRequestMemberSchema).optional(),
|
|
58
|
+
id_token: z.record(z.string(), claimRequestMemberSchema).optional()
|
|
59
|
+
}).passthrough();
|
|
60
|
+
function parseClaimsRequestValue(value) {
|
|
61
|
+
if (typeof value === "string") try {
|
|
62
|
+
return JSON.parse(value);
|
|
63
|
+
} catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function parseClaimsRequestObject(value) {
|
|
69
|
+
const parsed = parseClaimsRequestValue(value);
|
|
70
|
+
const result = claimsRequestObjectSchema.safeParse(parsed);
|
|
71
|
+
return result.success ? result.data : void 0;
|
|
72
|
+
}
|
|
73
|
+
const claimsRequestParameterSchema = z.union([z.string(), z.record(z.string(), z.unknown())]).superRefine((value, ctx) => {
|
|
74
|
+
if (!parseClaimsRequestObject(value)) ctx.addIssue({
|
|
75
|
+
code: "custom",
|
|
76
|
+
message: "claims must be a JSON object"
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
function getRequestedUserInfoClaims(value, supportedClaims) {
|
|
80
|
+
const userInfoClaims = parseClaimsRequestObject(value)?.userinfo;
|
|
81
|
+
if (!userInfoClaims) return [];
|
|
82
|
+
const names = Object.keys(userInfoClaims);
|
|
83
|
+
if (!supportedClaims) return names;
|
|
84
|
+
const allowed = new Set(supportedClaims);
|
|
85
|
+
return names.filter((name) => allowed.has(name));
|
|
86
|
+
}
|
|
87
|
+
function filterClaimsRequestUserInfoClaims(value, allowedUserInfoClaims) {
|
|
88
|
+
const claimsRequest = parseClaimsRequestObject(value);
|
|
89
|
+
if (!claimsRequest) return void 0;
|
|
90
|
+
const allowedClaimSet = new Set(allowedUserInfoClaims);
|
|
91
|
+
const userInfoClaims = Object.fromEntries(Object.entries(claimsRequest.userinfo ?? {}).filter(([claim]) => allowedClaimSet.has(claim)));
|
|
92
|
+
const filteredClaimsRequest = Object.keys(userInfoClaims).length ? {
|
|
93
|
+
...claimsRequest,
|
|
94
|
+
userinfo: userInfoClaims
|
|
95
|
+
} : Object.fromEntries(Object.entries(claimsRequest).filter(([key]) => key !== "userinfo"));
|
|
96
|
+
return Object.keys(filteredClaimsRequest).length ? filteredClaimsRequest : void 0;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/standard-claims.ts
|
|
100
|
+
function splitDisplayName(name) {
|
|
101
|
+
const parts = name.split(" ").filter((part) => part !== "");
|
|
102
|
+
if (parts.length <= 1) return {};
|
|
103
|
+
return {
|
|
104
|
+
given: parts.slice(0, -1).join(" "),
|
|
105
|
+
family: parts.at(-1)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The OIDC Standard Claims (OIDC Core §5.1) that Better Auth resolves from its
|
|
110
|
+
* own user model, each paired with the scope that requests it (§5.4).
|
|
111
|
+
*
|
|
112
|
+
* This is the single source for UserInfo scope-claim resolution, individual
|
|
113
|
+
* `claims.userinfo` resolution, the discovery `claims_supported` advertisement,
|
|
114
|
+
* and the bound on requested claim names. Adding a standard claim Better Auth
|
|
115
|
+
* can resolve means adding one entry here, not editing UserInfo, the metadata,
|
|
116
|
+
* and the plugin init separately.
|
|
117
|
+
*
|
|
118
|
+
* These claims are delivered only at the UserInfo endpoint, never the ID token:
|
|
119
|
+
* the authorization code flow always issues an access token, so §5.4 routes
|
|
120
|
+
* scope-requested claims to UserInfo. Deployments that support further standard
|
|
121
|
+
* profile claims (such as `birthdate` or `zoneinfo`) supply them through
|
|
122
|
+
* `customUserInfoClaims` and advertise them in `claims_supported`.
|
|
123
|
+
*/
|
|
124
|
+
const STANDARD_CLAIMS = {
|
|
125
|
+
name: {
|
|
126
|
+
scope: "profile",
|
|
127
|
+
resolve: (user) => user.name ?? void 0
|
|
128
|
+
},
|
|
129
|
+
picture: {
|
|
130
|
+
scope: "profile",
|
|
131
|
+
resolve: (user) => user.image ?? void 0
|
|
132
|
+
},
|
|
133
|
+
given_name: {
|
|
134
|
+
scope: "profile",
|
|
135
|
+
resolve: (user) => splitDisplayName(user.name).given
|
|
136
|
+
},
|
|
137
|
+
family_name: {
|
|
138
|
+
scope: "profile",
|
|
139
|
+
resolve: (user) => splitDisplayName(user.name).family
|
|
140
|
+
},
|
|
141
|
+
email: {
|
|
142
|
+
scope: "email",
|
|
143
|
+
resolve: (user) => user.email ?? void 0
|
|
144
|
+
},
|
|
145
|
+
email_verified: {
|
|
146
|
+
scope: "email",
|
|
147
|
+
resolve: (user) => user.emailVerified ?? false
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const STANDARD_CLAIM_NAMES = Object.keys(STANDARD_CLAIMS);
|
|
151
|
+
/**
|
|
152
|
+
* The claim names this provider advertises it can supply (discovery
|
|
153
|
+
* `claims_supported`): the operator's `advertisedMetadata.claims_supported`
|
|
154
|
+
* override when set, otherwise the scope-derived standard set computed at plugin
|
|
155
|
+
* init. Used both to advertise and to bound requested `claims.userinfo` names so
|
|
156
|
+
* a client cannot pin arbitrary attacker-controlled strings onto stored tokens.
|
|
157
|
+
*/
|
|
158
|
+
function getSupportedClaims(opts) {
|
|
159
|
+
return opts.advertisedMetadata?.claims_supported ?? opts.claims ?? [];
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
12
162
|
//#region src/claims.ts
|
|
13
163
|
/**
|
|
14
164
|
* Claim names the authorization server owns on a JWT access token, which no
|
|
@@ -851,11 +1001,12 @@ const dpopJktSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/, "dpop_jkt must be
|
|
|
851
1001
|
*/
|
|
852
1002
|
const authorizationQuerySchema = z.object({
|
|
853
1003
|
response_type: z.string().pipe(z.enum(["code"])).optional(),
|
|
1004
|
+
request: z.string().optional(),
|
|
854
1005
|
request_uri: z.string().optional(),
|
|
855
1006
|
redirect_uri: SafeUrlSchema.optional(),
|
|
856
1007
|
scope: z.string().optional(),
|
|
857
1008
|
state: z.string().optional(),
|
|
858
|
-
client_id: z.string(),
|
|
1009
|
+
client_id: z.string().min(1, "client_id is required"),
|
|
859
1010
|
prompt: authorizationPromptSchema.optional(),
|
|
860
1011
|
display: z.string().optional(),
|
|
861
1012
|
ui_locales: z.string().optional(),
|
|
@@ -866,10 +1017,11 @@ const authorizationQuerySchema = z.object({
|
|
|
866
1017
|
code_challenge: z.string().optional(),
|
|
867
1018
|
code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
|
|
868
1019
|
nonce: z.string().optional(),
|
|
1020
|
+
claims: claimsRequestParameterSchema.optional(),
|
|
869
1021
|
dpop_jkt: dpopJktSchema.optional(),
|
|
870
1022
|
resource: z.union([ResourceUriSchema, z.array(ResourceUriSchema).min(1)]).optional()
|
|
871
1023
|
}).passthrough();
|
|
872
|
-
const storedAuthorizationQuerySchema = authorizationQuerySchema.extend({ redirect_uri: SafeUrlSchema });
|
|
1024
|
+
const storedAuthorizationQuerySchema = authorizationQuerySchema.extend({ redirect_uri: SafeUrlSchema.optional() });
|
|
873
1025
|
/**
|
|
874
1026
|
* Runtime schema for the authorization code verification value.
|
|
875
1027
|
* Validates structure on deserialization from the JSON blob stored in the DB.
|
|
@@ -930,27 +1082,20 @@ const clientRegistrationRequestSchema = z.object({
|
|
|
930
1082
|
//#endregion
|
|
931
1083
|
//#region src/userinfo.ts
|
|
932
1084
|
/**
|
|
933
|
-
*
|
|
1085
|
+
* Builds the standard OIDC claims (OIDC Core §5.1) for the UserInfo response.
|
|
1086
|
+
*
|
|
1087
|
+
* A claim is included when its backing scope was granted, or when it was named
|
|
1088
|
+
* individually through the `claims.userinfo` request parameter (§5.4, §5.5).
|
|
1089
|
+
* `sub` is always present. Values come from the one claim registry, so the
|
|
1090
|
+
* advertisement, the scope mapping, and the resolution cannot drift apart.
|
|
934
1091
|
*
|
|
935
1092
|
* @see https://openid.net/specs/openid-connect-core-1_0.html#NormalClaims
|
|
936
1093
|
*/
|
|
937
|
-
function userNormalClaims(user, scopes) {
|
|
938
|
-
const
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
given_name: name.length > 1 ? name.slice(0, -1).join(" ") : void 0,
|
|
943
|
-
family_name: name.length > 1 ? name.at(-1) : void 0
|
|
944
|
-
};
|
|
945
|
-
const email = {
|
|
946
|
-
email: user.email ?? void 0,
|
|
947
|
-
email_verified: user.emailVerified ?? false
|
|
948
|
-
};
|
|
949
|
-
return {
|
|
950
|
-
sub: user.id ?? void 0,
|
|
951
|
-
...scopes.includes("profile") ? profile : {},
|
|
952
|
-
...scopes.includes("email") ? email : {}
|
|
953
|
-
};
|
|
1094
|
+
function userNormalClaims(user, scopes, requestedClaims = []) {
|
|
1095
|
+
const requested = new Set(requestedClaims);
|
|
1096
|
+
const claims = { sub: user.id ?? void 0 };
|
|
1097
|
+
for (const [name, definition] of Object.entries(STANDARD_CLAIMS)) if (scopes.includes(definition.scope) || requested.has(name)) claims[name] = definition.resolve(user);
|
|
1098
|
+
return claims;
|
|
954
1099
|
}
|
|
955
1100
|
/**
|
|
956
1101
|
* Returns the defined-valued entries of `claims`, dropping any key already
|
|
@@ -976,17 +1121,33 @@ function pickClaims(claims, base) {
|
|
|
976
1121
|
}
|
|
977
1122
|
return next;
|
|
978
1123
|
}
|
|
1124
|
+
function getUserInfoAccessToken(ctx) {
|
|
1125
|
+
const authorization = ctx.headers?.get("authorization");
|
|
1126
|
+
const headerAccessTokenAuthorization = parseAccessTokenAuthorization(authorization);
|
|
1127
|
+
const bodyToken = ctx.request?.method === "POST" ? ctx.body?.access_token ?? void 0 : void 0;
|
|
1128
|
+
if (headerAccessTokenAuthorization && bodyToken) throw new APIError("BAD_REQUEST", {
|
|
1129
|
+
error_description: "Multiple access token transport methods are not allowed",
|
|
1130
|
+
error: "invalid_request"
|
|
1131
|
+
});
|
|
1132
|
+
const accessTokenAuthorization = headerAccessTokenAuthorization ?? (bodyToken ? {
|
|
1133
|
+
scheme: "Bearer",
|
|
1134
|
+
token: bodyToken
|
|
1135
|
+
} : void 0);
|
|
1136
|
+
return {
|
|
1137
|
+
authorization: accessTokenAuthorization,
|
|
1138
|
+
token: accessTokenAuthorization?.token
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
979
1141
|
/**
|
|
980
1142
|
* Handles the /oauth2/userinfo endpoint
|
|
981
1143
|
*/
|
|
982
1144
|
async function userInfoEndpoint(ctx, opts) {
|
|
983
|
-
const authorization = ctx
|
|
984
|
-
const accessTokenAuthorization = parseAccessTokenAuthorization(authorization);
|
|
1145
|
+
const { authorization: accessTokenAuthorization } = getUserInfoAccessToken(ctx);
|
|
985
1146
|
if (!accessTokenAuthorization?.token) throw new APIError("UNAUTHORIZED", {
|
|
986
|
-
error_description: "
|
|
1147
|
+
error_description: "access token not found",
|
|
987
1148
|
error: "invalid_request"
|
|
988
1149
|
});
|
|
989
|
-
const jwt = await
|
|
1150
|
+
const { payload: jwt, requestedUserInfoClaims: requestedClaims } = await requireActiveAccessTokenWithClaims(ctx, opts, accessTokenAuthorization.token);
|
|
990
1151
|
if (getDpopJktFromPayload(jwt) && !ctx.request) throw new APIError("UNAUTHORIZED", {
|
|
991
1152
|
error_description: "DPoP-bound access token requires an HTTP request context",
|
|
992
1153
|
error: "invalid_token"
|
|
@@ -1023,7 +1184,7 @@ async function userInfoEndpoint(ctx, opts) {
|
|
|
1023
1184
|
error_description: "user not found",
|
|
1024
1185
|
error: "invalid_request"
|
|
1025
1186
|
});
|
|
1026
|
-
const baseUserClaims = userNormalClaims(user, scopes ?? []);
|
|
1187
|
+
const baseUserClaims = userNormalClaims(user, scopes ?? [], requestedClaims);
|
|
1027
1188
|
const clientId = jwt.client_id ?? jwt.azp;
|
|
1028
1189
|
const client = clientId && (opts.pairwiseSecret || hasUserInfoClaimExtension(opts)) ? await getClient(ctx, opts, clientId) : void 0;
|
|
1029
1190
|
if (opts.pairwiseSecret && client) baseUserClaims.sub = await resolveSubjectIdentifier(user.id, client, opts);
|
|
@@ -1033,12 +1194,14 @@ async function userInfoEndpoint(ctx, opts) {
|
|
|
1033
1194
|
user,
|
|
1034
1195
|
scopes,
|
|
1035
1196
|
jwt,
|
|
1036
|
-
client: client ?? void 0
|
|
1197
|
+
client: client ?? void 0,
|
|
1198
|
+
requestedClaims
|
|
1037
1199
|
}) : {};
|
|
1038
1200
|
const additionalInfoUserClaims = opts.customUserInfoClaims && scopes?.length ? await opts.customUserInfoClaims({
|
|
1039
1201
|
user,
|
|
1040
1202
|
scopes,
|
|
1041
|
-
jwt
|
|
1203
|
+
jwt,
|
|
1204
|
+
requestedClaims
|
|
1042
1205
|
}) : {};
|
|
1043
1206
|
return {
|
|
1044
1207
|
...baseUserClaims,
|
|
@@ -1049,6 +1212,7 @@ async function userInfoEndpoint(ctx, opts) {
|
|
|
1049
1212
|
}
|
|
1050
1213
|
//#endregion
|
|
1051
1214
|
//#region src/token.ts
|
|
1215
|
+
const ID_TOKEN_SCOPE_CLAIM_GUARDS = Object.fromEntries(STANDARD_CLAIM_NAMES.map((name) => [name, void 0]));
|
|
1052
1216
|
/**
|
|
1053
1217
|
* Token presentation scheme implied by a confirmation: a DPoP key thumbprint
|
|
1054
1218
|
* (`jkt`) yields `"DPoP"`; any other confirmation (including mTLS `x5t#S256`)
|
|
@@ -1183,24 +1347,22 @@ async function computeOidcHash(token, signingAlg) {
|
|
|
1183
1347
|
async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken, extraClaims) {
|
|
1184
1348
|
const iat = Math.floor(Date.now() / 1e3);
|
|
1185
1349
|
const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
|
|
1186
|
-
const userClaims = userNormalClaims(user, scopes);
|
|
1187
1350
|
const resolvedSub = await resolveSubjectIdentifier(user.id, client, opts);
|
|
1188
1351
|
const authTimeSec = authTime != null ? Math.floor(authTime.getTime() / 1e3) : void 0;
|
|
1189
|
-
const
|
|
1190
|
-
const customClaims = opts.customIdTokenClaims ? await opts.customIdTokenClaims({
|
|
1352
|
+
const customClaims = stripReservedIdTokenClaims(opts.customIdTokenClaims ? await opts.customIdTokenClaims({
|
|
1191
1353
|
user,
|
|
1192
1354
|
scopes,
|
|
1193
1355
|
metadata: parseClientMetadata(client.metadata)
|
|
1194
|
-
}) :
|
|
1356
|
+
}) : void 0);
|
|
1195
1357
|
const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
|
|
1196
1358
|
const resolvedKey = !opts.disableJwtPlugin && !jwtPluginOptions?.jwt?.sign ? await resolveSigningKey(ctx, jwtPluginOptions) : void 0;
|
|
1197
1359
|
const signingAlg = opts.disableJwtPlugin ? "HS256" : resolvedKey?.alg ?? jwtPluginOptions?.jwks?.keyPairConfig?.alg;
|
|
1198
1360
|
const atHash = accessToken ? await computeOidcHash(accessToken, signingAlg) : void 0;
|
|
1199
1361
|
const emitSid = Boolean(client.enableEndSession || client.backchannelLogoutUri);
|
|
1200
1362
|
const payload = {
|
|
1201
|
-
...
|
|
1363
|
+
...ID_TOKEN_SCOPE_CLAIM_GUARDS,
|
|
1202
1364
|
auth_time: authTimeSec,
|
|
1203
|
-
acr,
|
|
1365
|
+
acr: "0",
|
|
1204
1366
|
...customClaims,
|
|
1205
1367
|
at_hash: atHash,
|
|
1206
1368
|
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
@@ -1211,7 +1373,8 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
|
|
|
1211
1373
|
exp,
|
|
1212
1374
|
sid: emitSid ? sessionId : void 0
|
|
1213
1375
|
};
|
|
1214
|
-
|
|
1376
|
+
const additiveClaims = stripReservedIdTokenClaims(extraClaims);
|
|
1377
|
+
Object.assign(payload, pickClaims(additiveClaims, payload));
|
|
1215
1378
|
if (opts.disableJwtPlugin && !client.clientSecret) return;
|
|
1216
1379
|
const idToken = opts.disableJwtPlugin ? await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(new TextEncoder().encode(await decryptStoredClientSecret(ctx, opts.storeClientSecret, client.clientSecret))) : await signJWT(ctx, {
|
|
1217
1380
|
options: jwtPluginOptions,
|
|
@@ -1246,7 +1409,7 @@ async function decodeRefreshToken(opts, token) {
|
|
|
1246
1409
|
});
|
|
1247
1410
|
return opts.formatRefreshToken?.decrypt ? opts.formatRefreshToken?.decrypt(token) : { token };
|
|
1248
1411
|
}
|
|
1249
|
-
async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, refreshId, confirmation) {
|
|
1412
|
+
async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, authorizationCodeId, refreshId, confirmation, requestedUserInfoClaims) {
|
|
1250
1413
|
const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
|
|
1251
1414
|
const exp = payload?.exp ?? iat + (opts.accessTokenExpiresIn ?? 3600);
|
|
1252
1415
|
const token = opts.generateOpaqueAccessToken ? await opts.generateOpaqueAccessToken() : generateRandomString(32, "A-Z", "a-z");
|
|
@@ -1258,9 +1421,11 @@ async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload,
|
|
|
1258
1421
|
sessionId: payload?.sid,
|
|
1259
1422
|
userId: user?.id,
|
|
1260
1423
|
referenceId,
|
|
1424
|
+
authorizationCodeId,
|
|
1261
1425
|
resources,
|
|
1262
1426
|
refreshId,
|
|
1263
1427
|
confirmation,
|
|
1428
|
+
requestedUserInfoClaims: requestedUserInfoClaims?.length ? requestedUserInfoClaims : void 0,
|
|
1264
1429
|
scopes,
|
|
1265
1430
|
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
1266
1431
|
expiresAt: /* @__PURE__ */ new Date(exp * 1e3)
|
|
@@ -1315,7 +1480,24 @@ async function invalidateRefreshFamily(ctx, clientId, userId) {
|
|
|
1315
1480
|
}]
|
|
1316
1481
|
});
|
|
1317
1482
|
}
|
|
1318
|
-
async function
|
|
1483
|
+
async function revokeTokensIssuedForAuthorizationCode(ctx, authorizationCodeId) {
|
|
1484
|
+
const deleteIssuedTokens = async (model) => {
|
|
1485
|
+
try {
|
|
1486
|
+
await ctx.context.adapter.deleteMany({
|
|
1487
|
+
model,
|
|
1488
|
+
where: [{
|
|
1489
|
+
field: "authorizationCodeId",
|
|
1490
|
+
value: authorizationCodeId
|
|
1491
|
+
}]
|
|
1492
|
+
});
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
ctx.context.logger.error("authorization code replay cleanup failed", error);
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
await deleteIssuedTokens("oauthAccessToken");
|
|
1498
|
+
await deleteIssuedTokens("oauthRefreshToken");
|
|
1499
|
+
}
|
|
1500
|
+
async function createRefreshToken(ctx, opts, user, referenceId, authorizationCodeId, client, scopes, payload, originalRefresh, authTime, resources, confirmation, requestedUserInfoClaims) {
|
|
1319
1501
|
const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
|
|
1320
1502
|
const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
|
|
1321
1503
|
const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
|
|
@@ -1326,8 +1508,10 @@ async function createRefreshToken(ctx, opts, user, referenceId, client, scopes,
|
|
|
1326
1508
|
sessionId,
|
|
1327
1509
|
userId: user.id,
|
|
1328
1510
|
referenceId,
|
|
1511
|
+
authorizationCodeId,
|
|
1329
1512
|
authTime,
|
|
1330
1513
|
confirmation,
|
|
1514
|
+
requestedUserInfoClaims: requestedUserInfoClaims?.length ? requestedUserInfoClaims : void 0,
|
|
1331
1515
|
scopes,
|
|
1332
1516
|
resources,
|
|
1333
1517
|
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
@@ -1340,6 +1524,13 @@ async function createRefreshToken(ctx, opts, user, referenceId, client, scopes,
|
|
|
1340
1524
|
})).id,
|
|
1341
1525
|
token: await encodeRefreshToken(opts, token, sessionId)
|
|
1342
1526
|
};
|
|
1527
|
+
const rotatedAt = /* @__PURE__ */ new Date(iat * 1e3);
|
|
1528
|
+
const rotationUpdate = {
|
|
1529
|
+
revoked: rotatedAt,
|
|
1530
|
+
rotatedAt
|
|
1531
|
+
};
|
|
1532
|
+
const reuseInterval = opts.refreshTokenReuseInterval ?? 0;
|
|
1533
|
+
if (reuseInterval > 0) rotationUpdate.rotationReplayExpiresAt = /* @__PURE__ */ new Date((iat + reuseInterval) * 1e3);
|
|
1343
1534
|
if (!await ctx.context.adapter.incrementOne({
|
|
1344
1535
|
model: "oauthRefreshToken",
|
|
1345
1536
|
where: [{
|
|
@@ -1351,7 +1542,7 @@ async function createRefreshToken(ctx, opts, user, referenceId, client, scopes,
|
|
|
1351
1542
|
value: null
|
|
1352
1543
|
}],
|
|
1353
1544
|
increment: {},
|
|
1354
|
-
set:
|
|
1545
|
+
set: rotationUpdate
|
|
1355
1546
|
})) throw new APIError("BAD_REQUEST", {
|
|
1356
1547
|
error_description: "invalid refresh token",
|
|
1357
1548
|
error: "invalid_grant"
|
|
@@ -1420,6 +1611,126 @@ async function resolveDpopTokenBinding(ctx, opts, params) {
|
|
|
1420
1611
|
throw error;
|
|
1421
1612
|
}
|
|
1422
1613
|
}
|
|
1614
|
+
function normalizeReplayValues(values) {
|
|
1615
|
+
return values ? [...new Set(values)].sort() : void 0;
|
|
1616
|
+
}
|
|
1617
|
+
function sameReplayValues(left, right) {
|
|
1618
|
+
const normalizedLeft = normalizeReplayValues(left);
|
|
1619
|
+
const normalizedRight = normalizeReplayValues(right);
|
|
1620
|
+
if (normalizedLeft === void 0 || normalizedRight === void 0) return normalizedLeft === normalizedRight;
|
|
1621
|
+
if (normalizedLeft.length !== normalizedRight.length) return false;
|
|
1622
|
+
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
|
1623
|
+
}
|
|
1624
|
+
function confirmationReplayKey(confirmation) {
|
|
1625
|
+
if (!confirmation) return;
|
|
1626
|
+
if ("jkt" in confirmation) return `jkt:${confirmation.jkt}`;
|
|
1627
|
+
return `x5t#S256:${confirmation["x5t#S256"]}`;
|
|
1628
|
+
}
|
|
1629
|
+
function sameConfirmation(left, right) {
|
|
1630
|
+
return confirmationReplayKey(left) === confirmationReplayKey(right);
|
|
1631
|
+
}
|
|
1632
|
+
function isConfirmation(value) {
|
|
1633
|
+
if (!value || typeof value !== "object") return false;
|
|
1634
|
+
const confirmation = value;
|
|
1635
|
+
return typeof confirmation.jkt === "string" && confirmation["x5t#S256"] === void 0 || typeof confirmation["x5t#S256"] === "string" && confirmation.jkt === void 0;
|
|
1636
|
+
}
|
|
1637
|
+
function isTokenType(value) {
|
|
1638
|
+
return value === "Bearer" || value === "DPoP";
|
|
1639
|
+
}
|
|
1640
|
+
function isOAuthTokenResponse(value) {
|
|
1641
|
+
if (!value || typeof value !== "object") return false;
|
|
1642
|
+
const response = value;
|
|
1643
|
+
return typeof response.access_token === "string" && typeof response.expires_in === "number" && typeof response.expires_at === "number" && isTokenType(response.token_type) && typeof response.scope === "string" && (response.refresh_token === void 0 || typeof response.refresh_token === "string") && (response.id_token === void 0 || typeof response.id_token === "string");
|
|
1644
|
+
}
|
|
1645
|
+
function isRefreshTokenRotationReplay(value) {
|
|
1646
|
+
if (!value || typeof value !== "object") return false;
|
|
1647
|
+
const replay = value;
|
|
1648
|
+
const request = replay.request;
|
|
1649
|
+
return !!request && Array.isArray(request.effectiveScopes) && request.effectiveScopes.every((scope) => typeof scope === "string") && (request.requestedResources === void 0 || Array.isArray(request.requestedResources) && request.requestedResources.every((resource) => typeof resource === "string")) && (request.confirmation === void 0 || isConfirmation(request.confirmation)) && isOAuthTokenResponse(replay.response);
|
|
1650
|
+
}
|
|
1651
|
+
function buildRefreshTokenRotationReplayRequest(params) {
|
|
1652
|
+
const requestedResources = normalizeReplayValues(params.requestedResources);
|
|
1653
|
+
return {
|
|
1654
|
+
effectiveScopes: normalizeReplayValues(params.effectiveScopes) ?? [],
|
|
1655
|
+
...requestedResources ? { requestedResources } : {},
|
|
1656
|
+
...params.confirmation ? { confirmation: params.confirmation } : {}
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
function sameRefreshTokenRotationReplayRequest(left, right) {
|
|
1660
|
+
return sameReplayValues(left.effectiveScopes, right.effectiveScopes) && sameReplayValues(left.requestedResources, right.requestedResources) && sameConfirmation(left.confirmation, right.confirmation);
|
|
1661
|
+
}
|
|
1662
|
+
async function encryptRefreshTokenRotationReplay(ctx, replay) {
|
|
1663
|
+
return symmetricEncrypt({
|
|
1664
|
+
key: ctx.context.secretConfig,
|
|
1665
|
+
data: JSON.stringify(replay)
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
async function decryptRefreshTokenRotationReplay(ctx, value) {
|
|
1669
|
+
const decrypted = await symmetricDecrypt({
|
|
1670
|
+
key: ctx.context.secretConfig,
|
|
1671
|
+
data: value
|
|
1672
|
+
});
|
|
1673
|
+
const parsed = JSON.parse(decrypted);
|
|
1674
|
+
if (!isRefreshTokenRotationReplay(parsed)) return;
|
|
1675
|
+
return parsed;
|
|
1676
|
+
}
|
|
1677
|
+
function isWithinRefreshTokenReuseInterval(refreshToken) {
|
|
1678
|
+
return !!refreshToken.rotatedAt && !!refreshToken.rotationReplayExpiresAt && refreshToken.rotationReplayExpiresAt >= /* @__PURE__ */ new Date();
|
|
1679
|
+
}
|
|
1680
|
+
async function storeRefreshTokenRotationReplay(ctx, opts, refreshToken, request, response) {
|
|
1681
|
+
if ((opts.refreshTokenReuseInterval ?? 0) <= 0) return;
|
|
1682
|
+
await ctx.context.adapter.update({
|
|
1683
|
+
model: "oauthRefreshToken",
|
|
1684
|
+
where: [{
|
|
1685
|
+
field: "id",
|
|
1686
|
+
value: refreshToken.id
|
|
1687
|
+
}],
|
|
1688
|
+
update: { rotationReplayResponse: await encryptRefreshTokenRotationReplay(ctx, {
|
|
1689
|
+
request,
|
|
1690
|
+
response
|
|
1691
|
+
}) }
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
async function getRefreshTokenRotationReplay(ctx, refreshToken, request) {
|
|
1695
|
+
if (!isWithinRefreshTokenReuseInterval(refreshToken)) return;
|
|
1696
|
+
if (!refreshToken.rotationReplayResponse) return;
|
|
1697
|
+
try {
|
|
1698
|
+
const replay = await decryptRefreshTokenRotationReplay(ctx, refreshToken.rotationReplayResponse);
|
|
1699
|
+
if (!replay || !sameRefreshTokenRotationReplayRequest(replay.request, request)) return;
|
|
1700
|
+
return replay.response;
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
ctx.context.logger.error("refresh token rotation replay failed", error);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
async function resolveRefreshTokenRotationReplayRequest(ctx, opts, params) {
|
|
1707
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
1708
|
+
const grantIssuance = await resolveResourceGrantIssuance(ctx, opts, {
|
|
1709
|
+
clientId: params.client.clientId,
|
|
1710
|
+
requestedScopes: params.scopes,
|
|
1711
|
+
resources: params.resources,
|
|
1712
|
+
refreshToken: params.refreshToken,
|
|
1713
|
+
iat,
|
|
1714
|
+
scopeExpiresAtSeconds: iat + (opts.accessTokenExpiresIn ?? 3600)
|
|
1715
|
+
});
|
|
1716
|
+
const confirmation = await resolveDpopTokenBinding(ctx, opts, {
|
|
1717
|
+
client: params.client,
|
|
1718
|
+
grantIssuance,
|
|
1719
|
+
refreshToken: params.refreshToken
|
|
1720
|
+
});
|
|
1721
|
+
return buildRefreshTokenRotationReplayRequest({
|
|
1722
|
+
effectiveScopes: grantIssuance.effectiveScopes,
|
|
1723
|
+
requestedResources: params.resources,
|
|
1724
|
+
confirmation
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
function replayRefreshTokenRotationResponse(ctx, response) {
|
|
1728
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1729
|
+
return ctx.json({
|
|
1730
|
+
...response,
|
|
1731
|
+
expires_in: Math.max(0, response.expires_at - now)
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1423
1734
|
async function createUserTokens(ctx, opts, params) {
|
|
1424
1735
|
const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, verificationValue } = params;
|
|
1425
1736
|
const iat = Math.floor(Date.now() / 1e3);
|
|
@@ -1473,11 +1784,12 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
1473
1784
|
verificationValue,
|
|
1474
1785
|
refreshToken: existingRefreshToken
|
|
1475
1786
|
});
|
|
1476
|
-
const
|
|
1787
|
+
const requestedUserInfoClaims = params.requestedUserInfoClaims ?? existingRefreshToken?.requestedUserInfoClaims ?? [];
|
|
1788
|
+
const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, params.authorizationCodeId, client, effectiveScopes, {
|
|
1477
1789
|
iat,
|
|
1478
1790
|
exp: refreshTokenExp,
|
|
1479
1791
|
sid: sessionId
|
|
1480
|
-
}, existingRefreshToken, authTime, refreshResources, confirmation) : void 0;
|
|
1792
|
+
}, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0;
|
|
1481
1793
|
const accessTokenClaims = isJwtAccessToken ? await resolveAccessTokenClaims({
|
|
1482
1794
|
ctx,
|
|
1483
1795
|
opts,
|
|
@@ -1504,13 +1816,13 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
1504
1816
|
iat,
|
|
1505
1817
|
exp,
|
|
1506
1818
|
sid: sessionId
|
|
1507
|
-
}, params?.resources, referenceId, earlyRefreshToken?.id, confirmation), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, effectiveScopes, {
|
|
1819
|
+
}, params?.resources, referenceId, params.authorizationCodeId, earlyRefreshToken?.id, confirmation, requestedUserInfoClaims), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, params.authorizationCodeId, client, effectiveScopes, {
|
|
1508
1820
|
iat,
|
|
1509
1821
|
exp: refreshTokenExp,
|
|
1510
1822
|
sid: sessionId
|
|
1511
|
-
}, existingRefreshToken, authTime, refreshResources, confirmation) : void 0]);
|
|
1823
|
+
}, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0]);
|
|
1512
1824
|
const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, effectiveScopes, nonce, sessionId, authTime, accessToken, additionalIdTokenClaims) : void 0;
|
|
1513
|
-
|
|
1825
|
+
const responseBody = {
|
|
1514
1826
|
...customFields,
|
|
1515
1827
|
...params.tokenResponse ?? {},
|
|
1516
1828
|
access_token: accessToken,
|
|
@@ -1520,37 +1832,61 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
1520
1832
|
refresh_token: refreshToken?.token,
|
|
1521
1833
|
scope: effectiveScopes.join(" "),
|
|
1522
1834
|
id_token: idToken
|
|
1523
|
-
}
|
|
1835
|
+
};
|
|
1836
|
+
if (existingRefreshToken?.id && refreshToken?.id) try {
|
|
1837
|
+
await storeRefreshTokenRotationReplay(ctx, opts, existingRefreshToken, buildRefreshTokenRotationReplayRequest({
|
|
1838
|
+
effectiveScopes,
|
|
1839
|
+
requestedResources: params.resources,
|
|
1840
|
+
confirmation
|
|
1841
|
+
}), responseBody);
|
|
1842
|
+
} catch (error) {
|
|
1843
|
+
ctx.context.logger.error("failed to store refresh token rotation replay", error);
|
|
1844
|
+
}
|
|
1845
|
+
return ctx.json(responseBody);
|
|
1524
1846
|
}
|
|
1525
1847
|
/** Checks verification value */
|
|
1526
1848
|
async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resource) {
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1849
|
+
const authorizationCodeId = await storeToken(opts.storeTokens, code, "authorization_code");
|
|
1850
|
+
const verification = await ctx.context.internalAdapter.consumeVerificationValue(authorizationCodeId);
|
|
1851
|
+
if (!verification) {
|
|
1852
|
+
await revokeTokensIssuedForAuthorizationCode(ctx, authorizationCodeId);
|
|
1853
|
+
throw new APIError("BAD_REQUEST", {
|
|
1854
|
+
error_description: "invalid code",
|
|
1855
|
+
error: "invalid_grant"
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1532
1858
|
let rawValue;
|
|
1533
1859
|
try {
|
|
1534
1860
|
rawValue = JSON.parse(verification.value);
|
|
1535
1861
|
} catch {
|
|
1536
|
-
throw new APIError("
|
|
1862
|
+
throw new APIError("BAD_REQUEST", {
|
|
1537
1863
|
error_description: "malformed verification value",
|
|
1538
1864
|
error: "invalid_grant"
|
|
1539
1865
|
});
|
|
1540
1866
|
}
|
|
1541
1867
|
const parsed = verificationValueSchema.safeParse(rawValue);
|
|
1542
|
-
if (!parsed.success) throw new APIError("
|
|
1868
|
+
if (!parsed.success) throw new APIError("BAD_REQUEST", {
|
|
1543
1869
|
error_description: "malformed verification value",
|
|
1544
1870
|
error: "invalid_grant"
|
|
1545
1871
|
});
|
|
1546
1872
|
const verificationValue = parsed.data;
|
|
1547
|
-
if (verificationValue.query.client_id !== client_id) throw new APIError("
|
|
1873
|
+
if (verificationValue.query.client_id !== client_id) throw new APIError("BAD_REQUEST", {
|
|
1548
1874
|
error_description: "invalid client_id",
|
|
1549
|
-
error: "
|
|
1875
|
+
error: "invalid_grant"
|
|
1550
1876
|
});
|
|
1551
|
-
|
|
1877
|
+
const boundRedirectUri = verificationValue.query.redirect_uri;
|
|
1878
|
+
if (boundRedirectUri) {
|
|
1879
|
+
if (!redirect_uri) throw new APIError("BAD_REQUEST", {
|
|
1880
|
+
error_description: "redirect_uri is required",
|
|
1881
|
+
error: "invalid_request"
|
|
1882
|
+
});
|
|
1883
|
+
if (boundRedirectUri !== redirect_uri) throw new APIError("BAD_REQUEST", {
|
|
1884
|
+
error_description: "redirect_uri mismatch",
|
|
1885
|
+
error: "invalid_grant"
|
|
1886
|
+
});
|
|
1887
|
+
} else if (redirect_uri) throw new APIError("BAD_REQUEST", {
|
|
1552
1888
|
error_description: "redirect_uri mismatch",
|
|
1553
|
-
error: "
|
|
1889
|
+
error: "invalid_grant"
|
|
1554
1890
|
});
|
|
1555
1891
|
const storedResources = toResourceList(verificationValue.resource) ?? toResourceList(verificationValue.query.resource);
|
|
1556
1892
|
const effectiveResources = resource ?? storedResources;
|
|
@@ -1565,7 +1901,8 @@ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri,
|
|
|
1565
1901
|
return {
|
|
1566
1902
|
verificationValue,
|
|
1567
1903
|
effectiveResources,
|
|
1568
|
-
authorizedResources: storedResources
|
|
1904
|
+
authorizedResources: storedResources,
|
|
1905
|
+
authorizationCodeId
|
|
1569
1906
|
};
|
|
1570
1907
|
}
|
|
1571
1908
|
/**
|
|
@@ -1583,10 +1920,6 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
1583
1920
|
error_description: "code is required",
|
|
1584
1921
|
error: "invalid_request"
|
|
1585
1922
|
});
|
|
1586
|
-
if (!redirect_uri) throw new APIError("BAD_REQUEST", {
|
|
1587
|
-
error_description: "redirect_uri is required",
|
|
1588
|
-
error: "invalid_request"
|
|
1589
|
-
});
|
|
1590
1923
|
const isAuthCodeWithSecret = client_id && client_secret;
|
|
1591
1924
|
const isAuthCodeWithPkce = client_id && code && code_verifier;
|
|
1592
1925
|
if (!isAuthCodeWithSecret && !isAuthCodeWithPkce && !preVerified) throw new APIError("BAD_REQUEST", {
|
|
@@ -1594,7 +1927,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
1594
1927
|
error: "invalid_request"
|
|
1595
1928
|
});
|
|
1596
1929
|
/** Get and check Verification Value */
|
|
1597
|
-
const { verificationValue, effectiveResources, authorizedResources } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
|
|
1930
|
+
const { verificationValue, effectiveResources, authorizedResources, authorizationCodeId } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
|
|
1598
1931
|
const scopes = verificationValue.query.scope?.split(" ");
|
|
1599
1932
|
if (!scopes) throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
1600
1933
|
error_description: "verification scope unset",
|
|
@@ -1602,7 +1935,10 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
1602
1935
|
});
|
|
1603
1936
|
/** Verify Client */
|
|
1604
1937
|
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerified, "authorization_code", authMethod);
|
|
1605
|
-
if (isPKCERequired(client,
|
|
1938
|
+
if (isPKCERequired(client, {
|
|
1939
|
+
scopes: (verificationValue.query?.scope)?.split(" ") || [],
|
|
1940
|
+
nonce: verificationValue.query?.nonce
|
|
1941
|
+
})) {
|
|
1606
1942
|
if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
|
|
1607
1943
|
error_description: "PKCE is required for this client",
|
|
1608
1944
|
error: "invalid_request"
|
|
@@ -1650,6 +1986,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
1650
1986
|
error: "invalid_request"
|
|
1651
1987
|
});
|
|
1652
1988
|
const authTime = verificationValue.authTime != null ? normalizeTimestampValue(verificationValue.authTime) : resolveSessionAuthTime(session);
|
|
1989
|
+
const requestedUserInfoClaims = getRequestedUserInfoClaims(verificationValue.query.claims, getSupportedClaims(opts));
|
|
1653
1990
|
return createUserTokens(ctx, opts, {
|
|
1654
1991
|
client,
|
|
1655
1992
|
scopes: verificationValue.query.scope?.split(" ") ?? [],
|
|
@@ -1660,6 +1997,8 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
1660
1997
|
nonce: verificationValue.query?.nonce,
|
|
1661
1998
|
authTime,
|
|
1662
1999
|
verificationValue,
|
|
2000
|
+
authorizationCodeId,
|
|
2001
|
+
requestedUserInfoClaims,
|
|
1663
2002
|
resources: effectiveResources,
|
|
1664
2003
|
originalResources: authorizedResources
|
|
1665
2004
|
});
|
|
@@ -1739,20 +2078,13 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
1739
2078
|
error: "invalid_grant"
|
|
1740
2079
|
});
|
|
1741
2080
|
if (refreshToken.clientId !== client_id) throw new APIError("BAD_REQUEST", {
|
|
1742
|
-
error_description: "invalid
|
|
1743
|
-
error: "
|
|
2081
|
+
error_description: "invalid refresh token",
|
|
2082
|
+
error: "invalid_grant"
|
|
1744
2083
|
});
|
|
1745
2084
|
if (refreshToken.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
|
|
1746
2085
|
error_description: "invalid refresh token",
|
|
1747
2086
|
error: "invalid_grant"
|
|
1748
2087
|
});
|
|
1749
|
-
if (refreshToken.revoked) {
|
|
1750
|
-
await invalidateRefreshFamily(ctx, client_id, refreshToken.userId);
|
|
1751
|
-
throw new APIError("BAD_REQUEST", {
|
|
1752
|
-
error_description: "invalid refresh token",
|
|
1753
|
-
error: "invalid_grant"
|
|
1754
|
-
});
|
|
1755
|
-
}
|
|
1756
2088
|
if (resources && refreshToken.resources && !resources.every((v) => refreshToken.resources?.includes(v))) throw new APIError("BAD_REQUEST", {
|
|
1757
2089
|
error_description: "requested resource invalid",
|
|
1758
2090
|
error: "invalid_target"
|
|
@@ -1767,6 +2099,26 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
1767
2099
|
});
|
|
1768
2100
|
}
|
|
1769
2101
|
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes, preVerified, "refresh_token", authMethod);
|
|
2102
|
+
if (refreshToken.revoked) {
|
|
2103
|
+
if (isWithinRefreshTokenReuseInterval(refreshToken)) {
|
|
2104
|
+
const replay = await getRefreshTokenRotationReplay(ctx, refreshToken, await resolveRefreshTokenRotationReplayRequest(ctx, opts, {
|
|
2105
|
+
client,
|
|
2106
|
+
refreshToken,
|
|
2107
|
+
scopes: requestedScopes ?? scopes,
|
|
2108
|
+
resources: resources ?? refreshToken.resources
|
|
2109
|
+
}));
|
|
2110
|
+
if (replay) return replayRefreshTokenRotationResponse(ctx, replay);
|
|
2111
|
+
throw new APIError("BAD_REQUEST", {
|
|
2112
|
+
error_description: "invalid refresh token",
|
|
2113
|
+
error: "invalid_grant"
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
await invalidateRefreshFamily(ctx, client_id, refreshToken.userId);
|
|
2117
|
+
throw new APIError("BAD_REQUEST", {
|
|
2118
|
+
error_description: "invalid refresh token",
|
|
2119
|
+
error: "invalid_grant"
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
1770
2122
|
const user = await ctx.context.internalAdapter.findUserById(refreshToken.userId);
|
|
1771
2123
|
if (!user) throw new APIError("BAD_REQUEST", {
|
|
1772
2124
|
error_description: "user not found",
|
|
@@ -1779,10 +2131,12 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
1779
2131
|
user,
|
|
1780
2132
|
grantType: "refresh_token",
|
|
1781
2133
|
referenceId: refreshToken.referenceId,
|
|
2134
|
+
authorizationCodeId: refreshToken.authorizationCodeId,
|
|
1782
2135
|
sessionId: refreshToken.sessionId,
|
|
1783
2136
|
refreshToken,
|
|
1784
2137
|
resources: resources ?? refreshToken.resources,
|
|
1785
|
-
authTime
|
|
2138
|
+
authTime,
|
|
2139
|
+
requestedUserInfoClaims: refreshToken.requestedUserInfoClaims
|
|
1786
2140
|
});
|
|
1787
2141
|
}
|
|
1788
2142
|
//#endregion
|
|
@@ -1790,10 +2144,17 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
1790
2144
|
var introspect_exports = /* @__PURE__ */ __exportAll({
|
|
1791
2145
|
introspectEndpoint: () => introspectEndpoint,
|
|
1792
2146
|
requireActiveAccessToken: () => requireActiveAccessToken,
|
|
2147
|
+
requireActiveAccessTokenWithClaims: () => requireActiveAccessTokenWithClaims,
|
|
1793
2148
|
validateAccessToken: () => validateAccessToken
|
|
1794
2149
|
});
|
|
1795
2150
|
const INVALID_ACCESS_TOKEN_ERROR_DESCRIPTION = "Invalid access token";
|
|
1796
2151
|
const INVALID_ACCESS_TOKEN_WWW_AUTHENTICATE = `Bearer error="invalid_token", error_description="${INVALID_ACCESS_TOKEN_ERROR_DESCRIPTION}"`;
|
|
2152
|
+
function inactiveAccessToken() {
|
|
2153
|
+
return {
|
|
2154
|
+
payload: { active: false },
|
|
2155
|
+
requestedUserInfoClaims: []
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
1797
2158
|
/**
|
|
1798
2159
|
* IMPORTANT NOTES:
|
|
1799
2160
|
* Introspection follows RFC7662
|
|
@@ -1903,18 +2264,18 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
1903
2264
|
error_description: "opaque access token not found",
|
|
1904
2265
|
error: "invalid_token"
|
|
1905
2266
|
});
|
|
1906
|
-
if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return
|
|
1907
|
-
if (accessToken.revoked) return
|
|
2267
|
+
if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return inactiveAccessToken();
|
|
2268
|
+
if (accessToken.revoked) return inactiveAccessToken();
|
|
1908
2269
|
const resources = Array.isArray(accessToken.resources) ? accessToken.resources : void 0;
|
|
1909
2270
|
let client;
|
|
1910
2271
|
if (accessToken.clientId) {
|
|
1911
2272
|
client = await getClient(ctx, opts, accessToken.clientId);
|
|
1912
|
-
if (!client || client?.disabled) return
|
|
2273
|
+
if (!client || client?.disabled) return inactiveAccessToken();
|
|
1913
2274
|
if (!await isIntrospectionAuthorized(ctx, opts, {
|
|
1914
2275
|
introspectingClientId: clientId,
|
|
1915
2276
|
issuerClientId: accessToken.clientId,
|
|
1916
2277
|
audienceResources: resources ?? []
|
|
1917
|
-
})) return
|
|
2278
|
+
})) return inactiveAccessToken();
|
|
1918
2279
|
}
|
|
1919
2280
|
const sessionId = accessToken.sessionId ?? void 0;
|
|
1920
2281
|
if (sessionId) {
|
|
@@ -1925,12 +2286,12 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
1925
2286
|
value: sessionId
|
|
1926
2287
|
}]
|
|
1927
2288
|
});
|
|
1928
|
-
if (!session || session.expiresAt < /* @__PURE__ */ new Date()) return
|
|
2289
|
+
if (!session || session.expiresAt < /* @__PURE__ */ new Date()) return inactiveAccessToken();
|
|
1929
2290
|
}
|
|
1930
2291
|
let user;
|
|
1931
2292
|
if (accessToken.userId) user = await ctx.context.internalAdapter.findUserById(accessToken?.userId);
|
|
1932
2293
|
const userInfoEndpoint = `${ctx.context.baseURL}/oauth2/userinfo`;
|
|
1933
|
-
if (resources?.length && !await isAudienceClaimAllowed(ctx, opts, resources, [userInfoEndpoint])) return
|
|
2294
|
+
if (resources?.length && !await isAudienceClaimAllowed(ctx, opts, resources, [userInfoEndpoint])) return inactiveAccessToken();
|
|
1934
2295
|
const audienceClaim = resources ? [...resources] : void 0;
|
|
1935
2296
|
if (audienceClaim?.length && accessToken.scopes?.includes("openid")) {
|
|
1936
2297
|
if (!audienceClaim.includes(userInfoEndpoint)) audienceClaim.push(userInfoEndpoint);
|
|
@@ -1952,19 +2313,22 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
1952
2313
|
}) : {};
|
|
1953
2314
|
const jwtPluginOptions = (opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options;
|
|
1954
2315
|
return {
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2316
|
+
payload: {
|
|
2317
|
+
...accessTokenClaims,
|
|
2318
|
+
active: true,
|
|
2319
|
+
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
2320
|
+
aud: toAudienceClaim(audienceClaim),
|
|
2321
|
+
client_id: accessToken.clientId,
|
|
2322
|
+
azp: accessToken.clientId,
|
|
2323
|
+
sub: user?.id,
|
|
2324
|
+
sid: sessionId,
|
|
2325
|
+
exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
|
|
2326
|
+
iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
|
|
2327
|
+
scope: accessToken.scopes?.join(" "),
|
|
2328
|
+
token_type: confirmationTokenType(accessToken.confirmation),
|
|
2329
|
+
...accessToken.confirmation ? { cnf: accessToken.confirmation } : {}
|
|
2330
|
+
},
|
|
2331
|
+
requestedUserInfoClaims: accessToken.requestedUserInfoClaims ?? []
|
|
1968
2332
|
};
|
|
1969
2333
|
}
|
|
1970
2334
|
/**
|
|
@@ -2023,16 +2387,17 @@ function isInactiveTokenError(error) {
|
|
|
2023
2387
|
return error.status === "BAD_REQUEST" || error.body?.error === "invalid_token";
|
|
2024
2388
|
}
|
|
2025
2389
|
/**
|
|
2026
|
-
* We don't know the access token format so we try to validate it
|
|
2027
|
-
*
|
|
2028
|
-
*
|
|
2029
|
-
*
|
|
2030
|
-
*
|
|
2031
|
-
* @internal
|
|
2390
|
+
* We don't know the access token format so we try to validate it as a JWT
|
|
2391
|
+
* first, then as an opaque token. Returns the RFC 7662 introspection payload
|
|
2392
|
+
* alongside the per-issuance UserInfo claims the opaque row persisted (empty for
|
|
2393
|
+
* a JWT access token, which resolves UserInfo claims by scope).
|
|
2032
2394
|
*/
|
|
2033
|
-
async function
|
|
2395
|
+
async function resolveAccessTokenValidation(ctx, opts, token, clientId) {
|
|
2034
2396
|
try {
|
|
2035
|
-
return
|
|
2397
|
+
return {
|
|
2398
|
+
payload: await validateJwtAccessToken(ctx, opts, token, clientId),
|
|
2399
|
+
requestedUserInfoClaims: []
|
|
2400
|
+
};
|
|
2036
2401
|
} catch (err) {
|
|
2037
2402
|
if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
|
|
2038
2403
|
else throw new Error(err);
|
|
@@ -2045,12 +2410,35 @@ async function validateAccessToken(ctx, opts, token, clientId) {
|
|
|
2045
2410
|
}
|
|
2046
2411
|
throw createInvalidAccessTokenError();
|
|
2047
2412
|
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Validates an access token (JWT or opaque) and returns its RFC 7662
|
|
2415
|
+
* introspection-format payload.
|
|
2416
|
+
*
|
|
2417
|
+
* @internal
|
|
2418
|
+
*/
|
|
2419
|
+
async function validateAccessToken(ctx, opts, token, clientId) {
|
|
2420
|
+
return (await resolveAccessTokenValidation(ctx, opts, token, clientId)).payload;
|
|
2421
|
+
}
|
|
2048
2422
|
async function requireActiveAccessToken(ctx, opts, token, clientId) {
|
|
2049
2423
|
const payload = await validateAccessToken(ctx, opts, token, clientId);
|
|
2050
2424
|
if (payload.active) return payload;
|
|
2051
2425
|
throw createInvalidAccessTokenError();
|
|
2052
2426
|
}
|
|
2053
2427
|
/**
|
|
2428
|
+
* UserInfo entry point: validates the access token, requires it active, and
|
|
2429
|
+
* returns the introspection payload together with the OIDC `claims.userinfo`
|
|
2430
|
+
* names persisted for the token. Opaque tokens carry the names on their row;
|
|
2431
|
+
* JWT access tokens return an empty list and resolve UserInfo claims by scope.
|
|
2432
|
+
*/
|
|
2433
|
+
async function requireActiveAccessTokenWithClaims(ctx, opts, token, clientId) {
|
|
2434
|
+
const { payload, requestedUserInfoClaims } = await resolveAccessTokenValidation(ctx, opts, token, clientId);
|
|
2435
|
+
if (payload.active) return {
|
|
2436
|
+
payload,
|
|
2437
|
+
requestedUserInfoClaims
|
|
2438
|
+
};
|
|
2439
|
+
throw createInvalidAccessTokenError();
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2054
2442
|
* Resolves pairwise sub on an introspection payload.
|
|
2055
2443
|
* Applied at the presentation layer so internal validation functions
|
|
2056
2444
|
* keep real user.id (needed for user lookup in /userinfo).
|
|
@@ -2116,4 +2504,4 @@ async function introspectEndpoint(ctx, opts) {
|
|
|
2116
2504
|
}
|
|
2117
2505
|
}
|
|
2118
2506
|
//#endregion
|
|
2119
|
-
export { invalidateResourceCache as _, invalidateRefreshFamily as a, resolveResourcePolicy as b, ResourceUriSchema as c, clientRegistrationRequestSchema as d, JWS_ALGORITHMS as f, getResource as g, extractRepeatedResourceFromForm as h, getOAuthProviderApi as i, SafeUrlSchema as l, buildClientResourceLinkId as m, introspect_exports as n, tokenEndpoint as o, assertIdentifierValid as p, decodeRefreshToken as r, userInfoEndpoint as s, introspectEndpoint as t, authorizationQuerySchema as u, isAudienceClaimAllowed as v, seedResources as x, logEnforcePerClientResourcesResolution as y };
|
|
2507
|
+
export { STANDARD_CLAIM_NAMES as C, getRequestedUserInfoClaims as D, filterClaimsRequestUserInfoClaims as E, STANDARD_CLAIMS as S, claimsRequestParameterSchema as T, invalidateResourceCache as _, invalidateRefreshFamily as a, resolveResourcePolicy as b, ResourceUriSchema as c, clientRegistrationRequestSchema as d, JWS_ALGORITHMS as f, getResource as g, extractRepeatedResourceFromForm as h, getOAuthProviderApi as i, SafeUrlSchema as l, buildClientResourceLinkId as m, introspect_exports as n, tokenEndpoint as o, assertIdentifierValid as p, decodeRefreshToken as r, userInfoEndpoint as s, introspectEndpoint as t, authorizationQuerySchema as u, isAudienceClaimAllowed as v, getSupportedClaims as w, seedResources as x, logEnforcePerClientResourcesResolution as y };
|