@m1212e/oidc 0.0.0-stage → 0.1.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/LICENSE +201 -0
- package/README.md +164 -2
- package/out/index.cjs +1017 -0
- package/out/index.cjs.map +1 -0
- package/out/index.d.cts +298 -0
- package/out/index.d.cts.map +1 -0
- package/out/index.d.mts +298 -0
- package/out/index.d.mts.map +1 -0
- package/out/index.mjs +975 -0
- package/out/index.mjs.map +1 -0
- package/package.json +47 -4
package/out/index.cjs
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let jose = require("jose");
|
|
3
|
+
let openid_client = require("openid-client");
|
|
4
|
+
let typebox = require("typebox");
|
|
5
|
+
let typebox_compile = require("typebox/compile");
|
|
6
|
+
let typebox_value = require("typebox/value");
|
|
7
|
+
// lib/errors.ts
|
|
8
|
+
var OidcError = class extends Error {
|
|
9
|
+
refreshedTokens;
|
|
10
|
+
constructor(message, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = "OidcError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var OidcInputError = class extends OidcError {
|
|
16
|
+
constructor(message, options) {
|
|
17
|
+
super(message, options);
|
|
18
|
+
this.name = "OidcInputError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var OidcTokenError = class extends OidcError {
|
|
22
|
+
tokenKind;
|
|
23
|
+
claim;
|
|
24
|
+
constructor(tokenKind, claim, message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "OidcTokenError";
|
|
27
|
+
this.tokenKind = tokenKind;
|
|
28
|
+
this.claim = claim;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function describe(error) {
|
|
32
|
+
return error instanceof Error ? error.message : String(error);
|
|
33
|
+
}
|
|
34
|
+
var OidcTokenValidationError = class extends OidcError {
|
|
35
|
+
tokenKinds;
|
|
36
|
+
failures;
|
|
37
|
+
constructor(tokenKinds, failures) {
|
|
38
|
+
const relevant = failures.filter((failure) => tokenKinds.includes(failure.tokenKind));
|
|
39
|
+
const detail = relevant.length ? relevant.map((failure) => `${failure.tokenKind} token ${failure.stage} failed (${describe(failure.error)})`).join("; ") : "no validation strategy was available for it";
|
|
40
|
+
super(`could not validate the ${tokenKinds.join(" and ")} token${tokenKinds.length > 1 ? "s" : ""}: ${detail}`, { cause: relevant[0]?.error });
|
|
41
|
+
this.name = "OidcTokenValidationError";
|
|
42
|
+
this.tokenKinds = tokenKinds;
|
|
43
|
+
this.failures = relevant;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var OidcSubjectError = class extends OidcError {
|
|
47
|
+
expected;
|
|
48
|
+
actual;
|
|
49
|
+
constructor(message, expected, actual) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "OidcSubjectError";
|
|
52
|
+
this.expected = expected;
|
|
53
|
+
this.actual = actual;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var OidcGrantError = class extends OidcError {
|
|
57
|
+
grant;
|
|
58
|
+
constructor(grant, message, options) {
|
|
59
|
+
super(message, options);
|
|
60
|
+
this.name = "OidcGrantError";
|
|
61
|
+
this.grant = grant;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var OidcStateError = class extends OidcError {
|
|
65
|
+
constructor(message, options) {
|
|
66
|
+
super(message, options);
|
|
67
|
+
this.name = "OidcStateError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var OidcClaimsError = class extends OidcError {
|
|
71
|
+
constructor(message, options) {
|
|
72
|
+
super(message, options);
|
|
73
|
+
this.name = "OidcClaimsError";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var OidcUserInfoError = class extends OidcError {
|
|
77
|
+
constructor(message, options) {
|
|
78
|
+
super(message, options);
|
|
79
|
+
this.name = "OidcUserInfoError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var OidcLogoutError = class extends OidcError {
|
|
83
|
+
constructor(message, options) {
|
|
84
|
+
super(message, options);
|
|
85
|
+
this.name = "OidcLogoutError";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
function attachRefreshedTokens(error, refreshedTokens) {
|
|
89
|
+
if (refreshedTokens && error instanceof OidcError && !(error instanceof OidcSubjectError)) error.refreshedTokens = refreshedTokens;
|
|
90
|
+
return error;
|
|
91
|
+
}
|
|
92
|
+
// lib/audience.ts
|
|
93
|
+
const DEFAULT_AUTHORIZED_PARTY_CLAIMS = [
|
|
94
|
+
"azp",
|
|
95
|
+
"client_id",
|
|
96
|
+
"cid",
|
|
97
|
+
"appid"
|
|
98
|
+
];
|
|
99
|
+
var OidcAudienceError = class extends OidcTokenError {
|
|
100
|
+
constructor(tokenKind, claim, message) {
|
|
101
|
+
super(tokenKind, claim, message);
|
|
102
|
+
this.name = "OidcAudienceError";
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
function toAudienceList(aud) {
|
|
106
|
+
if (typeof aud === "string") return [aud];
|
|
107
|
+
if (Array.isArray(aud)) return aud.filter((entry) => typeof entry === "string");
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
function findAuthorizedParty(claims, claimNames) {
|
|
111
|
+
for (const claim of claimNames) {
|
|
112
|
+
const value = claims[claim];
|
|
113
|
+
if (value === void 0 || value === null) continue;
|
|
114
|
+
return {
|
|
115
|
+
claim,
|
|
116
|
+
value
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function audienceVerifyOption(policy, tokenKind) {
|
|
121
|
+
if (policy.verifyAudience) return void 0;
|
|
122
|
+
return tokenKind === "id" ? policy.clientId : policy.expectedAudience;
|
|
123
|
+
}
|
|
124
|
+
function assertAudienceClaim(claims, tokenKind, policy) {
|
|
125
|
+
const expected = tokenKind === "id" ? [policy.clientId] : policy.expectedAudience;
|
|
126
|
+
if (!expected?.length) return;
|
|
127
|
+
const aud = toAudienceList(claims.aud);
|
|
128
|
+
if (!aud.some((entry) => expected.includes(entry))) throw new OidcAudienceError(tokenKind, "aud", `${tokenKind} token "aud" (${aud.length ? aud.join(", ") : "absent"}) does not include any expected audience (${expected.join(", ")})`);
|
|
129
|
+
}
|
|
130
|
+
function assertAuthorizedParty(claims, tokenKind, policy) {
|
|
131
|
+
const { clientId, expectedAudience, authorizedPartyClaims } = policy;
|
|
132
|
+
if (tokenKind === "id") {
|
|
133
|
+
const azp = claims.azp;
|
|
134
|
+
if (azp !== void 0 && azp !== null) {
|
|
135
|
+
if (azp !== clientId) throw new OidcAudienceError("id", "azp", `id token "azp" (${String(azp)}) is not this client id (${clientId})`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const aud = toAudienceList(claims.aud);
|
|
139
|
+
if (aud.length > 1) throw new OidcAudienceError("id", "azp", `id token "aud" names multiple parties (${aud.join(", ")}) but carries no "azp" claim identifying the authorized one`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (expectedAudience?.length) {
|
|
143
|
+
assertAudienceClaim(claims, "access", policy);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const party = findAuthorizedParty(claims, authorizedPartyClaims);
|
|
147
|
+
if (party) {
|
|
148
|
+
if (typeof party.value !== "string" || party.value !== clientId) throw new OidcAudienceError("access", party.claim, `access token "${party.claim}" (${typeof party.value === "string" ? party.value : JSON.stringify(party.value)}) is not this client id (${clientId})`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
throw new OidcAudienceError("access", "aud", `access token cannot be bound to this client: it carries none of the authorized-party claims (${authorizedPartyClaims.join(", ")}) and no "expectedAudience" is configured. Set "expectedAudience" to the value this resource server expects in "aud", extend "authorizedPartyClaims" with the claim your provider uses, or supply "verifyAudience".`);
|
|
152
|
+
}
|
|
153
|
+
function assertAudienceAfterJwtVerify(claims, tokenKind, policy) {
|
|
154
|
+
if (policy.verifyAudience) {
|
|
155
|
+
if (!policy.verifyAudience(claims, tokenKind)) throw new OidcAudienceError(tokenKind, "aud", `verifyAudience rejected the ${tokenKind} token`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
assertAuthorizedParty(claims, tokenKind, policy);
|
|
159
|
+
}
|
|
160
|
+
function assertTokenAudience(claims, tokenKind, policy) {
|
|
161
|
+
if (!policy.verifyAudience) assertAudienceClaim(claims, tokenKind, policy);
|
|
162
|
+
assertAudienceAfterJwtVerify(claims, tokenKind, policy);
|
|
163
|
+
}
|
|
164
|
+
function assertIntrospectedIdTokenAudience(claims, policy) {
|
|
165
|
+
if (policy.verifyAudience) {
|
|
166
|
+
if (!policy.verifyAudience(claims, "id")) throw new OidcAudienceError("id", "aud", "verifyAudience rejected the id token");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const aud = toAudienceList(claims.aud);
|
|
170
|
+
if (aud.length && !aud.includes(policy.clientId)) throw new OidcAudienceError("id", "aud", `id token introspection response reports "aud" (${aud.join(", ")}), which does not include this client id (${policy.clientId})`);
|
|
171
|
+
const claimNames = policy.authorizedPartyClaims.includes("azp") ? policy.authorizedPartyClaims : ["azp", ...policy.authorizedPartyClaims];
|
|
172
|
+
const party = findAuthorizedParty(claims, claimNames);
|
|
173
|
+
if (party) {
|
|
174
|
+
if (typeof party.value !== "string" || party.value !== policy.clientId) throw new OidcAudienceError("id", party.claim, `id token introspection response reports "${party.claim}" (${typeof party.value === "string" ? party.value : JSON.stringify(party.value)}), which is not this client id (${policy.clientId})`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (aud.length > 1) throw new OidcAudienceError("id", "azp", `id token introspection response reports an "aud" naming multiple parties (${aud.join(", ")}) but nothing identifying the authorized one`);
|
|
178
|
+
if (aud.length) return;
|
|
179
|
+
throw new OidcAudienceError("id", "aud", `id token introspection response cannot be bound to this client: it reports no "aud" and carries none of the authorized-party claims (${claimNames.join(", ")}). Extend "authorizedPartyClaims" with the claim your provider reports, or supply "verifyAudience".`);
|
|
180
|
+
}
|
|
181
|
+
function buildAudiencePolicy(opts) {
|
|
182
|
+
const supplied = typeof opts.expectedAudience === "string" ? [opts.expectedAudience] : opts.expectedAudience ?? [];
|
|
183
|
+
const invalid = supplied.filter((entry) => typeof entry !== "string" || entry.length === 0);
|
|
184
|
+
if (invalid.length) throw new TypeError(`invalid expectedAudience: every entry must be a non-empty string, and ${invalid.length} of ${supplied.length} ${invalid.length === 1 ? "is" : "are"} not. An empty audience matches nothing, so it cannot be dropped without silently replacing the resource-audience binding with the authorized-party one. Omit "expectedAudience" entirely to use that binding on purpose.`);
|
|
185
|
+
const expected = supplied;
|
|
186
|
+
return {
|
|
187
|
+
clientId: opts.clientId,
|
|
188
|
+
expectedAudience: expected.length ? expected : void 0,
|
|
189
|
+
authorizedPartyClaims: opts.authorizedPartyClaims ?? DEFAULT_AUTHORIZED_PARTY_CLAIMS,
|
|
190
|
+
verifyAudience: opts.verifyAudience
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// lib/discovery.ts
|
|
194
|
+
function resolveDevelopment(development) {
|
|
195
|
+
if (development !== void 0) return development;
|
|
196
|
+
if (typeof process === "undefined") return false;
|
|
197
|
+
return process.env?.NODE_ENV === "development";
|
|
198
|
+
}
|
|
199
|
+
const DEFAULT_SIGNING_ALGORITHMS = [
|
|
200
|
+
"RS256",
|
|
201
|
+
"RS384",
|
|
202
|
+
"RS512",
|
|
203
|
+
"PS256",
|
|
204
|
+
"PS384",
|
|
205
|
+
"PS512",
|
|
206
|
+
"ES256",
|
|
207
|
+
"ES256K",
|
|
208
|
+
"ES384",
|
|
209
|
+
"ES512",
|
|
210
|
+
"EdDSA",
|
|
211
|
+
"Ed25519"
|
|
212
|
+
];
|
|
213
|
+
function isAsymmetricAlgorithm(alg) {
|
|
214
|
+
return /^(RS|PS|ES|Ed)/.test(alg);
|
|
215
|
+
}
|
|
216
|
+
function resolveJwksUri(jwksUri, development) {
|
|
217
|
+
let url;
|
|
218
|
+
try {
|
|
219
|
+
url = new URL(jwksUri);
|
|
220
|
+
} catch {
|
|
221
|
+
throw new TypeError(`the issuer advertises a jwks_uri that is not a URL ("${jwksUri}")`);
|
|
222
|
+
}
|
|
223
|
+
if (url.protocol === "https:") return url;
|
|
224
|
+
if (development && url.protocol === "http:") return url;
|
|
225
|
+
throw new TypeError(`refusing the issuer's jwks_uri ("${jwksUri}"): signing keys must be fetched over https, and this is ${url.protocol.replace(":", "")}. Every token signature is checked against those keys, so anyone on the network could substitute their own and mint tokens for any user. Set "development: true" only if this is a local IdP.`);
|
|
226
|
+
}
|
|
227
|
+
function issuerMatchExempt(url) {
|
|
228
|
+
return url.origin === "https://login.microsoftonline.com" || url.hostname.endsWith(".b2clogin.com");
|
|
229
|
+
}
|
|
230
|
+
const WELL_KNOWN = "/.well-known/";
|
|
231
|
+
const withoutTrailingSlash = (value) => value.replace(/\/+$/, "");
|
|
232
|
+
function assertDiscoveredIssuer(configured, discovered) {
|
|
233
|
+
let configuredUrl;
|
|
234
|
+
try {
|
|
235
|
+
configuredUrl = new URL(configured);
|
|
236
|
+
} catch {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const path = configuredUrl.origin + configuredUrl.pathname;
|
|
240
|
+
const wellKnownAt = path.indexOf(WELL_KNOWN);
|
|
241
|
+
if (wellKnownAt === -1) return;
|
|
242
|
+
if (!withoutTrailingSlash(path.slice(wellKnownAt + 13)).includes("/") && !issuerMatchExempt(configuredUrl)) {
|
|
243
|
+
const expected = withoutTrailingSlash(path.slice(0, wellKnownAt));
|
|
244
|
+
if (withoutTrailingSlash(discovered) === expected) return;
|
|
245
|
+
throw new TypeError(`the metadata document at "${configured}" names a different issuer ("${discovered}") than the "${expected}" it was fetched for. Every token is verified against the issuer the document declares, and its keys and endpoints come from the same document, so this is not a mismatch that can be accepted. Configure "issuer" as the issuer identifier itself rather than its .well-known URL, or point it at the provider that really serves "${discovered}".`);
|
|
246
|
+
}
|
|
247
|
+
let discoveredUrl;
|
|
248
|
+
try {
|
|
249
|
+
discoveredUrl = new URL(discovered);
|
|
250
|
+
} catch {
|
|
251
|
+
throw new TypeError(`the metadata document at "${configured}" names an issuer that is not a URL ("${discovered}"), so it cannot be checked against the provider it was fetched from. Every token is verified against the issuer the document declares, and its keys and endpoints come from the same document, so an issuer that cannot be compared cannot be accepted. Configure "issuer" as the issuer identifier itself rather than its .well-known URL.`);
|
|
252
|
+
}
|
|
253
|
+
if (discoveredUrl.origin === configuredUrl.origin) return;
|
|
254
|
+
throw new TypeError(`the metadata document at "${configured}" names an issuer on a different origin ("${discovered}"). Every token is verified against the issuer the document declares, and its keys and endpoints come from the same document, so a document that hands off to another host cannot be accepted. Configure "issuer" as the issuer identifier itself rather than its .well-known URL.`);
|
|
255
|
+
}
|
|
256
|
+
function assertPkceSupported(methods, clientSecret, log) {
|
|
257
|
+
if (methods?.includes("S256")) return;
|
|
258
|
+
if (methods?.length) throw new TypeError(`the issuer does not support the S256 PKCE challenge method (it advertises: ${methods.join(", ")}). This library sends S256 on every authorization request, and without it an authorization code isn't bound to the login that requested it. An issuer that ignores it would leave the code redeemable by anyone who obtained it.`);
|
|
259
|
+
log.warn(`[oidc] the issuer advertises no "code_challenge_methods_supported", so it cannot be confirmed that the PKCE challenge this library sends is honoured${clientSecret ? "" : ". This is a public client, where PKCE is the only thing protecting the token endpoint"}. Verify that PKCE is enforced for this client at the IdP.`);
|
|
260
|
+
}
|
|
261
|
+
const DURATION_MULTIPLIERS = {
|
|
262
|
+
s: 1,
|
|
263
|
+
m: 60,
|
|
264
|
+
h: 3600,
|
|
265
|
+
d: 86400,
|
|
266
|
+
w: 604800,
|
|
267
|
+
y: 31557600
|
|
268
|
+
};
|
|
269
|
+
const DURATION = /^(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)$/i;
|
|
270
|
+
function resolveClockTolerance(clockTolerance) {
|
|
271
|
+
if (clockTolerance === void 0) return 30;
|
|
272
|
+
if (typeof clockTolerance === "number") {
|
|
273
|
+
if (!Number.isFinite(clockTolerance) || clockTolerance < 0) throw new TypeError(`invalid clockTolerance (${clockTolerance}): expected a non-negative number of seconds`);
|
|
274
|
+
return clockTolerance;
|
|
275
|
+
}
|
|
276
|
+
const matched = DURATION.exec(clockTolerance.trim());
|
|
277
|
+
if (!matched) throw new TypeError(`invalid clockTolerance ("${clockTolerance}"): expected seconds as a number, or a duration such as "30 seconds" or "5m"`);
|
|
278
|
+
const unit = matched[2][0]?.toLowerCase();
|
|
279
|
+
return Math.round(Number.parseFloat(matched[1]) * DURATION_MULTIPLIERS[unit]);
|
|
280
|
+
}
|
|
281
|
+
function resolveSigningAlgorithms(config, signingAlgorithms, log) {
|
|
282
|
+
const advertised = config.serverMetadata().id_token_signing_alg_values_supported ?? [];
|
|
283
|
+
const advertisedAlgorithms = advertised.filter(isAsymmetricAlgorithm);
|
|
284
|
+
if (advertised.length !== advertisedAlgorithms.length) log.debug(`[oidc] ignoring advertised signing algorithms that no JWKS key can verify: ${advertised.filter((alg) => !isAsymmetricAlgorithm(alg)).join(", ")}`);
|
|
285
|
+
return signingAlgorithms ?? (advertisedAlgorithms.length ? advertisedAlgorithms : [...DEFAULT_SIGNING_ALGORITHMS]);
|
|
286
|
+
}
|
|
287
|
+
// lib/logger.ts
|
|
288
|
+
const LEVELS = {
|
|
289
|
+
silent: 0,
|
|
290
|
+
error: 1,
|
|
291
|
+
warn: 2,
|
|
292
|
+
info: 3,
|
|
293
|
+
debug: 4
|
|
294
|
+
};
|
|
295
|
+
const noop = () => {};
|
|
296
|
+
function buildLogger(logger, logLevel) {
|
|
297
|
+
if (logger) return logger;
|
|
298
|
+
const threshold = LEVELS[logLevel ?? "warn"];
|
|
299
|
+
return {
|
|
300
|
+
error: threshold >= LEVELS.error ? (...args) => console.error(...args) : noop,
|
|
301
|
+
warn: threshold >= LEVELS.warn ? (...args) => console.warn(...args) : noop,
|
|
302
|
+
info: threshold >= LEVELS.info ? (...args) => console.info(...args) : noop,
|
|
303
|
+
debug: threshold >= LEVELS.debug ? (...args) => console.debug(...args) : noop
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// lib/returnTo.ts
|
|
307
|
+
const PROBE_ORIGIN = "https://return-to.invalid";
|
|
308
|
+
const FORBIDDEN_CHARACTERS = /[\u0000-\u0020\u007f\\]/;
|
|
309
|
+
var OidcReturnToError = class extends OidcError {
|
|
310
|
+
returnTo;
|
|
311
|
+
constructor(returnTo, message) {
|
|
312
|
+
super(message);
|
|
313
|
+
this.name = "OidcReturnToError";
|
|
314
|
+
this.returnTo = returnTo;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
function buildReturnToPolicy(opts) {
|
|
318
|
+
return { allowedOrigins: (opts.allowedReturnToOrigins ?? []).map((entry) => {
|
|
319
|
+
let origin;
|
|
320
|
+
try {
|
|
321
|
+
origin = new URL(entry).origin;
|
|
322
|
+
} catch {
|
|
323
|
+
throw new TypeError(`invalid allowedReturnToOrigins entry ("${entry}"): expected an absolute URL such as "https://app.example.com"`);
|
|
324
|
+
}
|
|
325
|
+
if (origin === "null") throw new TypeError(`invalid allowedReturnToOrigins entry ("${entry}"): this URL has no origin to compare against`);
|
|
326
|
+
return origin;
|
|
327
|
+
}) };
|
|
328
|
+
}
|
|
329
|
+
function assertNoForbiddenCharacters(value) {
|
|
330
|
+
const forbidden = FORBIDDEN_CHARACTERS.exec(value);
|
|
331
|
+
if (!forbidden) return;
|
|
332
|
+
throw new OidcReturnToError(value, `invalid returnTo ("${value}"): contains ${forbidden[0] === "\\" ? "a backslash, which a URL parser reads as a slash" : `the character U+${value.codePointAt(forbidden.index)?.toString(16).padStart(4, "0").toUpperCase()}, which a browser would strip or refuse`}`);
|
|
333
|
+
}
|
|
334
|
+
function assertSafeRelativeReturnTo(returnTo) {
|
|
335
|
+
let resolved;
|
|
336
|
+
try {
|
|
337
|
+
resolved = new URL(returnTo, PROBE_ORIGIN);
|
|
338
|
+
} catch {
|
|
339
|
+
throw new OidcReturnToError(returnTo, `invalid returnTo ("${returnTo}"): not a usable path`);
|
|
340
|
+
}
|
|
341
|
+
if (resolved.origin !== PROBE_ORIGIN) throw new OidcReturnToError(returnTo, `refusing returnTo ("${returnTo}"): it looks relative but navigates to ${resolved.origin}. Only same-site paths are allowed unless the origin is listed in "allowedReturnToOrigins".`);
|
|
342
|
+
}
|
|
343
|
+
function assertSafeAbsoluteReturnTo(returnTo, policy) {
|
|
344
|
+
let absolute;
|
|
345
|
+
try {
|
|
346
|
+
absolute = new URL(returnTo);
|
|
347
|
+
} catch {
|
|
348
|
+
throw new OidcReturnToError(returnTo, `invalid returnTo ("${returnTo}"): expected a path beginning with "/", or an absolute URL on an allowed origin`);
|
|
349
|
+
}
|
|
350
|
+
if (absolute.protocol !== "https:" && absolute.protocol !== "http:") throw new OidcReturnToError(returnTo, `refusing returnTo ("${returnTo}"): only http and https URLs may be redirected to`);
|
|
351
|
+
if (!policy.allowedOrigins.includes(absolute.origin)) throw new OidcReturnToError(returnTo, policy.allowedOrigins.length ? `refusing returnTo ("${returnTo}"): ${absolute.origin} is not one of the allowed origins (${policy.allowedOrigins.join(", ")})` : `refusing returnTo ("${returnTo}"): absolute redirect targets need their origin listed in "allowedReturnToOrigins". A same-site path such as "/dashboard" needs no configuration.`);
|
|
352
|
+
if (absolute.username || absolute.password) throw new OidcReturnToError(returnTo, `refusing returnTo ("${returnTo}"): it embeds credentials, which disguise the origin it actually points at (${absolute.origin})`);
|
|
353
|
+
}
|
|
354
|
+
function assertSafeReturnTo(returnTo, policy) {
|
|
355
|
+
if (typeof returnTo !== "string" || returnTo.length === 0) throw new OidcReturnToError(String(returnTo), "invalid returnTo: expected a non-empty path such as \"/dashboard\"");
|
|
356
|
+
assertNoForbiddenCharacters(returnTo);
|
|
357
|
+
if (returnTo.startsWith("/")) {
|
|
358
|
+
assertSafeRelativeReturnTo(returnTo);
|
|
359
|
+
return returnTo;
|
|
360
|
+
}
|
|
361
|
+
assertSafeAbsoluteReturnTo(returnTo, policy);
|
|
362
|
+
return returnTo;
|
|
363
|
+
}
|
|
364
|
+
function assertSafeRedirectUri(uri, label) {
|
|
365
|
+
if (typeof uri !== "string" || uri.length === 0) throw new OidcReturnToError(String(uri), `invalid ${label}: expected an absolute URL such as "https://app.example.com/callback"`);
|
|
366
|
+
if (FORBIDDEN_CHARACTERS.test(uri)) throw new OidcReturnToError(uri, `invalid ${label} ("${uri}"): contains a control character, a space or a backslash`);
|
|
367
|
+
let parsed;
|
|
368
|
+
try {
|
|
369
|
+
parsed = new URL(uri);
|
|
370
|
+
} catch {
|
|
371
|
+
throw new OidcReturnToError(uri, `invalid ${label} ("${uri}"): expected an absolute URL such as "https://app.example.com/callback"`);
|
|
372
|
+
}
|
|
373
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new OidcReturnToError(uri, `refusing ${label} ("${uri}"): only http and https URLs may be redirected to`);
|
|
374
|
+
if (parsed.username || parsed.password) throw new OidcReturnToError(uri, `refusing ${label} ("${uri}"): it embeds credentials, which disguise the origin it actually points at (${parsed.origin})`);
|
|
375
|
+
return uri;
|
|
376
|
+
}
|
|
377
|
+
// lib/tokenType.ts
|
|
378
|
+
const DEFAULT_ID_TOKEN_ONLY_CLAIMS = [
|
|
379
|
+
"nonce",
|
|
380
|
+
"at_hash",
|
|
381
|
+
"c_hash",
|
|
382
|
+
"s_hash"
|
|
383
|
+
];
|
|
384
|
+
const ACCESS_TOKEN_TYP = "application/at+jwt";
|
|
385
|
+
const NAMED_TOKEN_KINDS = /* @__PURE__ */ new Map([
|
|
386
|
+
["access", "access"],
|
|
387
|
+
["access_token", "access"],
|
|
388
|
+
["bearer", "access"],
|
|
389
|
+
["dpop", "access"],
|
|
390
|
+
["mac", "access"],
|
|
391
|
+
["n_a", "access"],
|
|
392
|
+
["at+jwt", "access"],
|
|
393
|
+
["id", "id"],
|
|
394
|
+
["id_token", "id"],
|
|
395
|
+
["id+jwt", "id"],
|
|
396
|
+
["logout", "other"],
|
|
397
|
+
["logout+jwt", "other"],
|
|
398
|
+
["offline", "other"],
|
|
399
|
+
["refresh", "other"],
|
|
400
|
+
["refresh_token", "other"],
|
|
401
|
+
["refresh+jwt", "other"],
|
|
402
|
+
["secevent+jwt", "other"],
|
|
403
|
+
["serialized-id", "other"]
|
|
404
|
+
]);
|
|
405
|
+
function namedKind(value) {
|
|
406
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
407
|
+
const bare = value.trim().toLowerCase().replace(/^application\//, "");
|
|
408
|
+
return NAMED_TOKEN_KINDS.get(bare);
|
|
409
|
+
}
|
|
410
|
+
function normalizeTyp(typ) {
|
|
411
|
+
if (typeof typ !== "string" || typ.length === 0) return void 0;
|
|
412
|
+
const lower = typ.toLowerCase();
|
|
413
|
+
return lower.includes("/") ? lower : `application/${lower}`;
|
|
414
|
+
}
|
|
415
|
+
var OidcTokenTypeError = class extends OidcTokenError {
|
|
416
|
+
constructor(tokenKind, claim, message) {
|
|
417
|
+
super(tokenKind, claim, message);
|
|
418
|
+
this.name = "OidcTokenTypeError";
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
function buildTokenTypePolicy(opts) {
|
|
422
|
+
return {
|
|
423
|
+
idTokenOnlyClaims: opts.idTokenOnlyClaims ?? DEFAULT_ID_TOKEN_ONLY_CLAIMS,
|
|
424
|
+
accessTokenTyp: normalizeTyp(opts.accessTokenTyp),
|
|
425
|
+
requireIntrospectedTokenType: opts.requireIntrospectedTokenType ?? true
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function assertTokenTypeFromClaims(claims, tokenKind, policy) {
|
|
429
|
+
if (tokenKind === "access") for (const claim of policy.idTokenOnlyClaims) {
|
|
430
|
+
if (claims[claim] === void 0) continue;
|
|
431
|
+
throw new OidcTokenTypeError("access", claim, `access token carries "${claim}", a claim defined for ID tokens only. An ID token must never be accepted as an access token.`);
|
|
432
|
+
}
|
|
433
|
+
for (const claim of [
|
|
434
|
+
"token_use",
|
|
435
|
+
"typ",
|
|
436
|
+
"token_type"
|
|
437
|
+
]) {
|
|
438
|
+
const kind = namedKind(claims[claim]);
|
|
439
|
+
if (kind === void 0 || kind === tokenKind) continue;
|
|
440
|
+
throw new OidcTokenTypeError(tokenKind, claim, `${tokenKind} token has "${claim}" (${String(claims[claim])}), naming ${kind === "other" ? "neither an access token nor an ID token" : `an ${kind} token`}. A token must be the kind it was presented as.`);
|
|
441
|
+
}
|
|
442
|
+
if (claims.events !== void 0) throw new OidcTokenTypeError(tokenKind, "events", `${tokenKind} token carries "events", making it a back-channel logout token rather than ${tokenKind === "id" ? "an ID" : "an access"} token`);
|
|
443
|
+
}
|
|
444
|
+
function assertTokenTypeAfterJwtVerify(claims, header, tokenKind, policy) {
|
|
445
|
+
assertTokenTypeFromClaims(claims, tokenKind, policy);
|
|
446
|
+
const typ = normalizeTyp(header.typ);
|
|
447
|
+
if (tokenKind === "access" && policy.accessTokenTyp) {
|
|
448
|
+
if (typ !== policy.accessTokenTyp) throw new OidcTokenTypeError("access", "typ", `access token "typ" header (${typ ?? "absent"}) is not the required ${policy.accessTokenTyp}`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const named = namedKind(header.typ);
|
|
452
|
+
if (named !== void 0 && named !== tokenKind) throw new OidcTokenTypeError(tokenKind, "typ", `${tokenKind} token "typ" header (${typ}) names ${named === "other" ? "neither an access token nor an ID token" : `an ${named} token`}${typ === ACCESS_TOKEN_TYP ? " (the RFC 9068 access token type)" : ""}. A token must be the kind it was presented as.`);
|
|
453
|
+
}
|
|
454
|
+
function assertIntrospectedAccessToken(claims, policy) {
|
|
455
|
+
assertTokenTypeFromClaims(claims, "access", policy);
|
|
456
|
+
if (introspectedKind(claims) === "access") return;
|
|
457
|
+
if (!policy.requireIntrospectedTokenType) return;
|
|
458
|
+
throw new OidcTokenTypeError("access", "token_type", `introspection response does not name an access token: it carries no recognized "token_type", "typ" or "token_use". RFC 7662 reports a "token_type" only for tokens that have one, so a response without it is as consistent with a refresh token as with an access token, and a refresh token must never be accepted as one. Set "requireIntrospectedTokenType: false" only if your provider omits the type on genuine access tokens.`);
|
|
459
|
+
}
|
|
460
|
+
function assertIntrospectedIdToken(claims, policy) {
|
|
461
|
+
assertTokenTypeFromClaims(claims, "id", policy);
|
|
462
|
+
if (introspectedKind(claims) === "id") return;
|
|
463
|
+
if (!policy.requireIntrospectedTokenType) return;
|
|
464
|
+
throw new OidcTokenTypeError("id", "token_type", `introspection response does not name an ID token: it carries no recognized "token_type", "typ" or "token_use". One introspection endpoint answers for every token type an AS issues, so a response that does not say what it describes is as consistent with an access or refresh token as with an ID token, and neither may be accepted as one. Set "requireIntrospectedTokenType: false" only if your provider omits the type on genuine ID tokens.`);
|
|
465
|
+
}
|
|
466
|
+
function introspectedKind(claims) {
|
|
467
|
+
return [
|
|
468
|
+
"token_type",
|
|
469
|
+
"typ",
|
|
470
|
+
"token_use"
|
|
471
|
+
].map((claim) => namedKind(claims[claim])).find((kind) => kind !== void 0);
|
|
472
|
+
}
|
|
473
|
+
// lib/core/logout.ts
|
|
474
|
+
function createLogout(ctx) {
|
|
475
|
+
/**
|
|
476
|
+
* Builds the provider's RP-initiated logout URL.
|
|
477
|
+
* @example const url = await oidc.getLogoutUrl({ postLogoutRedirectUri: "https://app.example.com/", idTokenHint: id_token });
|
|
478
|
+
*/
|
|
479
|
+
return async function getLogoutUrl({ postLogoutRedirectUri, idTokenHint, state }) {
|
|
480
|
+
assertSafeRedirectUri(postLogoutRedirectUri, "postLogoutRedirectUri");
|
|
481
|
+
try {
|
|
482
|
+
return (0, openid_client.buildEndSessionUrl)(ctx.config, {
|
|
483
|
+
post_logout_redirect_uri: postLogoutRedirectUri,
|
|
484
|
+
...idTokenHint ? { id_token_hint: idTokenHint } : {},
|
|
485
|
+
...state ? { state } : {}
|
|
486
|
+
});
|
|
487
|
+
} catch (err) {
|
|
488
|
+
throw new OidcLogoutError(`could not build a logout URL for ${ctx.issuer}: ${err instanceof Error ? err.message : String(err)}. If the issuer advertises no "end_session_endpoint" it does not support RP-initiated logout, so you need its own logout URL instead. Destroying your session locally is all this library can do for you.`, { cause: err });
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// lib/tokenValidation.ts
|
|
493
|
+
const JWS_COMPACT = /^[\w-]+\.[\w-]*\.[\w-]+$/;
|
|
494
|
+
function looksLikeJwt(token) {
|
|
495
|
+
if (!JWS_COMPACT.test(token)) return false;
|
|
496
|
+
try {
|
|
497
|
+
return typeof (0, jose.decodeProtectedHeader)(token).alg === "string";
|
|
498
|
+
} catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function subjectOf(claims) {
|
|
503
|
+
const sub = claims?.sub;
|
|
504
|
+
return typeof sub === "string" && sub.length > 0 ? sub : void 0;
|
|
505
|
+
}
|
|
506
|
+
function unverifiedSubject(token) {
|
|
507
|
+
if (!token || !looksLikeJwt(token)) return void 0;
|
|
508
|
+
try {
|
|
509
|
+
return subjectOf((0, jose.decodeJwt)(token));
|
|
510
|
+
} catch {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
var StrategyOutcome = class {
|
|
515
|
+
log;
|
|
516
|
+
accessTokenValue;
|
|
517
|
+
idTokenValue;
|
|
518
|
+
failures = [];
|
|
519
|
+
expired = false;
|
|
520
|
+
fatal;
|
|
521
|
+
constructor(log) {
|
|
522
|
+
this.log = log;
|
|
523
|
+
}
|
|
524
|
+
onError(kind, stage) {
|
|
525
|
+
return (err) => {
|
|
526
|
+
if (err instanceof OidcTokenError) {
|
|
527
|
+
this.fatal ??= err;
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (err instanceof jose.errors.JWTClaimValidationFailed && err.claim === "aud") {
|
|
531
|
+
this.fatal ??= new OidcAudienceError(kind, "aud", err.message);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (err instanceof jose.errors.JWTExpired) this.expired = true;
|
|
535
|
+
this.failures.push({
|
|
536
|
+
tokenKind: kind,
|
|
537
|
+
stage,
|
|
538
|
+
error: err
|
|
539
|
+
});
|
|
540
|
+
this.log.debug(`[oidc] ${kind} token ${stage} failed:`, err.message);
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
async function verifyTokensLocally(jwks, tokens, jwtVerifyOptionsFor, tokenTypePolicy, audiencePolicy, log) {
|
|
545
|
+
const outcome = new StrategyOutcome(log);
|
|
546
|
+
const promises = [];
|
|
547
|
+
if (tokens.access) promises.push((0, jose.jwtVerify)(tokens.access, jwks, jwtVerifyOptionsFor("access")).then((result) => {
|
|
548
|
+
assertTokenTypeAfterJwtVerify(result.payload, result.protectedHeader, "access", tokenTypePolicy);
|
|
549
|
+
assertAudienceAfterJwtVerify(result.payload, "access", audiencePolicy);
|
|
550
|
+
outcome.accessTokenValue = result.payload;
|
|
551
|
+
}).catch(outcome.onError("access", "JWT verification")));
|
|
552
|
+
if (tokens.id) promises.push((0, jose.jwtVerify)(tokens.id, jwks, jwtVerifyOptionsFor("id")).then((result) => {
|
|
553
|
+
assertTokenTypeAfterJwtVerify(result.payload, result.protectedHeader, "id", tokenTypePolicy);
|
|
554
|
+
assertAudienceAfterJwtVerify(result.payload, "id", audiencePolicy);
|
|
555
|
+
outcome.idTokenValue = result.payload;
|
|
556
|
+
}).catch(outcome.onError("id", "JWT verification")));
|
|
557
|
+
await Promise.allSettled(promises);
|
|
558
|
+
if (outcome.fatal) throw outcome.fatal;
|
|
559
|
+
return outcome;
|
|
560
|
+
}
|
|
561
|
+
async function introspectTokens(config, tokens, which, tokenTypePolicy, audiencePolicy, log) {
|
|
562
|
+
const outcome = new StrategyOutcome(log);
|
|
563
|
+
const promises = [];
|
|
564
|
+
if (which.access && tokens.access) promises.push((0, openid_client.tokenIntrospection)(config, tokens.access).then((result) => {
|
|
565
|
+
if (!result.active) throw new Error("access token is not active");
|
|
566
|
+
assertIntrospectedAccessToken(result, tokenTypePolicy);
|
|
567
|
+
assertTokenAudience(result, "access", audiencePolicy);
|
|
568
|
+
outcome.accessTokenValue = result;
|
|
569
|
+
}).catch(outcome.onError("access", "introspection")));
|
|
570
|
+
if (which.id && tokens.id) promises.push((0, openid_client.tokenIntrospection)(config, tokens.id).then((result) => {
|
|
571
|
+
if (!result.active) throw new Error("id token is not active");
|
|
572
|
+
assertIntrospectedIdToken(result, tokenTypePolicy);
|
|
573
|
+
assertIntrospectedIdTokenAudience(result, audiencePolicy);
|
|
574
|
+
outcome.idTokenValue = result;
|
|
575
|
+
}).catch(outcome.onError("id", "introspection")));
|
|
576
|
+
await Promise.allSettled(promises);
|
|
577
|
+
if (outcome.fatal) throw outcome.fatal;
|
|
578
|
+
return outcome;
|
|
579
|
+
}
|
|
580
|
+
// lib/types.ts
|
|
581
|
+
const nullable = (t) => typebox.Type.Optional(typebox.Type.Union([typebox.Type.Null(), t]));
|
|
582
|
+
const LowercaseEmail = (0, typebox.Decode)(typebox.Type.String(), (s) => s.toLowerCase());
|
|
583
|
+
const VerifiedFlag = (0, typebox.Decode)(typebox.Type.Union([
|
|
584
|
+
typebox.Type.Boolean(),
|
|
585
|
+
typebox.Type.String(),
|
|
586
|
+
typebox.Type.Number()
|
|
587
|
+
]), (value) => {
|
|
588
|
+
if (typeof value === "boolean") return value;
|
|
589
|
+
if (typeof value === "number") return value === 1;
|
|
590
|
+
return value.trim().toLowerCase() === "true";
|
|
591
|
+
});
|
|
592
|
+
const OIDCUserSchema = typebox.Type.Object({
|
|
593
|
+
iss: typebox.Type.Optional(typebox.Type.String({ minLength: 1 })),
|
|
594
|
+
sub: typebox.Type.String({ minLength: 1 }),
|
|
595
|
+
email: nullable(LowercaseEmail),
|
|
596
|
+
email_verified: nullable(VerifiedFlag),
|
|
597
|
+
name: nullable(typebox.Type.String()),
|
|
598
|
+
given_name: nullable(typebox.Type.String()),
|
|
599
|
+
family_name: nullable(typebox.Type.String()),
|
|
600
|
+
middle_name: nullable(typebox.Type.String()),
|
|
601
|
+
nickname: nullable(typebox.Type.String()),
|
|
602
|
+
preferred_username: nullable(typebox.Type.String()),
|
|
603
|
+
profile: nullable(typebox.Type.String()),
|
|
604
|
+
picture: nullable(typebox.Type.String()),
|
|
605
|
+
website: nullable(typebox.Type.String()),
|
|
606
|
+
gender: nullable(typebox.Type.String()),
|
|
607
|
+
birthdate: nullable(typebox.Type.String()),
|
|
608
|
+
zoneinfo: nullable(typebox.Type.String()),
|
|
609
|
+
locale: nullable(typebox.Type.String()),
|
|
610
|
+
phone_number: nullable(typebox.Type.String()),
|
|
611
|
+
phone_number_verified: nullable(VerifiedFlag),
|
|
612
|
+
address: nullable(typebox.Type.Object({
|
|
613
|
+
formatted: typebox.Type.Optional(typebox.Type.String()),
|
|
614
|
+
street_address: typebox.Type.Optional(typebox.Type.String()),
|
|
615
|
+
locality: typebox.Type.Optional(typebox.Type.String()),
|
|
616
|
+
region: typebox.Type.Optional(typebox.Type.String()),
|
|
617
|
+
postal_code: typebox.Type.Optional(typebox.Type.String()),
|
|
618
|
+
country: typebox.Type.Optional(typebox.Type.String())
|
|
619
|
+
})),
|
|
620
|
+
updated_at: nullable(typebox.Type.Number())
|
|
621
|
+
});
|
|
622
|
+
const CompiledOIDCUser = (0, typebox_compile.Compile)(OIDCUserSchema);
|
|
623
|
+
const USER_CLAIM_SCHEMAS = OIDCUserSchema.properties;
|
|
624
|
+
const OPTIONAL_USER_CLAIMS = Object.keys(USER_CLAIM_SCHEMAS).filter((claim) => claim !== "sub");
|
|
625
|
+
function parseOIDCUser(value) {
|
|
626
|
+
const claims = typeof value === "object" && value !== null ? value : {};
|
|
627
|
+
const candidate = { sub: claims.sub };
|
|
628
|
+
for (const claim of OPTIONAL_USER_CLAIMS) {
|
|
629
|
+
const supplied = claims[claim];
|
|
630
|
+
if (supplied === void 0) continue;
|
|
631
|
+
const schema = USER_CLAIM_SCHEMAS[claim];
|
|
632
|
+
if (typebox_value.Value.Check(schema, supplied)) candidate[claim] = supplied;
|
|
633
|
+
}
|
|
634
|
+
if (!CompiledOIDCUser.Check(candidate)) {
|
|
635
|
+
const [first] = [...CompiledOIDCUser.Errors(candidate)];
|
|
636
|
+
throw new OidcClaimsError(`could not read the validated claims as a user: ${first ? `${first.instancePath || "/"} ${first.message}` : "unknown claim shape"}`);
|
|
637
|
+
}
|
|
638
|
+
return CompiledOIDCUser.Decode(candidate);
|
|
639
|
+
}
|
|
640
|
+
const OIDCFlowStateSchema = typebox.Type.Object({
|
|
641
|
+
visitedUrl: typebox.Type.String(),
|
|
642
|
+
random: typebox.Type.String(),
|
|
643
|
+
nonce: typebox.Type.Optional(typebox.Type.String())
|
|
644
|
+
});
|
|
645
|
+
const CompiledOIDCFlowState = (0, typebox_compile.Compile)(OIDCFlowStateSchema);
|
|
646
|
+
function parseOIDCFlowState(value) {
|
|
647
|
+
let parsed;
|
|
648
|
+
try {
|
|
649
|
+
parsed = JSON.parse(value);
|
|
650
|
+
} catch (err) {
|
|
651
|
+
throw new OidcStateError("could not read the stored OAuth state: it is not the value startSignin returned. Hand it back exactly as it was stored, unmodified.", { cause: err });
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
return CompiledOIDCFlowState.Decode(CompiledOIDCFlowState.Clean(parsed));
|
|
655
|
+
} catch (err) {
|
|
656
|
+
const [first] = [...CompiledOIDCFlowState.Errors(parsed)];
|
|
657
|
+
throw new OidcStateError(`the stored OAuth state is not the shape startSignin produces: ${first ? `${first.instancePath || "/"} ${first.message}` : "unknown state shape"}`, { cause: err });
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const VOUCHED_CLAIMS = [["email", "email_verified"], ["phone_number", "phone_number_verified"]];
|
|
661
|
+
function mergeTokenClaims(accessTokenClaims, idTokenClaims, onConflict) {
|
|
662
|
+
const access = accessTokenClaims ?? {};
|
|
663
|
+
const id = idTokenClaims ?? {};
|
|
664
|
+
const merged = {
|
|
665
|
+
...access,
|
|
666
|
+
...id
|
|
667
|
+
};
|
|
668
|
+
for (const [value, flag] of VOUCHED_CLAIMS) {
|
|
669
|
+
if (onConflict && access[value] !== void 0 && id[value] !== void 0 && access[value] !== id[value]) onConflict(value);
|
|
670
|
+
const source = id[value] !== void 0 ? id : access[value] !== void 0 ? access : void 0;
|
|
671
|
+
if (source?.[flag] === void 0) delete merged[flag];
|
|
672
|
+
else merged[flag] = source[flag];
|
|
673
|
+
}
|
|
674
|
+
return merged;
|
|
675
|
+
}
|
|
676
|
+
// lib/core/grant.ts
|
|
677
|
+
function jwtVerifyOptions(ctx, tokenKind) {
|
|
678
|
+
return {
|
|
679
|
+
issuer: ctx.issuer,
|
|
680
|
+
audience: audienceVerifyOption(ctx.audiencePolicy, tokenKind),
|
|
681
|
+
clockTolerance: ctx.clockTolerance,
|
|
682
|
+
algorithms: ctx.signingAlgorithms,
|
|
683
|
+
requiredClaims: tokenKind === "id" ? [
|
|
684
|
+
"iss",
|
|
685
|
+
"sub",
|
|
686
|
+
"aud",
|
|
687
|
+
"exp",
|
|
688
|
+
"iat"
|
|
689
|
+
] : [
|
|
690
|
+
"iss",
|
|
691
|
+
"sub",
|
|
692
|
+
"exp"
|
|
693
|
+
]
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function assertGrantedIdToken(ctx, tokens) {
|
|
697
|
+
const claims = tokens.claims();
|
|
698
|
+
if (claims) assertTokenAudience(claims, "id", ctx.audiencePolicy);
|
|
699
|
+
}
|
|
700
|
+
// lib/core/processTokens.ts
|
|
701
|
+
function assertOptionalToken(value, label) {
|
|
702
|
+
if (value === void 0) return void 0;
|
|
703
|
+
if (typeof value !== "string" || value.length === 0) throw new OidcInputError(`processTokens was given a malformed \`${label}\`: expected a non-empty string, or \`undefined\` to omit it, got ${value === null ? "null" : JSON.stringify(value)}. Pass \`undefined\`, not \`null\`/\`""\`, when you don't have this token.`);
|
|
704
|
+
return value;
|
|
705
|
+
}
|
|
706
|
+
function createSenderConstrainedWarning(log) {
|
|
707
|
+
let warned = false;
|
|
708
|
+
return (claims) => {
|
|
709
|
+
if (warned) return;
|
|
710
|
+
const cnf = claims?.cnf;
|
|
711
|
+
if (cnf === void 0 || cnf === null) return;
|
|
712
|
+
warned = true;
|
|
713
|
+
log.warn("[oidc] this access token carries a \"cnf\" claim: the issuer bound it to a key (DPoP or mTLS), and that binding is not being checked. Validating it needs the proof that travels with the HTTP request, which this library never sees. The token is accepted as an ordinary bearer token, so a stolen one is usable by anyone. Verify the proof at your edge if you accept tokens from callers you do not control.");
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function createProcessTokens(ctx, refresh) {
|
|
717
|
+
const warnIfSenderConstrained = createSenderConstrainedWarning(ctx.log);
|
|
718
|
+
function worthIntrospecting(token) {
|
|
719
|
+
return ctx.introspectionMode !== "never" && (ctx.introspectionMode === "auto" || !ctx.jwks || !looksLikeJwt(token));
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Validates the tokens and returns the user, refreshing once if a `refresh_token` is given and validation fails.
|
|
723
|
+
* @example const { user, refreshed, refreshedTokens } = await oidc.processTokens({ access_token, id_token, refresh_token });
|
|
724
|
+
*/
|
|
725
|
+
return async function processTokens(input) {
|
|
726
|
+
const access_token = assertOptionalToken(input.access_token, "access_token");
|
|
727
|
+
const id_token = assertOptionalToken(input.id_token, "id_token");
|
|
728
|
+
const refresh_token = assertOptionalToken(input.refresh_token, "refresh_token");
|
|
729
|
+
const { subject } = input;
|
|
730
|
+
if (!access_token && !id_token) throw new OidcInputError("processTokens needs an access token, an id token, or both. Validating neither is meaningless");
|
|
731
|
+
if (subject !== void 0 && (typeof subject !== "string" || !subject)) throw new OidcInputError("processTokens was given a `subject` that is not a non-empty string");
|
|
732
|
+
let currentAccessToken = access_token;
|
|
733
|
+
let currentIdToken = id_token;
|
|
734
|
+
let newTokens;
|
|
735
|
+
let accessTokenValue;
|
|
736
|
+
let idTokenValue;
|
|
737
|
+
let failures = [];
|
|
738
|
+
let boundSubject = subject ?? unverifiedSubject(id_token) ?? unverifiedSubject(access_token);
|
|
739
|
+
try {
|
|
740
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
741
|
+
accessTokenValue = void 0;
|
|
742
|
+
idTokenValue = void 0;
|
|
743
|
+
failures = [];
|
|
744
|
+
let expired = false;
|
|
745
|
+
if (ctx.jwks) {
|
|
746
|
+
const local = await verifyTokensLocally(ctx.jwks, {
|
|
747
|
+
access: currentAccessToken,
|
|
748
|
+
id: currentIdToken
|
|
749
|
+
}, (tokenKind) => jwtVerifyOptions(ctx, tokenKind), ctx.tokenTypePolicy, ctx.audiencePolicy, ctx.log);
|
|
750
|
+
accessTokenValue = local.accessTokenValue;
|
|
751
|
+
idTokenValue = local.idTokenValue;
|
|
752
|
+
failures = local.failures;
|
|
753
|
+
expired = local.expired;
|
|
754
|
+
}
|
|
755
|
+
if (!(expired && !!refresh_token)) {
|
|
756
|
+
const introspectAccess = !accessTokenValue && !!currentAccessToken && worthIntrospecting(currentAccessToken);
|
|
757
|
+
const introspectId = !idTokenValue && !!currentIdToken && worthIntrospecting(currentIdToken);
|
|
758
|
+
if (introspectAccess || introspectId) {
|
|
759
|
+
ctx.log.debug("[oidc] could not verify tokens locally, trying introspection...");
|
|
760
|
+
const introspected = await introspectTokens(ctx.config, {
|
|
761
|
+
access: currentAccessToken,
|
|
762
|
+
id: currentIdToken
|
|
763
|
+
}, {
|
|
764
|
+
access: introspectAccess,
|
|
765
|
+
id: introspectId
|
|
766
|
+
}, ctx.tokenTypePolicy, ctx.audiencePolicy, ctx.log);
|
|
767
|
+
accessTokenValue ??= introspected.accessTokenValue;
|
|
768
|
+
idTokenValue ??= introspected.idTokenValue;
|
|
769
|
+
failures = failures.concat(introspected.failures);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (!newTokens && !subject) boundSubject = subjectOf(accessTokenValue) ?? subjectOf(idTokenValue) ?? boundSubject;
|
|
773
|
+
if (!(!newTokens && !!refresh_token && !((!currentAccessToken || !!accessTokenValue) && (!currentIdToken || !!idTokenValue)))) break;
|
|
774
|
+
ctx.log.debug(expired ? "[oidc] token expired, refreshing directly..." : "[oidc] a token could not be validated, refreshing as a last resort...");
|
|
775
|
+
newTokens = await refresh({ refreshToken: refresh_token });
|
|
776
|
+
currentAccessToken = newTokens.access_token;
|
|
777
|
+
currentIdToken = newTokens.id_token;
|
|
778
|
+
}
|
|
779
|
+
return finalizeProcessTokens({
|
|
780
|
+
currentAccessToken,
|
|
781
|
+
currentIdToken,
|
|
782
|
+
accessTokenValue,
|
|
783
|
+
idTokenValue,
|
|
784
|
+
failures,
|
|
785
|
+
newTokens,
|
|
786
|
+
boundSubject,
|
|
787
|
+
subject,
|
|
788
|
+
issuer: ctx.issuer,
|
|
789
|
+
log: ctx.log,
|
|
790
|
+
warnIfSenderConstrained
|
|
791
|
+
});
|
|
792
|
+
} catch (err) {
|
|
793
|
+
throw attachRefreshedTokens(err, newTokens);
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function finalizeProcessTokens({ currentAccessToken, currentIdToken, accessTokenValue, idTokenValue, failures, newTokens, boundSubject, subject, issuer, log, warnIfSenderConstrained }) {
|
|
798
|
+
const unvalidated = [];
|
|
799
|
+
if (currentAccessToken && !accessTokenValue) unvalidated.push("access");
|
|
800
|
+
if (currentIdToken && !idTokenValue) unvalidated.push("id");
|
|
801
|
+
if (unvalidated.length) throw new OidcTokenValidationError(unvalidated, failures);
|
|
802
|
+
const accessSub = subjectOf(accessTokenValue);
|
|
803
|
+
const idSub = subjectOf(idTokenValue);
|
|
804
|
+
if (accessSub && idSub && accessSub !== idSub) throw new OidcSubjectError(`subject in access token and id token do not match (${accessSub} vs ${idSub})`, accessSub, idSub);
|
|
805
|
+
const finalSubject = accessSub ?? idSub;
|
|
806
|
+
if (newTokens && boundSubject && finalSubject && finalSubject !== boundSubject) throw new OidcSubjectError(`refresh returned a token set for a different subject (${finalSubject}) than the tokens it replaced (${boundSubject}). The refresh token does not belong to this session`, boundSubject, finalSubject);
|
|
807
|
+
if (subject && finalSubject && finalSubject !== subject) throw new OidcSubjectError(`the tokens describe subject ${finalSubject}, not the ${subject} they were presented for`, subject, finalSubject);
|
|
808
|
+
if (newTokens && !boundSubject) log.warn("[oidc] refreshed a token set whose original subject could not be determined. The supplied tokens were opaque or unreadable, so the refreshed set could not be checked against them. Pass `subject` to processTokens to enforce it.");
|
|
809
|
+
warnIfSenderConstrained(accessTokenValue);
|
|
810
|
+
const base = {
|
|
811
|
+
user: parseOIDCUser({
|
|
812
|
+
...mergeTokenClaims(accessTokenValue, idTokenValue, (claim) => log.warn(`[oidc] the access token and the id token disagree on "${claim}" for subject ${finalSubject ?? "(unknown)"}. Using the id token's value, and dropping any verification flag that did not come with it. One of the two is stale.`)),
|
|
813
|
+
iss: issuer
|
|
814
|
+
}),
|
|
815
|
+
accessTokenClaims: accessTokenValue,
|
|
816
|
+
idTokenClaims: idTokenValue
|
|
817
|
+
};
|
|
818
|
+
if (newTokens) return {
|
|
819
|
+
...base,
|
|
820
|
+
refreshed: true,
|
|
821
|
+
refreshedTokens: newTokens
|
|
822
|
+
};
|
|
823
|
+
return {
|
|
824
|
+
...base,
|
|
825
|
+
refreshed: false
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
// lib/core/refresh.ts
|
|
829
|
+
function createRefresh(ctx) {
|
|
830
|
+
/**
|
|
831
|
+
* Exchanges a refresh token for a new token set.
|
|
832
|
+
* @example const tokens = await oidc.refresh({ refreshToken, expectedSubject: user.sub });
|
|
833
|
+
*/
|
|
834
|
+
return async function refresh({ refreshToken, expectedSubject }) {
|
|
835
|
+
if (expectedSubject !== void 0 && (typeof expectedSubject !== "string" || !expectedSubject)) throw new OidcInputError("refresh was given an `expectedSubject` that is not a non-empty string");
|
|
836
|
+
let tokens;
|
|
837
|
+
try {
|
|
838
|
+
tokens = await (0, openid_client.refreshTokenGrant)(ctx.config, refreshToken);
|
|
839
|
+
} catch (err) {
|
|
840
|
+
throw new OidcGrantError("refresh_token", `could not exchange the refresh token: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
841
|
+
}
|
|
842
|
+
assertGrantedIdToken(ctx, tokens);
|
|
843
|
+
if (expectedSubject) {
|
|
844
|
+
const actual = subjectOf(tokens.claims());
|
|
845
|
+
if (!actual) ctx.log.warn("[oidc] refresh could not be checked against the expected subject: the provider returned no id token to read one from. The new token set is returned unchecked.");
|
|
846
|
+
else if (actual !== expectedSubject) throw new OidcSubjectError(`refresh returned a token set for a different subject (${actual}) than the ${expectedSubject} it was asked for. The refresh token does not belong to this session`, expectedSubject, actual);
|
|
847
|
+
}
|
|
848
|
+
return tokens;
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
// lib/core/signin.ts
|
|
852
|
+
function createSignin(ctx) {
|
|
853
|
+
/**
|
|
854
|
+
* Builds the authorization URL. Store `codeVerifier` and `state` until the callback.
|
|
855
|
+
* @example const { authorizationUrl, codeVerifier, state } = await oidc.startSignin({ redirectUri, returnTo: "/" });
|
|
856
|
+
*/
|
|
857
|
+
async function startSignin({ redirectUri, returnTo, scope = "openid profile email offline_access" }) {
|
|
858
|
+
assertSafeReturnTo(returnTo, ctx.returnToPolicy);
|
|
859
|
+
assertSafeRedirectUri(redirectUri, "redirectUri");
|
|
860
|
+
if (!scope.split(/\s+/).includes("openid")) throw new OidcInputError(`invalid scope ("${scope}"): it must include "openid"`);
|
|
861
|
+
const codeVerifier = (0, openid_client.randomPKCECodeVerifier)();
|
|
862
|
+
const codeChallenge = await (0, openid_client.calculatePKCECodeChallenge)(codeVerifier);
|
|
863
|
+
const nonce = (0, openid_client.randomNonce)();
|
|
864
|
+
const state = JSON.stringify({
|
|
865
|
+
visitedUrl: returnTo,
|
|
866
|
+
random: (0, openid_client.randomState)(),
|
|
867
|
+
nonce
|
|
868
|
+
});
|
|
869
|
+
return {
|
|
870
|
+
codeVerifier,
|
|
871
|
+
authorizationUrl: (0, openid_client.buildAuthorizationUrl)(ctx.config, {
|
|
872
|
+
redirect_uri: redirectUri,
|
|
873
|
+
scope,
|
|
874
|
+
code_challenge: codeChallenge,
|
|
875
|
+
code_challenge_method: "S256",
|
|
876
|
+
state,
|
|
877
|
+
nonce
|
|
878
|
+
}),
|
|
879
|
+
state
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Exchanges the callback's code for tokens and returns the `returnTo` passed to `startSignin`.
|
|
884
|
+
* @example const { tokens, returnTo } = await oidc.resolveSignin({ callbackUrl: new URL(req.url), codeVerifier, expectedState: state });
|
|
885
|
+
*/
|
|
886
|
+
async function resolveSignin({ callbackUrl, codeVerifier, expectedState }) {
|
|
887
|
+
if (typeof codeVerifier !== "string" || codeVerifier.length === 0) throw new OidcInputError("resolveSignin was given no PKCE code verifier. Pass the `codeVerifier` startSignin returned, exactly as it was stored.");
|
|
888
|
+
const parsedState = parseOIDCFlowState(expectedState);
|
|
889
|
+
const returnTo = assertSafeReturnTo(parsedState.visitedUrl, ctx.returnToPolicy);
|
|
890
|
+
let tokens;
|
|
891
|
+
try {
|
|
892
|
+
tokens = await (0, openid_client.authorizationCodeGrant)(ctx.config, callbackUrl, {
|
|
893
|
+
pkceCodeVerifier: codeVerifier,
|
|
894
|
+
expectedState,
|
|
895
|
+
expectedNonce: parsedState.nonce,
|
|
896
|
+
idTokenExpected: true
|
|
897
|
+
});
|
|
898
|
+
} catch (err) {
|
|
899
|
+
throw new OidcGrantError("authorization_code", `could not complete the login: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
900
|
+
}
|
|
901
|
+
assertGrantedIdToken(ctx, tokens);
|
|
902
|
+
return {
|
|
903
|
+
tokens,
|
|
904
|
+
returnTo
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
startSignin,
|
|
909
|
+
resolveSignin
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
// lib/core/userInfo.ts
|
|
913
|
+
function createUserInfoFetcher(ctx) {
|
|
914
|
+
/**
|
|
915
|
+
* Fetches the UserInfo endpoint, checking the response is for `expectedSubject`.
|
|
916
|
+
* @example const info = await oidc.fetchUserInfoFromIssuer({ accessToken, expectedSubject: user.sub });
|
|
917
|
+
*/
|
|
918
|
+
return async function fetchUserInfoFromIssuer({ accessToken, expectedSubject }) {
|
|
919
|
+
if (typeof expectedSubject !== "string" || !expectedSubject) throw new OidcInputError("fetchUserInfoFromIssuer needs the `sub` this token set was validated for, as a non-empty string. The response is only meaningful once it is known to describe that subject");
|
|
920
|
+
try {
|
|
921
|
+
return await (0, openid_client.fetchUserInfo)(ctx.config, accessToken, expectedSubject);
|
|
922
|
+
} catch (err) {
|
|
923
|
+
throw new OidcUserInfoError(`could not fetch UserInfo claims for subject ${expectedSubject}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
// lib/core/index.ts
|
|
928
|
+
/**
|
|
929
|
+
* Discovers the issuer and returns the sign-in, validation, refresh and logout functions.
|
|
930
|
+
* @example const oidc = await createOidcCore({ issuer: "https://auth.example.com", clientId: "my-app" });
|
|
931
|
+
*/
|
|
932
|
+
async function createOidcCore(opts) {
|
|
933
|
+
const log = buildLogger(opts.logger, opts.logLevel);
|
|
934
|
+
const development = resolveDevelopment(opts.development);
|
|
935
|
+
if (development) log.warn("[oidc] development mode is on: plain-HTTP requests to the IdP are allowed. Never enable this in production.");
|
|
936
|
+
if (typeof opts.clientId !== "string" || opts.clientId.length === 0) throw new TypeError("invalid clientId: expected a non-empty string. It is what every token is bound to, so there is nothing this library could check without it.");
|
|
937
|
+
if (opts.verifyAudience && opts.expectedAudience !== void 0) log.warn("[oidc] both \"verifyAudience\" and \"expectedAudience\" are set. \"verifyAudience\" replaces the audience policy entirely, so \"expectedAudience\" is not checked at all. Assert it inside your callback, or drop one of the two.");
|
|
938
|
+
const clockTolerance = resolveClockTolerance(opts.clockTolerance);
|
|
939
|
+
const config = await (0, openid_client.discovery)(new URL(opts.issuer), opts.clientId, {
|
|
940
|
+
client_secret: opts.clientSecret,
|
|
941
|
+
token_endpoint_auth_method: opts.clientSecret ? void 0 : "none",
|
|
942
|
+
[openid_client.clockTolerance]: clockTolerance
|
|
943
|
+
}, void 0, { execute: development ? [openid_client.allowInsecureRequests] : [] });
|
|
944
|
+
assertDiscoveredIssuer(opts.issuer, config.serverMetadata().issuer);
|
|
945
|
+
assertPkceSupported(config.serverMetadata().code_challenge_methods_supported, opts.clientSecret, log);
|
|
946
|
+
const jwksUri = config.serverMetadata().jwks_uri;
|
|
947
|
+
const jwks = jwksUri ? (0, jose.createRemoteJWKSet)(resolveJwksUri(jwksUri, development)) : void 0;
|
|
948
|
+
const issuer = config.serverMetadata().issuer;
|
|
949
|
+
if (!jwksUri) log.warn(`[oidc] the issuer advertises no "jwks_uri", so no token can be verified offline. Every validation will fall back to remote introspection${(opts.introspection ?? "opaque-only") === "never" ? ", which \"introspection: 'never'\" has switched off, so no token will validate at all" : ", one round trip to the IdP per request"}.`);
|
|
950
|
+
const ctx = {
|
|
951
|
+
config,
|
|
952
|
+
log,
|
|
953
|
+
issuer,
|
|
954
|
+
jwks,
|
|
955
|
+
audiencePolicy: buildAudiencePolicy(opts),
|
|
956
|
+
tokenTypePolicy: buildTokenTypePolicy(opts),
|
|
957
|
+
returnToPolicy: buildReturnToPolicy(opts),
|
|
958
|
+
introspectionMode: opts.introspection ?? "opaque-only",
|
|
959
|
+
clockTolerance,
|
|
960
|
+
signingAlgorithms: resolveSigningAlgorithms(config, opts.signingAlgorithms, log)
|
|
961
|
+
};
|
|
962
|
+
const { startSignin, resolveSignin } = createSignin(ctx);
|
|
963
|
+
const refresh = createRefresh(ctx);
|
|
964
|
+
return {
|
|
965
|
+
configuration: config,
|
|
966
|
+
startSignin,
|
|
967
|
+
resolveSignin,
|
|
968
|
+
processTokens: createProcessTokens(ctx, refresh),
|
|
969
|
+
refresh,
|
|
970
|
+
getLogoutUrl: createLogout(ctx),
|
|
971
|
+
fetchUserInfoFromIssuer: createUserInfoFetcher(ctx)
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
exports.DEFAULT_AUTHORIZED_PARTY_CLAIMS = DEFAULT_AUTHORIZED_PARTY_CLAIMS;
|
|
975
|
+
exports.DEFAULT_ID_TOKEN_ONLY_CLAIMS = DEFAULT_ID_TOKEN_ONLY_CLAIMS;
|
|
976
|
+
exports.DEFAULT_SIGNING_ALGORITHMS = DEFAULT_SIGNING_ALGORITHMS;
|
|
977
|
+
exports.OidcAudienceError = OidcAudienceError;
|
|
978
|
+
exports.OidcClaimsError = OidcClaimsError;
|
|
979
|
+
exports.OidcError = OidcError;
|
|
980
|
+
exports.OidcGrantError = OidcGrantError;
|
|
981
|
+
exports.OidcInputError = OidcInputError;
|
|
982
|
+
exports.OidcLogoutError = OidcLogoutError;
|
|
983
|
+
exports.OidcReturnToError = OidcReturnToError;
|
|
984
|
+
exports.OidcStateError = OidcStateError;
|
|
985
|
+
exports.OidcSubjectError = OidcSubjectError;
|
|
986
|
+
exports.OidcTokenError = OidcTokenError;
|
|
987
|
+
exports.OidcTokenTypeError = OidcTokenTypeError;
|
|
988
|
+
exports.OidcTokenValidationError = OidcTokenValidationError;
|
|
989
|
+
exports.OidcUserInfoError = OidcUserInfoError;
|
|
990
|
+
exports.assertAudienceAfterJwtVerify = assertAudienceAfterJwtVerify;
|
|
991
|
+
exports.assertAudienceClaim = assertAudienceClaim;
|
|
992
|
+
exports.assertAuthorizedParty = assertAuthorizedParty;
|
|
993
|
+
exports.assertDiscoveredIssuer = assertDiscoveredIssuer;
|
|
994
|
+
exports.assertIntrospectedAccessToken = assertIntrospectedAccessToken;
|
|
995
|
+
exports.assertIntrospectedIdToken = assertIntrospectedIdToken;
|
|
996
|
+
exports.assertIntrospectedIdTokenAudience = assertIntrospectedIdTokenAudience;
|
|
997
|
+
exports.assertPkceSupported = assertPkceSupported;
|
|
998
|
+
exports.assertSafeRedirectUri = assertSafeRedirectUri;
|
|
999
|
+
exports.assertSafeReturnTo = assertSafeReturnTo;
|
|
1000
|
+
exports.assertTokenAudience = assertTokenAudience;
|
|
1001
|
+
exports.assertTokenTypeAfterJwtVerify = assertTokenTypeAfterJwtVerify;
|
|
1002
|
+
exports.assertTokenTypeFromClaims = assertTokenTypeFromClaims;
|
|
1003
|
+
exports.attachRefreshedTokens = attachRefreshedTokens;
|
|
1004
|
+
exports.audienceVerifyOption = audienceVerifyOption;
|
|
1005
|
+
exports.buildAudiencePolicy = buildAudiencePolicy;
|
|
1006
|
+
exports.buildLogger = buildLogger;
|
|
1007
|
+
exports.buildReturnToPolicy = buildReturnToPolicy;
|
|
1008
|
+
exports.buildTokenTypePolicy = buildTokenTypePolicy;
|
|
1009
|
+
exports.createOidcCore = createOidcCore;
|
|
1010
|
+
exports.mergeTokenClaims = mergeTokenClaims;
|
|
1011
|
+
exports.parseOIDCFlowState = parseOIDCFlowState;
|
|
1012
|
+
exports.parseOIDCUser = parseOIDCUser;
|
|
1013
|
+
exports.resolveClockTolerance = resolveClockTolerance;
|
|
1014
|
+
exports.resolveDevelopment = resolveDevelopment;
|
|
1015
|
+
exports.resolveJwksUri = resolveJwksUri;
|
|
1016
|
+
|
|
1017
|
+
//# sourceMappingURL=index.cjs.map
|