@better-auth/oauth-provider 1.7.10 → 1.8.0-beta.1
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-dkOG8GqZ.mjs → authorize-Dg976Ug0.mjs} +583 -433
- package/dist/client-resource.d.mts +2 -2
- package/dist/client-resource.mjs +2 -2
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/index-DVydGHlT.d.mts +275 -0
- package/dist/index.d.mts +4 -203
- package/dist/index.mjs +196 -40
- package/dist/internal.d.mts +109 -22
- package/dist/internal.mjs +4 -2
- package/dist/{introspect-BVm_zFQC.mjs → introspect-DdmCPEHN.mjs} +294 -227
- package/dist/{oauth-DXiqbRzK.d.mts → oauth-CzT1PvAY.d.mts} +359 -13
- package/dist/{oauth-Cse_eZ6i.d.mts → oauth-DfH-ghKj.d.mts} +163 -87
- package/dist/{utils-DfMNRh3J.mjs → utils-D-CUrvd1.mjs} +287 -20
- package/dist/{version-aKfgWHzo.mjs → version-BwHks7Wg.mjs} +1 -1
- package/package.json +6 -6
- package/dist/client-jwks-DY2QBrsl.d.mts +0 -25
|
@@ -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-D-CUrvd1.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
|
-
import { SignJWT, base64url, createLocalJWKSet, decodeProtectedHeader, errors, jwtVerify } from "jose";
|
|
9
8
|
import { createDpopReplayStore, enforceDpopBinding, generateCodeChallenge, getConfirmationJkt, getDpopJktFromPayload, getJwks, isDpopBindingError, isDpopProofError, parseAccessTokenAuthorization, stripAccessTokenAuthorizationScheme, verifyDpopProof } from "better-auth/oauth2";
|
|
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
|
*
|
|
@@ -1132,6 +1148,129 @@ function validateClientCredentialsScopes(scopes, grantTypes, tokenEndpointAuthMe
|
|
|
1132
1148
|
});
|
|
1133
1149
|
}
|
|
1134
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
|
|
1135
1274
|
//#region src/userinfo.ts
|
|
1136
1275
|
/**
|
|
1137
1276
|
* Builds the standard OIDC claims (OIDC Core §5.1) for the UserInfo response.
|
|
@@ -1342,6 +1481,18 @@ function getOAuthProviderApi(ctx, opts, grantType) {
|
|
|
1342
1481
|
grantType
|
|
1343
1482
|
});
|
|
1344
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
|
+
},
|
|
1345
1496
|
hashToken: (token, type) => storeToken(opts.storeTokens, token, type),
|
|
1346
1497
|
validateAccessToken: async (token, clientId) => {
|
|
1347
1498
|
const { validateAccessToken } = await Promise.resolve().then(() => introspect_exports);
|
|
@@ -1371,6 +1522,7 @@ async function createJwtAccessToken(ctx, opts, user, client, audienceClaim, scop
|
|
|
1371
1522
|
azp: client.clientId,
|
|
1372
1523
|
scope: scopes.join(" "),
|
|
1373
1524
|
sid: overrides?.sid,
|
|
1525
|
+
reference_id: overrides?.referenceId,
|
|
1374
1526
|
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
1375
1527
|
iat,
|
|
1376
1528
|
exp,
|
|
@@ -1396,7 +1548,7 @@ async function computeOidcHash(token, signingAlg) {
|
|
|
1396
1548
|
* Creates a user id token in code_authorization with scope of 'openid'
|
|
1397
1549
|
* and hybrid/implicit (not yet implemented) flows
|
|
1398
1550
|
*/
|
|
1399
|
-
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) {
|
|
1400
1552
|
const iat = Math.floor(Date.now() / 1e3);
|
|
1401
1553
|
const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
|
|
1402
1554
|
const resolvedSub = await resolveSubjectIdentifier(user.id, client, opts);
|
|
@@ -1410,11 +1562,12 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
|
|
|
1410
1562
|
const resolvedKey = !opts.disableJwtPlugin && !jwtPluginOptions?.jwt?.sign ? await resolveSigningKey(ctx, jwtPluginOptions) : void 0;
|
|
1411
1563
|
const signingAlg = opts.disableJwtPlugin ? "HS256" : resolvedKey?.alg ?? jwtPluginOptions?.jwks?.keyPairConfig?.alg;
|
|
1412
1564
|
const atHash = accessToken ? await computeOidcHash(accessToken, signingAlg) : void 0;
|
|
1413
|
-
const emitSid = Boolean(client.enableEndSession || client.backchannelLogoutUri);
|
|
1565
|
+
const emitSid = Boolean(client.enableEndSession || client.backchannelLogoutUri || client.frontchannelLogoutUri);
|
|
1414
1566
|
const payload = {
|
|
1415
1567
|
...ID_TOKEN_SCOPE_CLAIM_GUARDS,
|
|
1416
1568
|
auth_time: authTimeSec,
|
|
1417
|
-
acr: "0",
|
|
1569
|
+
acr: authenticationContext?.acr ?? "0",
|
|
1570
|
+
amr: authenticationContext?.amr,
|
|
1418
1571
|
...customClaims,
|
|
1419
1572
|
at_hash: atHash,
|
|
1420
1573
|
iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
|
|
@@ -1446,20 +1599,7 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
|
|
|
1446
1599
|
* Encodes a refresh token for a client
|
|
1447
1600
|
*/
|
|
1448
1601
|
async function encodeRefreshToken(opts, token, sessionId) {
|
|
1449
|
-
return (opts.prefix?.refreshToken ?? "") + (opts.formatRefreshToken?.encrypt ? opts.formatRefreshToken.encrypt(token, sessionId) : token);
|
|
1450
|
-
}
|
|
1451
|
-
/**
|
|
1452
|
-
* Decodes a refresh token for a client
|
|
1453
|
-
*
|
|
1454
|
-
* @internal
|
|
1455
|
-
*/
|
|
1456
|
-
async function decodeRefreshToken(opts, token) {
|
|
1457
|
-
if (opts.prefix?.refreshToken) if (token.startsWith(opts.prefix.refreshToken)) token = token.replace(opts.prefix.refreshToken, "");
|
|
1458
|
-
else throw new APIError("BAD_REQUEST", {
|
|
1459
|
-
error_description: "refresh token not found",
|
|
1460
|
-
error: "invalid_token"
|
|
1461
|
-
});
|
|
1462
|
-
return opts.formatRefreshToken?.decrypt ? opts.formatRefreshToken?.decrypt(token) : { token };
|
|
1602
|
+
return (opts.prefix?.refreshToken ?? "") + (opts.formatRefreshToken?.encrypt ? await opts.formatRefreshToken.encrypt(token, sessionId) : token);
|
|
1463
1603
|
}
|
|
1464
1604
|
async function createOpaqueAccessToken(ctx, opts, user, client, scopes, payload, resources, referenceId, authorizationCodeId, refreshId, confirmation, requestedUserInfoClaims) {
|
|
1465
1605
|
const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
|
|
@@ -1549,7 +1689,7 @@ async function revokeTokensIssuedForAuthorizationCode(ctx, authorizationCodeId)
|
|
|
1549
1689
|
await deleteIssuedTokens("oauthAccessToken");
|
|
1550
1690
|
await deleteIssuedTokens("oauthRefreshToken");
|
|
1551
1691
|
}
|
|
1552
|
-
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) {
|
|
1553
1693
|
const iat = payload.iat ?? Math.floor(Date.now() / 1e3);
|
|
1554
1694
|
const exp = payload?.exp ?? iat + (opts.refreshTokenExpiresIn ?? 2592e3);
|
|
1555
1695
|
const token = opts.generateRefreshToken ? await opts.generateRefreshToken() : generateRandomString(32, "A-Z", "a-z");
|
|
@@ -1562,6 +1702,8 @@ async function createRefreshToken(ctx, opts, user, referenceId, authorizationCod
|
|
|
1562
1702
|
referenceId,
|
|
1563
1703
|
authorizationCodeId,
|
|
1564
1704
|
authTime,
|
|
1705
|
+
acr: authenticationContext?.acr,
|
|
1706
|
+
amr: authenticationContext?.amr,
|
|
1565
1707
|
confirmation,
|
|
1566
1708
|
requestedUserInfoClaims: requestedUserInfoClaims?.length ? requestedUserInfoClaims : void 0,
|
|
1567
1709
|
scopes,
|
|
@@ -1784,9 +1926,18 @@ function replayRefreshTokenRotationResponse(ctx, response) {
|
|
|
1784
1926
|
});
|
|
1785
1927
|
}
|
|
1786
1928
|
async function createUserTokens(ctx, opts, params) {
|
|
1787
|
-
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
|
+
};
|
|
1788
1934
|
const iat = Math.floor(Date.now() / 1e3);
|
|
1789
|
-
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
|
+
});
|
|
1790
1941
|
const scopeExp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
|
|
1791
1942
|
return prev < curr ? prev : curr;
|
|
1792
1943
|
}, defaultExp) : defaultExp;
|
|
@@ -1841,7 +1992,7 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
1841
1992
|
iat,
|
|
1842
1993
|
exp: refreshTokenExp,
|
|
1843
1994
|
sid: sessionId
|
|
1844
|
-
}, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0;
|
|
1995
|
+
}, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims, authenticationContext) : void 0;
|
|
1845
1996
|
const accessTokenClaims = isJwtAccessToken ? await resolveAccessTokenClaims({
|
|
1846
1997
|
ctx,
|
|
1847
1998
|
opts,
|
|
@@ -1860,6 +2011,7 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
1860
2011
|
iat,
|
|
1861
2012
|
exp,
|
|
1862
2013
|
sid: sessionId,
|
|
2014
|
+
referenceId,
|
|
1863
2015
|
signingAlgorithm: grantIssuance.signingAlgorithm,
|
|
1864
2016
|
signingKeyId: grantIssuance.signingKeyId,
|
|
1865
2017
|
accessTokenClaims,
|
|
@@ -1872,8 +2024,8 @@ async function createUserTokens(ctx, opts, params) {
|
|
|
1872
2024
|
iat,
|
|
1873
2025
|
exp: refreshTokenExp,
|
|
1874
2026
|
sid: sessionId
|
|
1875
|
-
}, existingRefreshToken, authTime, refreshResources, confirmation, requestedUserInfoClaims) : void 0]);
|
|
1876
|
-
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;
|
|
1877
2029
|
const responseBody = {
|
|
1878
2030
|
...customFields,
|
|
1879
2031
|
...params.tokenResponse ?? {},
|
|
@@ -2051,6 +2203,8 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
|
|
|
2051
2203
|
sessionId: session.id,
|
|
2052
2204
|
nonce: verificationValue.query?.nonce,
|
|
2053
2205
|
authTime,
|
|
2206
|
+
acr: verificationValue.acr,
|
|
2207
|
+
amr: verificationValue.amr,
|
|
2054
2208
|
verificationValue,
|
|
2055
2209
|
authorizationCodeId,
|
|
2056
2210
|
requestedUserInfoClaims,
|
|
@@ -2123,14 +2277,7 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
2123
2277
|
* `invalid_client`, so resolve it before the refresh token lookup can
|
|
2124
2278
|
* report `invalid_grant`). */
|
|
2125
2279
|
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, "refresh_token", authMethod);
|
|
2126
|
-
const
|
|
2127
|
-
const refreshToken = await ctx.context.adapter.findOne({
|
|
2128
|
-
model: "oauthRefreshToken",
|
|
2129
|
-
where: [{
|
|
2130
|
-
field: "token",
|
|
2131
|
-
value: await getStoredToken(opts.storeTokens, decodedRefresh.token, "refresh_token")
|
|
2132
|
-
}]
|
|
2133
|
-
});
|
|
2280
|
+
const refreshToken = await findRefreshToken(ctx, opts, refresh_token);
|
|
2134
2281
|
if (!refreshToken) throw new APIError("BAD_REQUEST", {
|
|
2135
2282
|
error_description: "session not found",
|
|
2136
2283
|
error: "invalid_grant"
|
|
@@ -2194,6 +2341,8 @@ async function handleRefreshTokenGrant(ctx, opts) {
|
|
|
2194
2341
|
refreshToken,
|
|
2195
2342
|
resources: resources ?? refreshToken.resources,
|
|
2196
2343
|
authTime,
|
|
2344
|
+
acr: refreshToken.acr ?? void 0,
|
|
2345
|
+
amr: refreshToken.amr ?? void 0,
|
|
2197
2346
|
requestedUserInfoClaims: refreshToken.requestedUserInfoClaims
|
|
2198
2347
|
});
|
|
2199
2348
|
}
|
|
@@ -2214,11 +2363,11 @@ function inactiveAccessToken() {
|
|
|
2214
2363
|
};
|
|
2215
2364
|
}
|
|
2216
2365
|
/**
|
|
2217
|
-
*
|
|
2218
|
-
* Introspection follows RFC7662
|
|
2366
|
+
* Introspection follows RFC 7662
|
|
2219
2367
|
* https://datatracker.ietf.org/doc/html/rfc7662
|
|
2220
|
-
*
|
|
2221
|
-
*
|
|
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.
|
|
2222
2371
|
*/
|
|
2223
2372
|
/**
|
|
2224
2373
|
* Resource identifiers in a token's audience, excluding the implicit
|
|
@@ -2244,38 +2393,14 @@ async function isIntrospectionAuthorized(ctx, opts, params) {
|
|
|
2244
2393
|
return isClientLinkedToAnyResource(ctx, opts, introspectingClientId, audienceResources);
|
|
2245
2394
|
}
|
|
2246
2395
|
/**
|
|
2247
|
-
* Validates a JWT access token
|
|
2396
|
+
* Validates a JWT access token whose signature and issuer are verified.
|
|
2248
2397
|
*
|
|
2249
2398
|
* @returns RFC7662 introspection format
|
|
2250
2399
|
*/
|
|
2251
|
-
async function validateJwtAccessToken(ctx, opts,
|
|
2252
|
-
|
|
2253
|
-
error_description: "JWT access tokens are disabled",
|
|
2254
|
-
error: "invalid_request"
|
|
2255
|
-
});
|
|
2256
|
-
const jwtPlugin = getJwtPlugin(ctx.context);
|
|
2257
|
-
const jwtPluginOptions = jwtPlugin.options;
|
|
2258
|
-
const userInfoAud = `${ctx.context.baseURL ?? ""}/oauth2/userinfo`;
|
|
2259
|
-
const expectedIssuer = jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL;
|
|
2260
|
-
let jwtPayload;
|
|
2261
|
-
try {
|
|
2262
|
-
jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
|
|
2263
|
-
jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
|
|
2264
|
-
return (await jwtPlugin.endpoints.getJwks(ctx))?.response;
|
|
2265
|
-
},
|
|
2266
|
-
jwksCacheKey: jwtPlugin
|
|
2267
|
-
})), { issuer: expectedIssuer })).payload;
|
|
2268
|
-
} catch (error) {
|
|
2269
|
-
if (error instanceof errors.JWTExpired || error instanceof errors.JWTInvalid) return { active: false };
|
|
2270
|
-
if (error instanceof TypeError || isInvalidJwtError(error)) throw new APIError$1("BAD_REQUEST", {
|
|
2271
|
-
error_description: "invalid JWT signature",
|
|
2272
|
-
error: "invalid_request"
|
|
2273
|
-
});
|
|
2274
|
-
throw error;
|
|
2275
|
-
}
|
|
2400
|
+
async function validateJwtAccessToken(ctx, opts, jwtPayload, clientId) {
|
|
2401
|
+
const userInfoAud = userInfoResource(ctx.context.baseURL ?? "");
|
|
2276
2402
|
const rawAud = jwtPayload.aud;
|
|
2277
2403
|
if (!await isAudienceClaimAllowed(ctx, opts, rawAud, [userInfoAud])) return { active: false };
|
|
2278
|
-
if (!jwtPayload.azp) return { active: false };
|
|
2279
2404
|
const client = await getClient(ctx, opts, jwtPayload.azp);
|
|
2280
2405
|
if (!client || client?.disabled) return { active: false };
|
|
2281
2406
|
if (!await isIntrospectionAuthorized(ctx, opts, {
|
|
@@ -2300,28 +2425,11 @@ async function validateJwtAccessToken(ctx, opts, token, clientId) {
|
|
|
2300
2425
|
return jwtPayload;
|
|
2301
2426
|
}
|
|
2302
2427
|
/**
|
|
2303
|
-
*
|
|
2428
|
+
* Validates an opaque access token record.
|
|
2304
2429
|
*
|
|
2305
2430
|
* @returns RFC7662 introspection format
|
|
2306
2431
|
*/
|
|
2307
|
-
async function validateOpaqueAccessToken(ctx, opts,
|
|
2308
|
-
let tokenValue = token;
|
|
2309
|
-
if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
|
|
2310
|
-
else throw new APIError$1("BAD_REQUEST", {
|
|
2311
|
-
error_description: "opaque access token not found",
|
|
2312
|
-
error: "invalid_request"
|
|
2313
|
-
});
|
|
2314
|
-
const accessToken = await ctx.context.adapter.findOne({
|
|
2315
|
-
model: "oauthAccessToken",
|
|
2316
|
-
where: [{
|
|
2317
|
-
field: "token",
|
|
2318
|
-
value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
|
|
2319
|
-
}]
|
|
2320
|
-
});
|
|
2321
|
-
if (!accessToken) throw new APIError$1("BAD_REQUEST", {
|
|
2322
|
-
error_description: "opaque access token not found",
|
|
2323
|
-
error: "invalid_token"
|
|
2324
|
-
});
|
|
2432
|
+
async function validateOpaqueAccessToken(ctx, opts, accessToken, clientId) {
|
|
2325
2433
|
if (!accessToken.expiresAt || accessToken.expiresAt < /* @__PURE__ */ new Date()) return inactiveAccessToken();
|
|
2326
2434
|
if (accessToken.revoked) return inactiveAccessToken();
|
|
2327
2435
|
const resources = Array.isArray(accessToken.resources) ? accessToken.resources : void 0;
|
|
@@ -2380,6 +2488,7 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
2380
2488
|
azp: accessToken.clientId,
|
|
2381
2489
|
sub: user?.id,
|
|
2382
2490
|
sid: sessionId,
|
|
2491
|
+
reference_id: accessToken.referenceId ?? void 0,
|
|
2383
2492
|
exp: Math.floor(new Date(accessToken.expiresAt).getTime() / 1e3),
|
|
2384
2493
|
iat: Math.floor(new Date(accessToken.createdAt).getTime() / 1e3),
|
|
2385
2494
|
scope: accessToken.scopes?.join(" "),
|
|
@@ -2390,22 +2499,11 @@ async function validateOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
|
2390
2499
|
};
|
|
2391
2500
|
}
|
|
2392
2501
|
/**
|
|
2393
|
-
* Validates a refresh token
|
|
2502
|
+
* Validates a refresh token record.
|
|
2394
2503
|
*
|
|
2395
2504
|
* @returns payload in RFC7662 introspection format
|
|
2396
2505
|
*/
|
|
2397
|
-
async function validateRefreshToken(ctx, opts,
|
|
2398
|
-
const refreshToken = await ctx.context.adapter.findOne({
|
|
2399
|
-
model: "oauthRefreshToken",
|
|
2400
|
-
where: [{
|
|
2401
|
-
field: "token",
|
|
2402
|
-
value: await getStoredToken(opts.storeTokens, token, "refresh_token")
|
|
2403
|
-
}]
|
|
2404
|
-
});
|
|
2405
|
-
if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
|
|
2406
|
-
error_description: "token not found",
|
|
2407
|
-
error: "invalid_token"
|
|
2408
|
-
});
|
|
2506
|
+
async function validateRefreshToken(ctx, opts, refreshToken, clientId) {
|
|
2409
2507
|
if (!refreshToken.clientId || refreshToken.clientId !== clientId) return { active: false };
|
|
2410
2508
|
if (!refreshToken.expiresAt || refreshToken.expiresAt < /* @__PURE__ */ new Date()) return { active: false };
|
|
2411
2509
|
if (refreshToken.revoked) return { active: false };
|
|
@@ -2428,6 +2526,7 @@ async function validateRefreshToken(ctx, opts, token, clientId) {
|
|
|
2428
2526
|
iss: ((opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context))?.options)?.jwt?.issuer ?? ctx.context.baseURL,
|
|
2429
2527
|
sub: user?.id,
|
|
2430
2528
|
sid: sessionId,
|
|
2529
|
+
reference_id: refreshToken.referenceId ?? void 0,
|
|
2431
2530
|
exp: Math.floor(new Date(refreshToken.expiresAt).getTime() / 1e3),
|
|
2432
2531
|
iat: Math.floor(new Date(refreshToken.createdAt).getTime() / 1e3),
|
|
2433
2532
|
scope: refreshToken.scopes?.join(" "),
|
|
@@ -2441,32 +2540,23 @@ function createInvalidAccessTokenError() {
|
|
|
2441
2540
|
error: "invalid_token"
|
|
2442
2541
|
}, { "WWW-Authenticate": INVALID_ACCESS_TOKEN_WWW_AUTHENTICATE });
|
|
2443
2542
|
}
|
|
2444
|
-
function
|
|
2445
|
-
|
|
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
|
+
};
|
|
2446
2549
|
}
|
|
2447
2550
|
/**
|
|
2448
|
-
*
|
|
2449
|
-
*
|
|
2450
|
-
*
|
|
2451
|
-
*
|
|
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).
|
|
2452
2555
|
*/
|
|
2453
2556
|
async function resolveAccessTokenValidation(ctx, opts, token, clientId) {
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
requestedUserInfoClaims: []
|
|
2458
|
-
};
|
|
2459
|
-
} catch (err) {
|
|
2460
|
-
if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
|
|
2461
|
-
else throw new Error(err);
|
|
2462
|
-
}
|
|
2463
|
-
try {
|
|
2464
|
-
return await validateOpaqueAccessToken(ctx, opts, token, clientId);
|
|
2465
|
-
} catch (err) {
|
|
2466
|
-
if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
|
|
2467
|
-
else throw new Error("Unknown error validating access token");
|
|
2468
|
-
}
|
|
2469
|
-
throw createInvalidAccessTokenError();
|
|
2557
|
+
const found = await findAccessToken(ctx, opts, token);
|
|
2558
|
+
if (!found) return inactiveAccessToken();
|
|
2559
|
+
return validateFoundAccessToken(ctx, opts, found, clientId);
|
|
2470
2560
|
}
|
|
2471
2561
|
/**
|
|
2472
2562
|
* Validates an access token (JWT or opaque) and returns its RFC 7662
|
|
@@ -2515,7 +2605,6 @@ async function resolveIntrospectionSub(ctx, opts, payload, introspectingClient)
|
|
|
2515
2605
|
}
|
|
2516
2606
|
async function introspectEndpoint(ctx, opts) {
|
|
2517
2607
|
let { token, token_type_hint } = ctx.body;
|
|
2518
|
-
if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
|
|
2519
2608
|
const { clientId: client_id, clientSecret: client_secret, preVerified, authMethod } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
|
|
2520
2609
|
if (!client_id || !client_secret && !preVerified) throw new APIError$1("UNAUTHORIZED", {
|
|
2521
2610
|
error_description: "missing required credentials",
|
|
@@ -2528,38 +2617,16 @@ async function introspectEndpoint(ctx, opts) {
|
|
|
2528
2617
|
});
|
|
2529
2618
|
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, void 0, authMethod);
|
|
2530
2619
|
try {
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
} else if (error instanceof Error) throw error;
|
|
2537
|
-
else throw new Error(error);
|
|
2538
|
-
}
|
|
2539
|
-
if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
|
|
2540
|
-
return resolveIntrospectionSub(ctx, opts, await validateRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId), client);
|
|
2541
|
-
} catch (error) {
|
|
2542
|
-
if (error instanceof APIError$1) {
|
|
2543
|
-
if (token_type_hint === "refresh_token") throw error;
|
|
2544
|
-
} else if (error instanceof Error) throw error;
|
|
2545
|
-
else throw new Error(error);
|
|
2546
|
-
}
|
|
2547
|
-
throw new APIError$1("BAD_REQUEST", {
|
|
2548
|
-
error_description: "token not found",
|
|
2549
|
-
error: "invalid_request"
|
|
2550
|
-
});
|
|
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);
|
|
2551
2625
|
} catch (error) {
|
|
2552
|
-
if (error instanceof APIError$1)
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
} else if (error instanceof Error) {
|
|
2556
|
-
logger.error("Introspection error:", error.message, error.stack);
|
|
2557
|
-
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
2558
|
-
} else {
|
|
2559
|
-
logger.error("Introspection error:", error);
|
|
2560
|
-
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
2561
|
-
}
|
|
2626
|
+
if (error instanceof APIError$1) throw error;
|
|
2627
|
+
logger.error("Introspection error:", error);
|
|
2628
|
+
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
2562
2629
|
}
|
|
2563
2630
|
}
|
|
2564
2631
|
//#endregion
|
|
2565
|
-
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 };
|