@better-auth/oauth-provider 1.7.9 → 1.8.0-beta.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/{authorize-D8r6mvZx.mjs → authorize-92EtLsBi.mjs} +735 -502
- 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 +1 -1
- package/dist/index-sGnh4IfY.d.mts +275 -0
- package/dist/index.d.mts +4 -203
- package/dist/index.mjs +198 -41
- package/dist/internal.d.mts +108 -40
- package/dist/internal.mjs +4 -2
- package/dist/{introspect-7WCMPe5Z.mjs → introspect-CEo9Q06v.mjs} +318 -237
- package/dist/{oauth-DAMtT2vg.d.mts → oauth-BlEuA8oD.d.mts} +380 -20
- package/dist/{oauth-BWgg8X-A.d.mts → oauth-mdkaIwmm.d.mts} +191 -86
- package/dist/{utils-DKU5wblX.mjs → utils-BMNe8Sg2.mjs} +343 -5
- package/dist/{version-DVUrz-nv.mjs → version-DeABnmP9.mjs} +1 -1
- package/package.json +6 -6
|
@@ -1,56 +1,14 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { B as collectExtensionIdTokenClaims, D as resolveSessionAuthTime, F as validateClientCredentials, G as getExtensionGrantHandler, I as validateClientScopes, J as getSupportedGrantTypes, M as toAudienceClaim, N as toResourceList, O as resolveSubjectIdentifier, V as collectExtensionUserInfoClaims, X as hasUserInfoClaimExtension, a as decryptStoredClientSecret, c as extractClientCredentials, f as getJwtPlugin, h as isPKCERequired, j as storeToken, m as getStoredToken, r as clientAllowsGrant, s as destructureCredentials, u as getClient, x as parseClientMetadata, y as normalizeTimestampValue, z as collectExtensionAccessTokenClaims } from "./utils-BMNe8Sg2.mjs";
|
|
3
3
|
import { APIError } from "better-auth/api";
|
|
4
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";
|
|
8
8
|
import { createDpopReplayStore, enforceDpopBinding, generateCodeChallenge, getConfirmationJkt, getDpopJktFromPayload, getJwks, isDpopBindingError, isDpopProofError, parseAccessTokenAuthorization, stripAccessTokenAuthorizationScheme, verifyDpopProof } from "better-auth/oauth2";
|
|
9
|
-
import { SignJWT, base64url, createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
|
|
10
9
|
import { SafeUrlSchema } from "@better-auth/core/utils/redirect-uri";
|
|
11
10
|
import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
|
|
12
|
-
|
|
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
|
|
11
|
+
import { SignJWT, base64url, createLocalJWKSet, decodeProtectedHeader, errors, jwtVerify } from "jose";
|
|
54
12
|
//#region src/claims-request.ts
|
|
55
13
|
const claimRequestMemberSchema = z.record(z.string(), z.unknown()).nullable();
|
|
56
14
|
const acrClaimRequestMemberSchema = z.looseObject({
|
|
@@ -99,6 +57,18 @@ function canSatisfyEssentialAcrRequest(value, currentAcr) {
|
|
|
99
57
|
const valuesMatch = acrRequest.values === void 0 || acrRequest.values.includes(currentAcr);
|
|
100
58
|
return valueMatches && valuesMatch;
|
|
101
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Lists the ACR values requested through `claims.id_token.acr` and the
|
|
62
|
+
* space-separated `acr_values` parameter, without duplicates.
|
|
63
|
+
*/
|
|
64
|
+
function getRequestedAcrValues(claims, acrValues) {
|
|
65
|
+
const acrRequest = parseOidcClaimsRequestObject(claims)?.id_token?.acr;
|
|
66
|
+
return Array.from(new Set([
|
|
67
|
+
...acrRequest?.value !== void 0 ? [acrRequest.value] : [],
|
|
68
|
+
...acrRequest?.values ?? [],
|
|
69
|
+
...acrValues?.split(" ").filter(Boolean) ?? []
|
|
70
|
+
]));
|
|
71
|
+
}
|
|
102
72
|
function filterClaimsRequestUserInfoClaims(value, allowedUserInfoClaims) {
|
|
103
73
|
const claimsRequest = parseOidcClaimsRequestObject(value);
|
|
104
74
|
if (!claimsRequest) return void 0;
|
|
@@ -110,6 +80,81 @@ function filterClaimsRequestUserInfoClaims(value, allowedUserInfoClaims) {
|
|
|
110
80
|
} : Object.fromEntries(Object.entries(claimsRequest).filter(([key]) => key !== "userinfo"));
|
|
111
81
|
return Object.keys(filteredClaimsRequest).length ? filteredClaimsRequest : void 0;
|
|
112
82
|
}
|
|
83
|
+
/** The ACR values the provider may assert, advertised in discovery. */
|
|
84
|
+
function getAcrValuesSupported(opts) {
|
|
85
|
+
return opts.authenticationContext?.acrValuesSupported ?? ["0"];
|
|
86
|
+
}
|
|
87
|
+
function invalidAuthenticationContext(description) {
|
|
88
|
+
throw new APIError$1("INTERNAL_SERVER_ERROR", {
|
|
89
|
+
error: "server_error",
|
|
90
|
+
error_description: `authenticationContext.resolve ${description}`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Runs the configured `authenticationContext.resolve` for an approval and
|
|
95
|
+
* validates its result. Returns an empty context, which ID tokens report as
|
|
96
|
+
* `acr: "0"`, when no resolver is configured or no ID token can be issued.
|
|
97
|
+
* Invalid results throw an `APIError` with `server_error`.
|
|
98
|
+
*/
|
|
99
|
+
async function resolveAuthenticationContext(opts, info) {
|
|
100
|
+
const resolve = opts.authenticationContext?.resolve;
|
|
101
|
+
if (!resolve || !info.scopes.includes("openid")) return {};
|
|
102
|
+
const { query, ...resolveInfo } = info;
|
|
103
|
+
const resolved = await resolve({
|
|
104
|
+
...resolveInfo,
|
|
105
|
+
requestedAcrValues: getRequestedAcrValues(query.claims, query.acr_values)
|
|
106
|
+
});
|
|
107
|
+
if (resolved !== void 0 && (typeof resolved !== "object" || resolved === null)) invalidAuthenticationContext("returned a value that is not an object");
|
|
108
|
+
const { acr = "0", amr } = resolved ?? {};
|
|
109
|
+
if (typeof acr !== "string") invalidAuthenticationContext("returned a non-string acr");
|
|
110
|
+
if (!getAcrValuesSupported(opts).includes(acr)) invalidAuthenticationContext(`returned acr "${acr}", which is not in acrValuesSupported`);
|
|
111
|
+
if (amr === void 0) return { acr };
|
|
112
|
+
if (!Array.isArray(amr) || !Array.from(amr).every((value) => typeof value === "string")) invalidAuthenticationContext("returned an amr that is not a string array");
|
|
113
|
+
return {
|
|
114
|
+
acr,
|
|
115
|
+
amr: amr.length ? amr : void 0
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const RESERVED_ID_TOKEN_CLAIMS = new Set([
|
|
119
|
+
"iss",
|
|
120
|
+
"sub",
|
|
121
|
+
"aud",
|
|
122
|
+
"exp",
|
|
123
|
+
"nbf",
|
|
124
|
+
"iat",
|
|
125
|
+
"jti",
|
|
126
|
+
"auth_time",
|
|
127
|
+
"nonce",
|
|
128
|
+
"acr",
|
|
129
|
+
"amr",
|
|
130
|
+
"azp",
|
|
131
|
+
"sid",
|
|
132
|
+
"at_hash",
|
|
133
|
+
"c_hash",
|
|
134
|
+
"s_hash"
|
|
135
|
+
]);
|
|
136
|
+
/**
|
|
137
|
+
* Removes provider-owned ID token claim names from custom claims.
|
|
138
|
+
*
|
|
139
|
+
* `customIdTokenClaims` is an extension point for additional claims. It must not
|
|
140
|
+
* replace the issuer, subject, audience, lifetime, nonce, authorized party,
|
|
141
|
+
* token-binding hashes, session binding, or authentication context that the
|
|
142
|
+
* provider owns.
|
|
143
|
+
*/
|
|
144
|
+
function stripReservedIdTokenClaims(claims) {
|
|
145
|
+
if (!claims) return {};
|
|
146
|
+
const stripped = [];
|
|
147
|
+
const safeClaims = Object.create(null);
|
|
148
|
+
for (const [key, value] of Object.entries(claims)) {
|
|
149
|
+
if (RESERVED_ID_TOKEN_CLAIMS.has(key)) {
|
|
150
|
+
stripped.push(key);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
safeClaims[key] = value;
|
|
154
|
+
}
|
|
155
|
+
if (stripped.length > 0) logger.warn(`oauth-provider: stripped reserved id-token claim name(s): ${stripped.join(", ")}. The AS owns these claim values.`);
|
|
156
|
+
return safeClaims;
|
|
157
|
+
}
|
|
113
158
|
//#endregion
|
|
114
159
|
//#region src/standard-claims.ts
|
|
115
160
|
function splitDisplayName(name) {
|
|
@@ -198,7 +243,8 @@ const RESERVED_ACCESS_TOKEN_CLAIMS = new Set([
|
|
|
198
243
|
"auth_time",
|
|
199
244
|
"acr",
|
|
200
245
|
"amr",
|
|
201
|
-
"cnf"
|
|
246
|
+
"cnf",
|
|
247
|
+
"reference_id"
|
|
202
248
|
]);
|
|
203
249
|
/**
|
|
204
250
|
* Returns a copy of `claims` with reserved AS-owned names removed. Emits a
|
|
@@ -388,6 +434,8 @@ const verificationValueSchema = z.object({
|
|
|
388
434
|
userId: z.string(),
|
|
389
435
|
referenceId: z.string().optional(),
|
|
390
436
|
authTime: z.number().optional(),
|
|
437
|
+
acr: z.string().optional(),
|
|
438
|
+
amr: z.array(z.string()).optional(),
|
|
391
439
|
resource: z.array(z.string()).optional()
|
|
392
440
|
}).passthrough();
|
|
393
441
|
/**
|
|
@@ -418,6 +466,8 @@ const clientRegistrationRequestSchema = z.object({
|
|
|
418
466
|
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
419
467
|
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
420
468
|
backchannel_logout_session_required: z.boolean().optional(),
|
|
469
|
+
frontchannel_logout_uri: SafeUrlSchema.optional(),
|
|
470
|
+
frontchannel_logout_session_required: z.boolean().optional(),
|
|
421
471
|
token_endpoint_auth_method: z.string().trim().min(1).optional(),
|
|
422
472
|
jwks: clientJwksSchema.optional(),
|
|
423
473
|
jwks_uri: z.string().optional(),
|
|
@@ -525,40 +575,6 @@ async function assertIdentifierValid(opts, identifier) {
|
|
|
525
575
|
*/
|
|
526
576
|
const userInfoResource = (baseURL) => `${baseURL}/oauth2/userinfo`;
|
|
527
577
|
/**
|
|
528
|
-
* Re-parses a request's raw `application/x-www-form-urlencoded` body to
|
|
529
|
-
* recover every value of the repeated `resource` parameter (RFC 8707 §2).
|
|
530
|
-
*
|
|
531
|
-
* Workaround for better-call's form-body parser (better-call ≤1.3.5), which
|
|
532
|
-
* collapses repeated form keys with last-write-wins. A client sending
|
|
533
|
-
* `resource=https://a&resource=https://b` would otherwise arrive in the
|
|
534
|
-
* handler as `{ resource: "https://b" }`, silently narrowing the issued
|
|
535
|
-
* token's `aud` to a single resource.
|
|
536
|
-
*
|
|
537
|
-
* Returns the full ordered list when the body is form-encoded and contains
|
|
538
|
-
* any `resource` entries; `undefined` otherwise. Caller MUST enable
|
|
539
|
-
* `cloneRequest: true` on the endpoint — the body stream is read here a
|
|
540
|
-
* second time, and that only works when better-call cloned the request
|
|
541
|
-
* before its own parse.
|
|
542
|
-
*
|
|
543
|
-
* Note: the URL-encoded body path is the only one affected. The query-string
|
|
544
|
-
* path (e.g. `/oauth2/authorize?resource=…&resource=…`) is parsed by the
|
|
545
|
-
* router itself, which DOES array-promote duplicate keys correctly.
|
|
546
|
-
*
|
|
547
|
-
* @internal
|
|
548
|
-
*/
|
|
549
|
-
async function extractRepeatedResourceFromForm(request) {
|
|
550
|
-
if (!(request.headers.get("content-type")?.toLowerCase() ?? "").includes("application/x-www-form-urlencoded")) return;
|
|
551
|
-
let text;
|
|
552
|
-
try {
|
|
553
|
-
text = await request.clone().text();
|
|
554
|
-
} catch {
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
if (!text) return void 0;
|
|
558
|
-
const values = new URLSearchParams(text).getAll("resource").filter((value) => value.length > 0);
|
|
559
|
-
return values.length > 0 ? values : void 0;
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
578
|
* Normalizes a `resource` parameter into an array. Accepts the RFC 8707
|
|
563
579
|
* forms: a single string, a string[], or undefined.
|
|
564
580
|
*
|
|
@@ -1084,14 +1100,21 @@ function getDpopProofJwt(ctx) {
|
|
|
1084
1100
|
return ctx.headers?.get("dpop") ?? void 0;
|
|
1085
1101
|
}
|
|
1086
1102
|
/**
|
|
1087
|
-
* The endpoint's public URL,
|
|
1088
|
-
*
|
|
1103
|
+
* The endpoint's public URL, which clients use as the DPoP `htu`: the origin
|
|
1104
|
+
* and base path of `baseURL` (not `request.url`, whose host proxies and dev
|
|
1105
|
+
* servers can rewrite) plus the request's own path below the base path, so
|
|
1106
|
+
* route params and a trailing slash match what the client called. `path` is
|
|
1107
|
+
* used when there is no HTTP request, such as an `auth.api` call.
|
|
1089
1108
|
*/
|
|
1090
1109
|
function getEndpointUrl(ctx, path) {
|
|
1091
1110
|
const { baseURL } = ctx.context;
|
|
1092
1111
|
let end = baseURL.length;
|
|
1093
1112
|
while (end > 0 && baseURL[end - 1] === "/") end--;
|
|
1094
|
-
|
|
1113
|
+
const base = baseURL.slice(0, end);
|
|
1114
|
+
if (!ctx.request || !base) return `${base}${path}`;
|
|
1115
|
+
const basePath = new URL(base).pathname.replace(/\/$/, "");
|
|
1116
|
+
const requestPath = new URL(ctx.request.url).pathname;
|
|
1117
|
+
return `${base}${requestPath.startsWith(`${basePath}/`) ? requestPath.slice(basePath.length) : path}`;
|
|
1095
1118
|
}
|
|
1096
1119
|
//#endregion
|
|
1097
1120
|
//#region src/oauthClient/client-credentials.ts
|
|
@@ -1125,6 +1148,129 @@ function validateClientCredentialsScopes(scopes, grantTypes, tokenEndpointAuthMe
|
|
|
1125
1148
|
});
|
|
1126
1149
|
}
|
|
1127
1150
|
//#endregion
|
|
1151
|
+
//#region src/token-lookup.ts
|
|
1152
|
+
const jwksInfrastructureErrorCodes = new Set([
|
|
1153
|
+
errors.JWKSTimeout.code,
|
|
1154
|
+
errors.JWKSInvalid.code,
|
|
1155
|
+
errors.JWKSMultipleMatchingKeys.code
|
|
1156
|
+
]);
|
|
1157
|
+
/**
|
|
1158
|
+
* Verifies a JWT access token issued by this server. Returns `"inactive"` for
|
|
1159
|
+
* one that is expired or fails claim validation, so the lookup can stop: it
|
|
1160
|
+
* cannot be another token type. Returns `null` for anything else (an opaque
|
|
1161
|
+
* token, a malformed or forged JWT, a JWT that is not an access token) so the
|
|
1162
|
+
* lookup can continue. Key-set failures still throw.
|
|
1163
|
+
*/
|
|
1164
|
+
async function verifyJwtAccessToken(ctx, opts, token) {
|
|
1165
|
+
if (opts.disableJwtPlugin) return null;
|
|
1166
|
+
if (token.split(".").length !== 3) return null;
|
|
1167
|
+
try {
|
|
1168
|
+
decodeProtectedHeader(token);
|
|
1169
|
+
} catch {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
const jwtPlugin = getJwtPlugin(ctx.context);
|
|
1173
|
+
const jwtPluginOptions = jwtPlugin.options;
|
|
1174
|
+
const jwks = await getJwks(token, {
|
|
1175
|
+
jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
|
|
1176
|
+
return (await jwtPlugin.endpoints.getJwks(ctx))?.response;
|
|
1177
|
+
},
|
|
1178
|
+
jwksCacheKey: jwtPlugin
|
|
1179
|
+
});
|
|
1180
|
+
let payload;
|
|
1181
|
+
try {
|
|
1182
|
+
({payload} = await jwtVerify(token, createLocalJWKSet(jwks), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL }));
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
if (error instanceof errors.JWTExpired || error instanceof errors.JWTClaimValidationFailed) return isAccessTokenPayload(error.payload) ? "inactive" : null;
|
|
1185
|
+
if (error instanceof errors.JOSEError && !jwksInfrastructureErrorCodes.has(error.code)) return null;
|
|
1186
|
+
throw error;
|
|
1187
|
+
}
|
|
1188
|
+
return isAccessTokenPayload(payload) ? payload : null;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* The OAuth token endpoint stamps `azp` (the client) on every JWT access token.
|
|
1192
|
+
* The JWT plugin signs other JWTs with the same issuer and keys, such as its
|
|
1193
|
+
* `/token` session JWT, so one without `azp` is not an access token.
|
|
1194
|
+
*/
|
|
1195
|
+
function isAccessTokenPayload(payload) {
|
|
1196
|
+
return typeof payload.azp === "string" && payload.azp.length > 0;
|
|
1197
|
+
}
|
|
1198
|
+
async function findOpaqueAccessToken(ctx, opts, token) {
|
|
1199
|
+
const prefix = opts.prefix?.opaqueAccessToken;
|
|
1200
|
+
if (prefix && !token.startsWith(prefix)) return null;
|
|
1201
|
+
return ctx.context.adapter.findOne({
|
|
1202
|
+
model: "oauthAccessToken",
|
|
1203
|
+
where: [{
|
|
1204
|
+
field: "token",
|
|
1205
|
+
value: await getStoredToken(opts.storeTokens, prefix ? token.slice(prefix.length) : token, "access_token")
|
|
1206
|
+
}]
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Looks up a refresh token by its presented value, after removing the
|
|
1211
|
+
* configured prefix and applying `formatRefreshToken.decrypt`. Returns `null`
|
|
1212
|
+
* when the value is not a refresh token this server stored.
|
|
1213
|
+
*/
|
|
1214
|
+
async function findRefreshToken(ctx, opts, token) {
|
|
1215
|
+
const prefix = opts.prefix?.refreshToken;
|
|
1216
|
+
if (prefix && !token.startsWith(prefix)) return null;
|
|
1217
|
+
const value = prefix ? token.slice(prefix.length) : token;
|
|
1218
|
+
let decoded;
|
|
1219
|
+
try {
|
|
1220
|
+
decoded = opts.formatRefreshToken?.decrypt ? await opts.formatRefreshToken.decrypt(value) : { token: value };
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
if (error instanceof APIError$1 && error.statusCode === 400 || error instanceof errors.JOSEError) return null;
|
|
1223
|
+
throw error;
|
|
1224
|
+
}
|
|
1225
|
+
return ctx.context.adapter.findOne({
|
|
1226
|
+
model: "oauthRefreshToken",
|
|
1227
|
+
where: [{
|
|
1228
|
+
field: "token",
|
|
1229
|
+
value: await getStoredToken(opts.storeTokens, decoded.token, "refresh_token")
|
|
1230
|
+
}]
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
async function identifyAccessToken(ctx, opts, token) {
|
|
1234
|
+
const payload = await verifyJwtAccessToken(ctx, opts, token);
|
|
1235
|
+
if (payload === "inactive") return payload;
|
|
1236
|
+
if (payload) return {
|
|
1237
|
+
type: "jwt",
|
|
1238
|
+
payload
|
|
1239
|
+
};
|
|
1240
|
+
const record = await findOpaqueAccessToken(ctx, opts, token);
|
|
1241
|
+
return record ? {
|
|
1242
|
+
type: "opaque",
|
|
1243
|
+
record
|
|
1244
|
+
} : null;
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Identifies an access token as a JWT or an opaque token issued by this
|
|
1248
|
+
* server. Returns `null` when it is neither, including an expired JWT.
|
|
1249
|
+
*/
|
|
1250
|
+
async function findAccessToken(ctx, opts, token) {
|
|
1251
|
+
const found = await identifyAccessToken(ctx, opts, token);
|
|
1252
|
+
return found === "inactive" ? null : found;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Identifies a token presented to revocation or introspection. Every token
|
|
1256
|
+
* type is searched; `token_type_hint` only decides which one is tried first
|
|
1257
|
+
* (RFC 7009 §2.1, RFC 7662 §2.1). Returns `null` when no type matches. Only
|
|
1258
|
+
* infrastructure failures (key set, database, refresh-token decryption) throw.
|
|
1259
|
+
*/
|
|
1260
|
+
async function findToken(ctx, opts, token, tokenTypeHint) {
|
|
1261
|
+
const asRefreshToken = async () => {
|
|
1262
|
+
const record = await findRefreshToken(ctx, opts, token);
|
|
1263
|
+
return record ? {
|
|
1264
|
+
type: "refresh",
|
|
1265
|
+
record
|
|
1266
|
+
} : null;
|
|
1267
|
+
};
|
|
1268
|
+
if (tokenTypeHint === "refresh_token") return await asRefreshToken() ?? findAccessToken(ctx, opts, token);
|
|
1269
|
+
const found = await identifyAccessToken(ctx, opts, token);
|
|
1270
|
+
if (found === "inactive") return null;
|
|
1271
|
+
return found ?? asRefreshToken();
|
|
1272
|
+
}
|
|
1273
|
+
//#endregion
|
|
1128
1274
|
//#region src/userinfo.ts
|
|
1129
1275
|
/**
|
|
1130
1276
|
* Builds the standard OIDC claims (OIDC Core §5.1) for the UserInfo response.
|
|
@@ -1335,6 +1481,18 @@ function getOAuthProviderApi(ctx, opts, grantType) {
|
|
|
1335
1481
|
grantType
|
|
1336
1482
|
});
|
|
1337
1483
|
},
|
|
1484
|
+
resolveConfirmation: async (params) => {
|
|
1485
|
+
if (params.confirmation) return params.confirmation;
|
|
1486
|
+
const policy = await resolveResourcePolicy(ctx, opts, {
|
|
1487
|
+
resource: params.resources,
|
|
1488
|
+
clientId: params.client.clientId,
|
|
1489
|
+
requestedScopes: params.scopes
|
|
1490
|
+
});
|
|
1491
|
+
return resolveDpopTokenBinding(ctx, opts, {
|
|
1492
|
+
client: params.client,
|
|
1493
|
+
grantIssuance: { dpopBoundAccessTokensRequired: policy.dpopBoundAccessTokensRequired }
|
|
1494
|
+
});
|
|
1495
|
+
},
|
|
1338
1496
|
hashToken: (token, type) => storeToken(opts.storeTokens, token, type),
|
|
1339
1497
|
validateAccessToken: async (token, clientId) => {
|
|
1340
1498
|
const { validateAccessToken } = await Promise.resolve().then(() => introspect_exports);
|
|
@@ -1364,6 +1522,7 @@ async function createJwtAccessToken(ctx, opts, user, client, audienceClaim, scop
|
|
|
1364
1522
|
azp: client.clientId,
|
|
1365
1523
|
scope: scopes.join(" "),
|
|
1366
1524
|
sid: overrides?.sid,
|
|
1525
|
+
reference_id: overrides?.referenceId,
|
|
1367
1526
|
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
1368
1527
|
iat,
|
|
1369
1528
|
exp,
|
|
@@ -1389,7 +1548,7 @@ async function computeOidcHash(token, signingAlg) {
|
|
|
1389
1548
|
* Creates a user id token in code_authorization with scope of 'openid'
|
|
1390
1549
|
* and hybrid/implicit (not yet implemented) flows
|
|
1391
1550
|
*/
|
|
1392
|
-
async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken, extraClaims) {
|
|
1551
|
+
async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken, extraClaims, authenticationContext) {
|
|
1393
1552
|
const iat = Math.floor(Date.now() / 1e3);
|
|
1394
1553
|
const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
|
|
1395
1554
|
const resolvedSub = await resolveSubjectIdentifier(user.id, client, opts);
|
|
@@ -1403,11 +1562,12 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
|
|
|
1403
1562
|
const resolvedKey = !opts.disableJwtPlugin && !jwtPluginOptions?.jwt?.sign ? await resolveSigningKey(ctx, jwtPluginOptions) : void 0;
|
|
1404
1563
|
const signingAlg = opts.disableJwtPlugin ? "HS256" : resolvedKey?.alg ?? jwtPluginOptions?.jwks?.keyPairConfig?.alg;
|
|
1405
1564
|
const atHash = accessToken ? await computeOidcHash(accessToken, signingAlg) : void 0;
|
|
1406
|
-
const emitSid = Boolean(client.enableEndSession || client.backchannelLogoutUri);
|
|
1565
|
+
const emitSid = Boolean(client.enableEndSession || client.backchannelLogoutUri || client.frontchannelLogoutUri);
|
|
1407
1566
|
const payload = {
|
|
1408
1567
|
...ID_TOKEN_SCOPE_CLAIM_GUARDS,
|
|
1409
1568
|
auth_time: authTimeSec,
|
|
1410
|
-
acr: "0",
|
|
1569
|
+
acr: authenticationContext?.acr ?? "0",
|
|
1570
|
+
amr: authenticationContext?.amr,
|
|
1411
1571
|
...customClaims,
|
|
1412
1572
|
at_hash: atHash,
|
|
1413
1573
|
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
@@ -1439,20 +1599,7 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
|
|
|
1439
1599
|
* Encodes a refresh token for a client
|
|
1440
1600
|
*/
|
|
1441
1601
|
async function encodeRefreshToken(opts, token, sessionId) {
|
|
1442
|
-
return (opts.prefix?.refreshToken ?? "") + (opts.formatRefreshToken?.encrypt ? opts.formatRefreshToken.encrypt(token, sessionId) : token);
|
|
1443
|
-
}
|
|
1444
|
-
/**
|
|
1445
|
-
* Decodes a refresh token for a client
|
|
1446
|
-
*
|
|
1447
|
-
* @internal
|
|
1448
|
-
*/
|
|
1449
|
-
async function decodeRefreshToken(opts, token) {
|
|
1450
|
-
if (opts.prefix?.refreshToken) if (token.startsWith(opts.prefix.refreshToken)) token = token.replace(opts.prefix.refreshToken, "");
|
|
1451
|
-
else throw new APIError("BAD_REQUEST", {
|
|
1452
|
-
error_description: "refresh token not found",
|
|
1453
|
-
error: "invalid_token"
|
|
1454
|
-
});
|
|
1455
|
-
return opts.formatRefreshToken?.decrypt ? opts.formatRefreshToken?.decrypt(token) : { token };
|
|
1602
|
+
return (opts.prefix?.refreshToken ?? "") + (opts.formatRefreshToken?.encrypt ? await opts.formatRefreshToken.encrypt(token, sessionId) : token);
|
|
1456
1603
|
}
|
|
1457
1604
|
async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, authorizationCodeId, refreshId, confirmation, requestedUserInfoClaims) {
|
|
1458
1605
|
const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
|
|
@@ -1542,7 +1689,7 @@ async function revokeTokensIssuedForAuthorizationCode(ctx, authorizationCodeId)
|
|
|
1542
1689
|
await deleteIssuedTokens("oauthAccessToken");
|
|
1543
1690
|
await deleteIssuedTokens("oauthRefreshToken");
|
|
1544
1691
|
}
|
|
1545
|
-
async function createRefreshToken(ctx, opts, user, referenceId, authorizationCodeId, client, scopes, payload, originalRefresh, authTime, resources, confirmation, requestedUserInfoClaims) {
|
|
1692
|
+
async function createRefreshToken(ctx, opts, user, referenceId, authorizationCodeId, client, scopes, payload, originalRefresh, authTime, resources, confirmation, requestedUserInfoClaims, authenticationContext) {
|
|
1546
1693
|
const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
|
|
1547
1694
|
const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
|
|
1548
1695
|
const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
|
|
@@ -1555,6 +1702,8 @@ async function createRefreshToken(ctx, opts, user, referenceId, authorizationCod
|
|
|
1555
1702
|
referenceId,
|
|
1556
1703
|
authorizationCodeId,
|
|
1557
1704
|
authTime,
|
|
1705
|
+
acr: authenticationContext?.acr,
|
|
1706
|
+
amr: authenticationContext?.amr,
|
|
1558
1707
|
confirmation,
|
|
1559
1708
|
requestedUserInfoClaims: requestedUserInfoClaims?.length ? requestedUserInfoClaims : void 0,
|
|
1560
1709
|
scopes,
|
|
@@ -1777,9 +1926,18 @@ function replayRefreshTokenRotationResponse(ctx, response) {
|
|
|
1777
1926
|
});
|
|
1778
1927
|
}
|
|
1779
1928
|
async function createUserTokens(ctx, opts, params) {
|
|
1780
|
-
const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, verificationValue } = params;
|
|
1929
|
+
const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, acr, amr, verificationValue } = params;
|
|
1930
|
+
const authenticationContext = {
|
|
1931
|
+
acr,
|
|
1932
|
+
amr
|
|
1933
|
+
};
|
|
1781
1934
|
const iat = Math.floor(Date.now() / 1e3);
|
|
1782
|
-
const
|
|
1935
|
+
const baseExpiry = user ? opts.accessTokenExpiresIn ?? 3600 : opts.m2mAccessTokenExpiresIn ?? 3600;
|
|
1936
|
+
const defaultExp = Math.min(iat + baseExpiry, params.maxExpiresAt ? Math.floor(params.maxExpiresAt.getTime() / 1e3) : Number.POSITIVE_INFINITY);
|
|
1937
|
+
if (defaultExp <= iat) throw new APIError("BAD_REQUEST", {
|
|
1938
|
+
error: "invalid_request",
|
|
1939
|
+
error_description: "the token this grant derives from has expired"
|
|
1940
|
+
});
|
|
1783
1941
|
const scopeExp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
|
|
1784
1942
|
return prev < curr ? prev : curr;
|
|
1785
1943
|
}, defaultExp) : defaultExp;
|
|
@@ -1834,7 +1992,7 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
1834
1992
|
iat,
|
|
1835
1993
|
exp: refreshTokenExp,
|
|
1836
1994
|
sid: sessionId
|
|
1837
|
-
}, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0;
|
|
1995
|
+
}, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims, authenticationContext) : void 0;
|
|
1838
1996
|
const accessTokenClaims = isJwtAccessToken ? await resolveAccessTokenClaims({
|
|
1839
1997
|
ctx,
|
|
1840
1998
|
opts,
|
|
@@ -1853,6 +2011,7 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
1853
2011
|
iat,
|
|
1854
2012
|
exp,
|
|
1855
2013
|
sid: sessionId,
|
|
2014
|
+
referenceId,
|
|
1856
2015
|
signingAlgorithm: grantIssuance.signingAlgorithm,
|
|
1857
2016
|
signingKeyId: grantIssuance.signingKeyId,
|
|
1858
2017
|
accessTokenClaims,
|
|
@@ -1865,8 +2024,8 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
1865
2024
|
iat,
|
|
1866
2025
|
exp: refreshTokenExp,
|
|
1867
2026
|
sid: sessionId
|
|
1868
|
-
}, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0]);
|
|
1869
|
-
const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, effectiveScopes, nonce, sessionId, authTime, accessToken, additionalIdTokenClaims) : void 0;
|
|
2027
|
+
}, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims, authenticationContext) : void 0]);
|
|
2028
|
+
const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, effectiveScopes, nonce, sessionId, authTime, accessToken, additionalIdTokenClaims, authenticationContext) : void 0;
|
|
1870
2029
|
const responseBody = {
|
|
1871
2030
|
...customFields,
|
|
1872
2031
|
...params.tokenResponse ?? {},
|
|
@@ -1971,6 +2130,10 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
1971
2130
|
error_description: "Either code_verifier or client_secret is required",
|
|
1972
2131
|
error: "invalid_request"
|
|
1973
2132
|
});
|
|
2133
|
+
/** Verify Client (RFC 6749 §5.2: an unknown or unauthenticated client is
|
|
2134
|
+
* `invalid_client`, so resolve it before the code lookup can report
|
|
2135
|
+
* `invalid_grant` and before a one-time code is consumed). */
|
|
2136
|
+
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, "authorization_code", authMethod);
|
|
1974
2137
|
/** Get and check Verification Value */
|
|
1975
2138
|
const { verificationValue, effectiveResources, authorizedResources, authorizationCodeId } = await checkVerificationValue(ctx, opts, code, client_id, redirect_uri, resources);
|
|
1976
2139
|
const scopes = verificationValue.query.scope?.split(" ");
|
|
@@ -1978,8 +2141,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
1978
2141
|
error_description: "verification scope unset",
|
|
1979
2142
|
error: "invalid_scope"
|
|
1980
2143
|
});
|
|
1981
|
-
|
|
1982
|
-
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerified, "authorization_code", authMethod);
|
|
2144
|
+
validateClientScopes(client, scopes);
|
|
1983
2145
|
if (isPKCERequired(client, {
|
|
1984
2146
|
scopes: (verificationValue.query?.scope)?.split(" ") || [],
|
|
1985
2147
|
nonce: verificationValue.query?.nonce
|
|
@@ -1996,17 +2158,17 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
1996
2158
|
const pkceUsedInAuth = !!verificationValue.query?.code_challenge;
|
|
1997
2159
|
const pkceUsedInToken = !!code_verifier;
|
|
1998
2160
|
if (pkceUsedInAuth || pkceUsedInToken) {
|
|
1999
|
-
if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("
|
|
2161
|
+
if (pkceUsedInAuth && !pkceUsedInToken) throw new APIError("BAD_REQUEST", {
|
|
2000
2162
|
error_description: "code_verifier required because PKCE was used in authorization",
|
|
2001
2163
|
error: "invalid_request"
|
|
2002
2164
|
});
|
|
2003
|
-
if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("
|
|
2165
|
+
if (!pkceUsedInAuth && pkceUsedInToken) throw new APIError("BAD_REQUEST", {
|
|
2004
2166
|
error_description: "code_verifier provided but PKCE was not used in authorization",
|
|
2005
2167
|
error: "invalid_request"
|
|
2006
2168
|
});
|
|
2007
|
-
if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("
|
|
2169
|
+
if ((verificationValue.query?.code_challenge_method === "S256" ? await generateCodeChallenge(code_verifier) : void 0) !== verificationValue.query?.code_challenge) throw new APIError("BAD_REQUEST", {
|
|
2008
2170
|
error_description: "code verification failed",
|
|
2009
|
-
error: "
|
|
2171
|
+
error: "invalid_grant"
|
|
2010
2172
|
});
|
|
2011
2173
|
}
|
|
2012
2174
|
/** Get user */
|
|
@@ -2041,6 +2203,8 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
2041
2203
|
sessionId: session.id,
|
|
2042
2204
|
nonce: verificationValue.query?.nonce,
|
|
2043
2205
|
authTime,
|
|
2206
|
+
acr: verificationValue.acr,
|
|
2207
|
+
amr: verificationValue.amr,
|
|
2044
2208
|
verificationValue,
|
|
2045
2209
|
authorizationCodeId,
|
|
2046
2210
|
requestedUserInfoClaims,
|
|
@@ -2109,14 +2273,11 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
2109
2273
|
error_description: "Missing a required refresh_token for refresh_token grant",
|
|
2110
2274
|
error: "invalid_request"
|
|
2111
2275
|
});
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
value: await getStoredToken(opts.storeTokens, decodedRefresh.token, "refresh_token")
|
|
2118
|
-
}]
|
|
2119
|
-
});
|
|
2276
|
+
/** Verify Client (RFC 6749 §5.2: an unknown or unauthenticated client is
|
|
2277
|
+
* `invalid_client`, so resolve it before the refresh token lookup can
|
|
2278
|
+
* report `invalid_grant`). */
|
|
2279
|
+
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, "refresh_token", authMethod);
|
|
2280
|
+
const refreshToken = await findRefreshToken(ctx, opts, refresh_token);
|
|
2120
2281
|
if (!refreshToken) throw new APIError("BAD_REQUEST", {
|
|
2121
2282
|
error_description: "session not found",
|
|
2122
2283
|
error: "invalid_grant"
|
|
@@ -2142,7 +2303,7 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
2142
2303
|
error: "invalid_scope"
|
|
2143
2304
|
});
|
|
2144
2305
|
}
|
|
2145
|
-
|
|
2306
|
+
validateClientScopes(client, requestedScopes ?? scopes);
|
|
2146
2307
|
if (refreshToken.revoked) {
|
|
2147
2308
|
if (isWithinRefreshTokenReuseInterval(refreshToken)) {
|
|
2148
2309
|
const replay = await getRefreshTokenRotationReplay(ctx, refreshToken, await resolveRefreshTokenRotationReplayRequest(ctx, opts, {
|
|
@@ -2180,6 +2341,8 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
2180
2341
|
refreshToken,
|
|
2181
2342
|
resources: resources ?? refreshToken.resources,
|
|
2182
2343
|
authTime,
|
|
2344
|
+
acr: refreshToken.acr ?? void 0,
|
|
2345
|
+
amr: refreshToken.amr ?? void 0,
|
|
2183
2346
|
requestedUserInfoClaims: refreshToken.requestedUserInfoClaims
|
|
2184
2347
|
});
|
|
2185
2348
|
}
|
|
@@ -2200,11 +2363,11 @@ function inactiveAccessToken() {
|
|
|
2200
2363
|
};
|
|
2201
2364
|
}
|
|
2202
2365
|
/**
|
|
2203
|
-
*
|
|
2204
|
-
* Introspection follows RFC7662
|
|
2366
|
+
* Introspection follows RFC 7662
|
|
2205
2367
|
* https://datatracker.ietf.org/doc/html/rfc7662
|
|
2206
|
-
*
|
|
2207
|
-
*
|
|
2368
|
+
*
|
|
2369
|
+
* A token this server cannot find, or one the caller may not see, resolves as
|
|
2370
|
+
* `{ active: false }` (§2.2, §4). Only protocol errors surface.
|
|
2208
2371
|
*/
|
|
2209
2372
|
/**
|
|
2210
2373
|
* Resource identifiers in a token's audience, excluding the implicit
|
|
@@ -2230,38 +2393,14 @@ async function isIntrospectionAuthorized(ctx, opts, params) {
|
|
|
2230
2393
|
return isClientLinkedToAnyResource(ctx, opts, introspectingClientId, audienceResources);
|
|
2231
2394
|
}
|
|
2232
2395
|
/**
|
|
2233
|
-
* Validates a JWT access token
|
|
2396
|
+
* Validates a JWT access token whose signature and issuer are verified.
|
|
2234
2397
|
*
|
|
2235
2398
|
* @returns RFC7662 introspection format
|
|
2236
2399
|
*/
|
|
2237
|
-
async function validateJwtAccessToken(ctx, opts,
|
|
2238
|
-
const
|
|
2239
|
-
const jwtPluginOptions = jwtPlugin?.options;
|
|
2240
|
-
const userInfoAud = `${ctx.context.baseURL ?? ""}/oauth2/userinfo`;
|
|
2241
|
-
const expectedIssuer = jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL;
|
|
2242
|
-
let jwtPayload;
|
|
2243
|
-
try {
|
|
2244
|
-
jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
|
|
2245
|
-
jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
|
|
2246
|
-
return (await jwtPlugin?.endpoints.getJwks(ctx))?.response;
|
|
2247
|
-
},
|
|
2248
|
-
jwksCacheKey: jwtPlugin
|
|
2249
|
-
})), { issuer: expectedIssuer })).payload;
|
|
2250
|
-
} catch (error) {
|
|
2251
|
-
if (error instanceof Error) {
|
|
2252
|
-
if (error.name === "TypeError" || error.name === "JWSInvalid") throw new APIError$1("BAD_REQUEST", {
|
|
2253
|
-
error_description: "invalid JWT signature",
|
|
2254
|
-
error: "invalid_request"
|
|
2255
|
-
});
|
|
2256
|
-
else if (error.name === "JWTExpired") return { active: false };
|
|
2257
|
-
else if (error.name === "JWTInvalid") return { active: false };
|
|
2258
|
-
throw error;
|
|
2259
|
-
}
|
|
2260
|
-
throw new Error(error);
|
|
2261
|
-
}
|
|
2400
|
+
async function validateJwtAccessToken(ctx, opts, jwtPayload, clientId) {
|
|
2401
|
+
const userInfoAud = userInfoResource(ctx.context.baseURL ?? "");
|
|
2262
2402
|
const rawAud = jwtPayload.aud;
|
|
2263
2403
|
if (!await isAudienceClaimAllowed(ctx, opts, rawAud, [userInfoAud])) return { active: false };
|
|
2264
|
-
if (!jwtPayload.azp) return { active: false };
|
|
2265
2404
|
const client = await getClient(ctx, opts, jwtPayload.azp);
|
|
2266
2405
|
if (!client || client?.disabled) return { active: false };
|
|
2267
2406
|
if (!await isIntrospectionAuthorized(ctx, opts, {
|
|
@@ -2286,28 +2425,11 @@ async function validateJwtAccessToken(ctx, opts, token, clientId) {
|
|
|
2286
2425
|
return jwtPayload;
|
|
2287
2426
|
}
|
|
2288
2427
|
/**
|
|
2289
|
-
*
|
|
2428
|
+
* Validates an opaque access token record.
|
|
2290
2429
|
*
|
|
2291
2430
|
* @returns RFC7662 introspection format
|
|
2292
2431
|
*/
|
|
2293
|
-
async function validateOpaqueAccessToken(ctx, opts,
|
|
2294
|
-
let tokenValue = token;
|
|
2295
|
-
if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
|
|
2296
|
-
else throw new APIError$1("BAD_REQUEST", {
|
|
2297
|
-
error_description: "opaque access token not found",
|
|
2298
|
-
error: "invalid_request"
|
|
2299
|
-
});
|
|
2300
|
-
const accessToken = await ctx.context.adapter.findOne({
|
|
2301
|
-
model: "oauthAccessToken",
|
|
2302
|
-
where: [{
|
|
2303
|
-
field: "token",
|
|
2304
|
-
value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
|
|
2305
|
-
}]
|
|
2306
|
-
});
|
|
2307
|
-
if (!accessToken) throw new APIError$1("BAD_REQUEST", {
|
|
2308
|
-
error_description: "opaque access token not found",
|
|
2309
|
-
error: "invalid_token"
|
|
2310
|
-
});
|
|
2432
|
+
async function validateOpaqueAccessToken(ctx, opts, accessToken, clientId) {
|
|
2311
2433
|
if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return inactiveAccessToken();
|
|
2312
2434
|
if (accessToken.revoked) return inactiveAccessToken();
|
|
2313
2435
|
const resources = Array.isArray(accessToken.resources) ? accessToken.resources : void 0;
|
|
@@ -2366,6 +2488,7 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
2366
2488
|
azp: accessToken.clientId,
|
|
2367
2489
|
sub: user?.id,
|
|
2368
2490
|
sid: sessionId,
|
|
2491
|
+
reference_id: accessToken.referenceId ?? void 0,
|
|
2369
2492
|
exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
|
|
2370
2493
|
iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
|
|
2371
2494
|
scope: accessToken.scopes?.join(" "),
|
|
@@ -2376,22 +2499,11 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
2376
2499
|
};
|
|
2377
2500
|
}
|
|
2378
2501
|
/**
|
|
2379
|
-
* Validates a refresh token
|
|
2502
|
+
* Validates a refresh token record.
|
|
2380
2503
|
*
|
|
2381
2504
|
* @returns payload in RFC7662 introspection format
|
|
2382
2505
|
*/
|
|
2383
|
-
async function validateRefreshToken(ctx, opts,
|
|
2384
|
-
const refreshToken = await ctx.context.adapter.findOne({
|
|
2385
|
-
model: "oauthRefreshToken",
|
|
2386
|
-
where: [{
|
|
2387
|
-
field: "token",
|
|
2388
|
-
value: await getStoredToken(opts.storeTokens, token, "refresh_token")
|
|
2389
|
-
}]
|
|
2390
|
-
});
|
|
2391
|
-
if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
|
|
2392
|
-
error_description: "token not found",
|
|
2393
|
-
error: "invalid_token"
|
|
2394
|
-
});
|
|
2506
|
+
async function validateRefreshToken(ctx, opts, refreshToken, clientId) {
|
|
2395
2507
|
if (!refreshToken.clientId || refreshToken.clientId !== clientId) return { active: false };
|
|
2396
2508
|
if (!refreshToken.expiresAt || refreshToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
|
|
2397
2509
|
if (refreshToken.revoked) return { active: false };
|
|
@@ -2414,6 +2526,7 @@ async function validateRefreshToken(ctx, opts, token, clientId) {
|
|
|
2414
2526
|
iss: ((opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options)?.jwt?.issuer ?? ctx.context.baseURL,
|
|
2415
2527
|
sub: user?.id,
|
|
2416
2528
|
sid: sessionId,
|
|
2529
|
+
reference_id: refreshToken.referenceId ?? void 0,
|
|
2417
2530
|
exp: Math.floor(new Date(refreshToken.expiresAt).getTime() / 1e3),
|
|
2418
2531
|
iat: Math.floor(new Date(refreshToken.createdAt).getTime() / 1e3),
|
|
2419
2532
|
scope: refreshToken.scopes?.join(" "),
|
|
@@ -2427,32 +2540,23 @@ function createInvalidAccessTokenError() {
|
|
|
2427
2540
|
error: "invalid_token"
|
|
2428
2541
|
}, { "WWW-Authenticate": INVALID_ACCESS_TOKEN_WWW_AUTHENTICATE });
|
|
2429
2542
|
}
|
|
2430
|
-
function
|
|
2431
|
-
|
|
2543
|
+
async function validateFoundAccessToken(ctx, opts, found, clientId) {
|
|
2544
|
+
if (found.type === "opaque") return validateOpaqueAccessToken(ctx, opts, found.record, clientId);
|
|
2545
|
+
return {
|
|
2546
|
+
payload: await validateJwtAccessToken(ctx, opts, found.payload, clientId),
|
|
2547
|
+
requestedUserInfoClaims: []
|
|
2548
|
+
};
|
|
2432
2549
|
}
|
|
2433
2550
|
/**
|
|
2434
|
-
*
|
|
2435
|
-
*
|
|
2436
|
-
*
|
|
2437
|
-
*
|
|
2551
|
+
* Validates an access token (JWT or opaque). Returns the RFC 7662
|
|
2552
|
+
* introspection payload, inactive for an unknown token, alongside the
|
|
2553
|
+
* per-issuance UserInfo claims the opaque row persisted (empty for a JWT access
|
|
2554
|
+
* token, which resolves UserInfo claims by scope).
|
|
2438
2555
|
*/
|
|
2439
2556
|
async function resolveAccessTokenValidation(ctx, opts, token, clientId) {
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
requestedUserInfoClaims: []
|
|
2444
|
-
};
|
|
2445
|
-
} catch (err) {
|
|
2446
|
-
if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
|
|
2447
|
-
else throw new Error(err);
|
|
2448
|
-
}
|
|
2449
|
-
try {
|
|
2450
|
-
return await validateOpaqueAccessToken(ctx, opts, token, clientId);
|
|
2451
|
-
} catch (err) {
|
|
2452
|
-
if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
|
|
2453
|
-
else throw new Error("Unknown error validating access token");
|
|
2454
|
-
}
|
|
2455
|
-
throw createInvalidAccessTokenError();
|
|
2557
|
+
const found = await findAccessToken(ctx, opts, token);
|
|
2558
|
+
if (!found) return inactiveAccessToken();
|
|
2559
|
+
return validateFoundAccessToken(ctx, opts, found, clientId);
|
|
2456
2560
|
}
|
|
2457
2561
|
/**
|
|
2458
2562
|
* Validates an access token (JWT or opaque) and returns its RFC 7662
|
|
@@ -2501,7 +2605,6 @@ async function resolveIntrospectionSub(ctx, opts, payload, introspectingClient)
|
|
|
2501
2605
|
}
|
|
2502
2606
|
async function introspectEndpoint(ctx, opts) {
|
|
2503
2607
|
let { token, token_type_hint } = ctx.body;
|
|
2504
|
-
if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
|
|
2505
2608
|
const { clientId: client_id, clientSecret: client_secret, preVerified, authMethod } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
|
|
2506
2609
|
if (!client_id || !client_secret && !preVerified) throw new APIError$1("UNAUTHORIZED", {
|
|
2507
2610
|
error_description: "missing required credentials",
|
|
@@ -2514,38 +2617,16 @@ async function introspectEndpoint(ctx, opts) {
|
|
|
2514
2617
|
});
|
|
2515
2618
|
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, void 0, authMethod);
|
|
2516
2619
|
try {
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
} else if (error instanceof Error) throw error;
|
|
2523
|
-
else throw new Error(error);
|
|
2524
|
-
}
|
|
2525
|
-
if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
|
|
2526
|
-
return resolveIntrospectionSub(ctx, opts, await validateRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId), client);
|
|
2527
|
-
} catch (error) {
|
|
2528
|
-
if (error instanceof APIError$1) {
|
|
2529
|
-
if (token_type_hint === "refresh_token") throw error;
|
|
2530
|
-
} else if (error instanceof Error) throw error;
|
|
2531
|
-
else throw new Error(error);
|
|
2532
|
-
}
|
|
2533
|
-
throw new APIError$1("BAD_REQUEST", {
|
|
2534
|
-
error_description: "token not found",
|
|
2535
|
-
error: "invalid_request"
|
|
2536
|
-
});
|
|
2620
|
+
const found = await findToken(ctx, opts, token, token_type_hint);
|
|
2621
|
+
if (!found) return { active: false };
|
|
2622
|
+
if (found.type === "refresh") return resolveIntrospectionSub(ctx, opts, await validateRefreshToken(ctx, opts, found.record, client.clientId), client);
|
|
2623
|
+
const { payload } = await validateFoundAccessToken(ctx, opts, found, client.clientId);
|
|
2624
|
+
return resolveIntrospectionSub(ctx, opts, payload, client);
|
|
2537
2625
|
} catch (error) {
|
|
2538
|
-
if (error instanceof APIError$1)
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
} else if (error instanceof Error) {
|
|
2542
|
-
logger.error("Introspection error:", error.message, error.stack);
|
|
2543
|
-
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
2544
|
-
} else {
|
|
2545
|
-
logger.error("Introspection error:", error);
|
|
2546
|
-
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
2547
|
-
}
|
|
2626
|
+
if (error instanceof APIError$1) throw error;
|
|
2627
|
+
logger.error("Introspection error:", error);
|
|
2628
|
+
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
2548
2629
|
}
|
|
2549
2630
|
}
|
|
2550
2631
|
//#endregion
|
|
2551
|
-
export {
|
|
2632
|
+
export { getAcrValuesSupported as A, authorizationQuerySchema as C, STANDARD_CLAIMS as D, oauthClientMetadataSchema as E, getRequestedUserInfoClaims as F, isValidOidcClaimsRequest as I, canSatisfyEssentialAcrRequest as M, claimsRequestParameterSchema as N, STANDARD_CLAIM_NAMES as O, filterClaimsRequestUserInfoClaims as P, SafeUrlSchema as S, clientRegistrationRequestSchema as T, logStartupSeedFailure as _, tokenEndpoint as a, userInfoResource as b, normalizeClientCredentialsScopes as c, JWS_ALGORITHMS as d, assertIdentifierValid as f, logEnforcePerClientResourcesResolution as g, isAudienceClaimAllowed as h, invalidateRefreshFamily as i, resolveAuthenticationContext as j, getSupportedClaims as k, validateClientCredentialsScopes as l, invalidateResourceCache as m, introspect_exports as n, userInfoEndpoint as o, getResource as p, getOAuthProviderApi as r, findToken as s, introspectEndpoint as t, getEndpointUrl as u, resolveResourcePolicy as v, clientJwksSchema as w, ResourceUriSchema as x, seedResourcesOnce as y };
|