@hyper.authio/server 0.1.1 → 0.7.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/README.md +37 -7
- package/dist/index.cjs +270 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -16
- package/dist/index.d.ts +165 -16
- package/dist/index.js +267 -98
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
|
-
import { systemClock, AuthioClaimTypes, parseResourceAccess, AuthioError, AuthioConfigurationError, resolveConnection, createAuthioDiscoveryClient, seconds,
|
|
2
|
-
import { jwtVerify, createRemoteJWKSet, customFetch, errors, decodeProtectedHeader } from 'jose';
|
|
1
|
+
import { systemClock, AuthioClaimTypes, parseResourceAccess, AuthioError, AuthioDefaults, resolveIssuer, AuthioConfigurationError, resolveConnection, createAuthioDiscoveryClient, seconds, createAuthioSigningKeys, encryptionKeyFromFile, createAuthioTokenDecrypter, createAuthioClient, toScopeString, AuthioWebhookHeaders, verifyWebhookSignature, tryParseWebhookEvent, toClientCredentialsToken, isAuthioTokenEncrypted, AuthioTokenValidationError, verifyAuthioJwt, AuthioTokenTypes } from '@hyper.authio/client';
|
|
3
2
|
|
|
4
3
|
// src/user.ts
|
|
5
|
-
var
|
|
4
|
+
var accessTokenTypes = /* @__PURE__ */ new Set(["bearer", "api_key"]);
|
|
5
|
+
var AuthioUser = class _AuthioUser {
|
|
6
6
|
/** `sub`: the person's id, or the credential's for a machine caller. */
|
|
7
7
|
subject;
|
|
8
|
+
/**
|
|
9
|
+
* Who called: `user` (a person signed in), `client` (a client as itself), `consumer` (a machine consumer of a
|
|
10
|
+
* client) or `account_credential` (a credential of an Authio account).
|
|
11
|
+
*/
|
|
12
|
+
kind;
|
|
13
|
+
/**
|
|
14
|
+
* How a machine caller proved who it is (`client_secret`, `personal_access_token`, `api_key`,
|
|
15
|
+
* `consumer_api_key`); `undefined` for a person or a client.
|
|
16
|
+
*/
|
|
17
|
+
credentialForm;
|
|
18
|
+
/** `consumer_name`, for a consumer. */
|
|
19
|
+
consumerName;
|
|
20
|
+
/** `credential_name`, for an account credential. */
|
|
21
|
+
credentialName;
|
|
22
|
+
/** `owner_user_id`: the person who made the credential. */
|
|
23
|
+
ownerUserId;
|
|
24
|
+
/**
|
|
25
|
+
* `owner_username`: the person who made the credential. On an account credential Authio wrote it to
|
|
26
|
+
* `consumer_name` before, which is read when `owner_username` is absent.
|
|
27
|
+
*/
|
|
28
|
+
ownerUsername;
|
|
29
|
+
/** `credential_realm`: the one realm an account credential is limited to. */
|
|
30
|
+
credentialRealm;
|
|
31
|
+
/** `credential_client`: the one client an account credential is limited to. */
|
|
32
|
+
credentialClient;
|
|
33
|
+
/** Whether an administrator impersonates the subject. */
|
|
34
|
+
impersonated;
|
|
35
|
+
/** The administrator impersonating the subject. */
|
|
36
|
+
impersonator;
|
|
37
|
+
/** Whether a client acts for the subject through a delegation (`act` with `client_id`). */
|
|
38
|
+
delegated;
|
|
39
|
+
/** The clients acting for the subject, outermost first. */
|
|
40
|
+
actorChain;
|
|
41
|
+
/** `auth_time`, in seconds since the epoch. */
|
|
42
|
+
authTime;
|
|
8
43
|
/** The client the token was issued to (`azp`, else `client_id`). */
|
|
9
44
|
clientId;
|
|
10
45
|
/** `sid`. */
|
|
@@ -58,8 +93,11 @@ var AuthioUser = class {
|
|
|
58
93
|
* @param claims The token's payload, or the introspection answer's members.
|
|
59
94
|
* @param settings Where flat roles and groups are read from.
|
|
60
95
|
* @param token The access token, kept for forwarding and on-behalf-of.
|
|
96
|
+
* @param source Where the claims come from: a JWT (default) or an introspection answer. An introspection answer for
|
|
97
|
+
* a consumer that reports no `exp` and no `actor_type` is the consumer's API key (Authio reports no lifetime for a
|
|
98
|
+
* key), so its `credentialForm` is `consumer_api_key`.
|
|
61
99
|
*/
|
|
62
|
-
constructor(claims, settings = {}, token) {
|
|
100
|
+
constructor(claims, settings = {}, token, source = "token") {
|
|
63
101
|
const text = (name) => typeof claims[name] === "string" && claims[name] ? claims[name] : void 0;
|
|
64
102
|
this.claims = Object.freeze({ ...claims });
|
|
65
103
|
this.subject = text(AuthioClaimTypes.Subject) ?? "";
|
|
@@ -89,11 +127,29 @@ var AuthioUser = class {
|
|
|
89
127
|
this.preferredUsername = text(AuthioClaimTypes.PreferredUsername);
|
|
90
128
|
this.credentialType = text(AuthioClaimTypes.CredentialType);
|
|
91
129
|
this.credentialId = text(AuthioClaimTypes.CredentialId);
|
|
130
|
+
this.authTime = toSeconds(claims[AuthioClaimTypes.AuthTime]);
|
|
131
|
+
this.kind = this.credentialType === "client_consumer" ? "consumer" : this.credentialType === "account_secret" ? "account_credential" : this.authTime !== void 0 ? "user" : "client";
|
|
132
|
+
const machine = this.kind === "consumer" || this.kind === "account_credential";
|
|
133
|
+
const keyIntrospected = source === "introspection" && this.kind === "consumer" && claims[AuthioClaimTypes.Expiration] === void 0;
|
|
134
|
+
this.credentialForm = machine ? text(AuthioClaimTypes.ActorType) ?? (keyIntrospected ? "consumer_api_key" : "client_secret") : void 0;
|
|
135
|
+
this.consumerName = this.kind === "consumer" ? text(AuthioClaimTypes.ConsumerName) : void 0;
|
|
136
|
+
this.credentialName = text(AuthioClaimTypes.CredentialName);
|
|
137
|
+
this.ownerUserId = text(AuthioClaimTypes.OwnerUserId);
|
|
138
|
+
this.ownerUsername = text(AuthioClaimTypes.OwnerUsername) ?? (this.kind === "account_credential" ? text(AuthioClaimTypes.ConsumerName) : void 0);
|
|
139
|
+
this.credentialRealm = text(AuthioClaimTypes.CredentialRealm);
|
|
140
|
+
this.credentialClient = text(AuthioClaimTypes.CredentialClient);
|
|
141
|
+
this.delegated = this.actor?.clientId !== void 0;
|
|
142
|
+
this.actorChain = Object.freeze(chainOf(this.actor));
|
|
143
|
+
this.impersonated = !this.delegated && this.actor?.subject !== void 0;
|
|
144
|
+
this.impersonator = this.impersonated ? Object.freeze({ subject: this.actor.subject, name: this.actor.name }) : void 0;
|
|
92
145
|
Object.defineProperty(this, "token", { value: token, enumerable: false });
|
|
93
146
|
}
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Reads the caller from an introspection answer: `client_id` becomes `azp`, `username` becomes
|
|
149
|
+
* `preferred_username`, and `token_type` becomes `typ` unless it is `api_key` (a consumer's API key).
|
|
150
|
+
*/
|
|
151
|
+
static fromIntrospection(result, settings = {}, token) {
|
|
152
|
+
return new _AuthioUser(claimsOfIntrospection(result), settings, token, "introspection");
|
|
97
153
|
}
|
|
98
154
|
/** Whether the scope was granted. */
|
|
99
155
|
hasScope(scope) {
|
|
@@ -137,9 +193,20 @@ var AuthioUser = class {
|
|
|
137
193
|
/** Every member but the raw token. */
|
|
138
194
|
toJSON() {
|
|
139
195
|
const { flatRoles: _flatRoles, ...members } = this;
|
|
140
|
-
return { ...members, resourceAccess: this.resourceAccess.entries
|
|
196
|
+
return { ...members, resourceAccess: this.resourceAccess.entries };
|
|
141
197
|
}
|
|
142
198
|
};
|
|
199
|
+
function claimsOfIntrospection(result) {
|
|
200
|
+
const { active: _active, token_type: tokenType, username, ...members } = result;
|
|
201
|
+
const claims = { ...members };
|
|
202
|
+
if (typeof tokenType === "string" && claims.typ === void 0 && tokenType.toLowerCase() !== "api_key") claims.typ = tokenType;
|
|
203
|
+
if (typeof username === "string" && claims.preferred_username === void 0) claims.preferred_username = username;
|
|
204
|
+
if (typeof result.client_id === "string" && claims.azp === void 0) claims.azp = result.client_id;
|
|
205
|
+
return claims;
|
|
206
|
+
}
|
|
207
|
+
function isAccessTokenType(type) {
|
|
208
|
+
return accessTokenTypes.has(type.toLowerCase());
|
|
209
|
+
}
|
|
143
210
|
function valuesOf(value) {
|
|
144
211
|
if (value === void 0 || value === null) return [];
|
|
145
212
|
if (Array.isArray(value)) return value.flatMap((item) => item === null || item === void 0 ? [] : [toText(item)]);
|
|
@@ -168,11 +235,20 @@ function readActor(value, depth = 0) {
|
|
|
168
235
|
if (!actor || typeof actor !== "object" || Array.isArray(actor) || depth > 16) return void 0;
|
|
169
236
|
const record = actor;
|
|
170
237
|
return Object.freeze({
|
|
171
|
-
clientId: typeof record.client_id === "string" ? record.client_id : void 0,
|
|
172
|
-
subject: typeof record.sub === "string" ? record.sub : void 0,
|
|
238
|
+
clientId: typeof record.client_id === "string" && record.client_id ? record.client_id : void 0,
|
|
239
|
+
subject: typeof record.sub === "string" && record.sub ? record.sub : void 0,
|
|
240
|
+
name: typeof record.name === "string" && record.name ? record.name : void 0,
|
|
173
241
|
actor: readActor(record.act, depth + 1)
|
|
174
242
|
});
|
|
175
243
|
}
|
|
244
|
+
function chainOf(actor) {
|
|
245
|
+
const chain = [];
|
|
246
|
+
for (let current = actor; current; current = current.actor) if (current.clientId) chain.push(current.clientId);
|
|
247
|
+
return chain;
|
|
248
|
+
}
|
|
249
|
+
function toSeconds(value) {
|
|
250
|
+
return typeof value === "number" && Number.isFinite(value) ? value : typeof value === "string" && /^\d+$/.test(value) ? Number(value) : void 0;
|
|
251
|
+
}
|
|
176
252
|
function toDate(value) {
|
|
177
253
|
const seconds3 = typeof value === "number" ? value : typeof value === "string" && /^\d+$/.test(value) ? Number(value) : void 0;
|
|
178
254
|
return seconds3 === void 0 ? void 0 : new Date(seconds3 * 1e3);
|
|
@@ -195,16 +271,19 @@ function distinctIgnoringCase(values) {
|
|
|
195
271
|
return true;
|
|
196
272
|
});
|
|
197
273
|
}
|
|
198
|
-
|
|
199
|
-
// src/rules.ts
|
|
200
274
|
function authorize(user, ...rules) {
|
|
201
275
|
const failures = rules.map((rule) => rule.check(user)).filter((failure) => failure !== void 0);
|
|
202
276
|
if (failures.length === 0) return void 0;
|
|
203
|
-
const
|
|
277
|
+
const stepUps = failures.filter((failure) => failure.error === "insufficient_user_authentication");
|
|
278
|
+
const bearer = stepUps[0] ?? failures.find((failure) => failure.error);
|
|
279
|
+
const acrValues = strongestAcr(stepUps.map((failure) => failure.acrValues));
|
|
280
|
+
const maxAges = stepUps.map((failure) => failure.maxAge).filter((value) => value !== void 0);
|
|
204
281
|
return {
|
|
205
282
|
message: failures.map((failure) => failure.message).join(" "),
|
|
206
283
|
...bearer?.error ? { error: bearer.error } : {},
|
|
207
|
-
...bearer?.scope ? { scope: bearer.scope } : {}
|
|
284
|
+
...bearer?.scope ? { scope: bearer.scope } : {},
|
|
285
|
+
...acrValues ? { acrValues } : {},
|
|
286
|
+
...maxAges.length > 0 ? { maxAge: Math.min(...maxAges) } : {}
|
|
208
287
|
};
|
|
209
288
|
}
|
|
210
289
|
function requireScope(scopes, match = "any") {
|
|
@@ -259,23 +338,60 @@ function requireOrganization(organization) {
|
|
|
259
338
|
}
|
|
260
339
|
};
|
|
261
340
|
}
|
|
262
|
-
function requireAuthenticationLevel(minimumLevel) {
|
|
341
|
+
function requireAuthenticationLevel(minimumLevel, options = {}) {
|
|
263
342
|
if (!Number.isInteger(minimumLevel) || minimumLevel < 0) throw new RangeError("The minimum level must be a non-negative integer.");
|
|
343
|
+
const recency = options.maxAge === void 0 ? void 0 : requireRecentAuthentication(options.maxAge, { clock: options.clock });
|
|
264
344
|
return {
|
|
265
|
-
description: `Authio authentication level >= ${minimumLevel}`,
|
|
266
|
-
check: (user) =>
|
|
345
|
+
description: `Authio authentication level >= ${minimumLevel}${recency ? `, ${recency.description}` : ""}`,
|
|
346
|
+
check: (user) => {
|
|
347
|
+
const levelMet = user.authenticationLevel !== void 0 && user.authenticationLevel >= minimumLevel;
|
|
348
|
+
const recent = recency?.check(user);
|
|
349
|
+
if (levelMet && !recent) return void 0;
|
|
350
|
+
return {
|
|
351
|
+
message: [levelMet ? void 0 : `The authentication level must be at least ${minimumLevel}.`, recent?.message].filter(Boolean).join(" "),
|
|
352
|
+
error: "insufficient_user_authentication",
|
|
353
|
+
acrValues: String(minimumLevel),
|
|
354
|
+
...options.maxAge !== void 0 ? { maxAge: options.maxAge } : {}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
267
357
|
};
|
|
268
358
|
}
|
|
269
|
-
|
|
359
|
+
var methodLevels = { mfa: "2", otp: "2", mca: "2", sms: "2", swk: "3", hwk: "3" };
|
|
360
|
+
function requireAuthenticationMethod(methods, match = "any", options = {}) {
|
|
270
361
|
const required = listOf(methods, "authentication method");
|
|
362
|
+
const levels = required.map((method) => methodLevels[method.toLowerCase()]).filter((level) => level !== void 0);
|
|
363
|
+
const acrValues = options.acrValues ?? (levels.length === 0 ? void 0 : match === "all" ? strongestAcr(levels) : weakestAcr(levels));
|
|
271
364
|
return {
|
|
272
365
|
description: `Authio authentication method (${match}): ${required.join(", ")}`,
|
|
273
366
|
check: (user) => {
|
|
274
367
|
const used = user.authenticationMethods.map((method) => method.toLowerCase());
|
|
275
|
-
return satisfies(required, match, (method) => used.includes(method.toLowerCase())) ? void 0 : {
|
|
368
|
+
return satisfies(required, match, (method) => used.includes(method.toLowerCase())) ? void 0 : {
|
|
369
|
+
message: `The user must authenticate with (${match}): ${required.join(", ")}.`,
|
|
370
|
+
error: "insufficient_user_authentication",
|
|
371
|
+
...acrValues ? { acrValues } : {}
|
|
372
|
+
};
|
|
276
373
|
}
|
|
277
374
|
};
|
|
278
375
|
}
|
|
376
|
+
function requireRecentAuthentication(maxAge, options = {}) {
|
|
377
|
+
if (!Number.isInteger(maxAge) || maxAge < 0) throw new RangeError("maxAge must be a non-negative whole number of seconds.");
|
|
378
|
+
const clock = options.clock ?? systemClock;
|
|
379
|
+
return {
|
|
380
|
+
description: `Authio sign-in at most ${maxAge} s old`,
|
|
381
|
+
check: (user) => user.authTime !== void 0 && clock.now() - user.authTime * 1e3 <= maxAge * 1e3 ? void 0 : {
|
|
382
|
+
message: user.authTime === void 0 ? `The token carries no sign-in time; a sign-in at most ${maxAge} seconds old is required.` : `The sign-in must be at most ${maxAge} seconds old.`,
|
|
383
|
+
error: "insufficient_user_authentication",
|
|
384
|
+
maxAge
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function requireCallerKind(kinds) {
|
|
389
|
+
const required = listOf(kinds, "caller kind");
|
|
390
|
+
return {
|
|
391
|
+
description: `Authio caller kind: ${required.join(", ")}`,
|
|
392
|
+
check: (user) => required.includes(user.kind) ? void 0 : { message: `The caller is a ${user.kind}, not one of: ${required.join(", ")}.` }
|
|
393
|
+
};
|
|
394
|
+
}
|
|
279
395
|
function requireClient(clientIds) {
|
|
280
396
|
const required = listOf(clientIds, "client id");
|
|
281
397
|
return {
|
|
@@ -307,24 +423,49 @@ function listOf(values, name) {
|
|
|
307
423
|
function satisfies(required, match, holds) {
|
|
308
424
|
return match === "all" ? required.every(holds) : required.some(holds);
|
|
309
425
|
}
|
|
426
|
+
function strongestAcr(values) {
|
|
427
|
+
return rankAcr(values, (left, right) => right - left);
|
|
428
|
+
}
|
|
429
|
+
function weakestAcr(values) {
|
|
430
|
+
return rankAcr(values, (left, right) => left - right);
|
|
431
|
+
}
|
|
432
|
+
function rankAcr(values, order) {
|
|
433
|
+
const known = values.filter((value) => !!value);
|
|
434
|
+
const numeric = known.filter((value) => /^\d+$/.test(value)).sort((left, right) => order(Number(left), Number(right)));
|
|
435
|
+
return numeric[0] ?? known[0];
|
|
436
|
+
}
|
|
310
437
|
function describeSources(sources) {
|
|
311
438
|
return sources && sources.length > 0 ? ` from ${[...new Set(sources)].join(", ")}` : "";
|
|
312
439
|
}
|
|
313
|
-
var AuthioBearerError = class extends AuthioError {
|
|
440
|
+
var AuthioBearerError = class _AuthioBearerError extends AuthioError {
|
|
314
441
|
status = 401;
|
|
315
442
|
/** `invalid_token`, or `undefined` when the request had no token at all (RFC 6750 §3.1). */
|
|
316
443
|
error;
|
|
317
444
|
/** Why the token was refused, safe to send to the caller. */
|
|
318
445
|
description;
|
|
446
|
+
/** Why the token was refused, as a stable code; `undefined` when there was no token or Authio could not be asked. */
|
|
447
|
+
reason;
|
|
448
|
+
/** The API's protected resource metadata URL (RFC 9728), sent as `resource_metadata` when configured. */
|
|
449
|
+
resourceMetadata;
|
|
319
450
|
constructor(description, options = {}) {
|
|
320
451
|
super(description ?? "A valid Authio access token is required.", { cause: options.cause });
|
|
321
452
|
this.error = "error" in options ? options.error : "invalid_token";
|
|
322
453
|
this.description = description === void 0 ? void 0 : toHeaderText(description);
|
|
454
|
+
this.reason = options.reason;
|
|
455
|
+
this.resourceMetadata = options.resourceMetadata;
|
|
456
|
+
}
|
|
457
|
+
/** The same refusal, pointing at the API's protected resource metadata. */
|
|
458
|
+
withResourceMetadata(resourceMetadata) {
|
|
459
|
+
if (!resourceMetadata || resourceMetadata === this.resourceMetadata) return this;
|
|
460
|
+
return new _AuthioBearerError(this.description, { error: this.error, cause: this.cause, reason: this.reason, resourceMetadata });
|
|
323
461
|
}
|
|
324
462
|
/** The `WWW-Authenticate` value. */
|
|
325
463
|
get wwwAuthenticate() {
|
|
326
|
-
|
|
327
|
-
|
|
464
|
+
const parameters = [];
|
|
465
|
+
if (this.error) parameters.push(`error="${this.error}"`);
|
|
466
|
+
if (this.error && this.description) parameters.push(`error_description="${this.description}"`);
|
|
467
|
+
if (this.resourceMetadata) parameters.push(`resource_metadata="${escapeQuoted(this.resourceMetadata)}"`);
|
|
468
|
+
return parameters.length === 0 ? "Bearer" : `Bearer ${parameters.join(", ")}`;
|
|
328
469
|
}
|
|
329
470
|
};
|
|
330
471
|
var AuthioForbiddenError = class extends AuthioError {
|
|
@@ -341,8 +482,11 @@ var AuthioForbiddenError = class extends AuthioError {
|
|
|
341
482
|
};
|
|
342
483
|
function toForbiddenChallenge(failure) {
|
|
343
484
|
if (!failure.error) return void 0;
|
|
344
|
-
const
|
|
345
|
-
|
|
485
|
+
const parameters = [`error="${failure.error}"`, `error_description="${escapeQuoted(toHeaderText(failure.message))}"`];
|
|
486
|
+
if (failure.scope) parameters.push(`scope="${escapeQuoted(failure.scope)}"`);
|
|
487
|
+
if (failure.acrValues) parameters.push(`acr_values="${escapeQuoted(failure.acrValues)}"`);
|
|
488
|
+
if (failure.maxAge !== void 0) parameters.push(`max_age=${failure.maxAge}`);
|
|
489
|
+
return `Bearer ${parameters.join(", ")}`;
|
|
346
490
|
}
|
|
347
491
|
function toProblemDetails(error) {
|
|
348
492
|
if (error instanceof AuthioBearerError)
|
|
@@ -358,7 +502,9 @@ function toProblemDetails(error) {
|
|
|
358
502
|
status: 403,
|
|
359
503
|
detail: error.failure.message || "The caller is not allowed to access this resource.",
|
|
360
504
|
...error.failure.error ? { error: error.failure.error } : {},
|
|
361
|
-
...error.failure.scope ? { scope: error.failure.scope } : {}
|
|
505
|
+
...error.failure.scope ? { scope: error.failure.scope } : {},
|
|
506
|
+
...error.failure.acrValues ? { acr_values: error.failure.acrValues } : {},
|
|
507
|
+
...error.failure.maxAge !== void 0 ? { max_age: error.failure.maxAge } : {}
|
|
362
508
|
};
|
|
363
509
|
}
|
|
364
510
|
function toHeaderText(text) {
|
|
@@ -367,17 +513,53 @@ function toHeaderText(text) {
|
|
|
367
513
|
function escapeQuoted(value) {
|
|
368
514
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
369
515
|
}
|
|
516
|
+
function protectedResourceMetadataUrl(resource) {
|
|
517
|
+
const url = parseResource(resource);
|
|
518
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
519
|
+
return `${url.origin}${AuthioDefaults.ProtectedResourceMetadataPath}${path}`;
|
|
520
|
+
}
|
|
521
|
+
function protectedResourceMetadataPath(resource) {
|
|
522
|
+
return new URL(protectedResourceMetadataUrl(resource)).pathname;
|
|
523
|
+
}
|
|
524
|
+
function createProtectedResourceMetadata(options) {
|
|
525
|
+
parseResource(options.resource);
|
|
526
|
+
const document = {
|
|
527
|
+
...options.extra,
|
|
528
|
+
resource: options.resource,
|
|
529
|
+
authorization_servers: [resolveIssuer(options)],
|
|
530
|
+
bearer_methods_supported: ["header"]
|
|
531
|
+
};
|
|
532
|
+
if (options.scopesSupported && options.scopesSupported.length > 0) document.scopes_supported = [...options.scopesSupported];
|
|
533
|
+
if (options.resourceName) document.resource_name = options.resourceName;
|
|
534
|
+
if (options.resourceDocumentation) document.resource_documentation = options.resourceDocumentation;
|
|
535
|
+
return document;
|
|
536
|
+
}
|
|
537
|
+
function parseResource(resource) {
|
|
538
|
+
let url;
|
|
539
|
+
try {
|
|
540
|
+
url = new URL(resource);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
throw new AuthioConfigurationError(`The resource '${resource}' is not an absolute URL.`, { cause: error });
|
|
543
|
+
}
|
|
544
|
+
if (url.hash) throw new AuthioConfigurationError(`The resource '${resource}' must not carry a fragment (RFC 8707).`);
|
|
545
|
+
return url;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/verifier.ts
|
|
370
549
|
var AuthioBearerErrorDescriptions = {
|
|
371
550
|
Inactive: "Authio reports the token as not active to this API's client. It was revoked or has expired or its session ended; or this API's client is not the introspection client of the resource server the token is for.",
|
|
372
551
|
Unconfirmed: "Authio could not be reached to confirm that the token is active.",
|
|
373
552
|
Empty: "The token is empty.",
|
|
374
|
-
Encrypted: "The token is encrypted; validate it with introspection ('introspectionOnly').",
|
|
553
|
+
Encrypted: "The token is encrypted; set clientEncryptionKey to decrypt it, or validate it with introspection ('introspectionOnly').",
|
|
375
554
|
SigningKeysUnavailable: "Authio could not be reached to read its signing keys."
|
|
376
555
|
};
|
|
377
556
|
var maxCachedAnswers = 1e4;
|
|
378
557
|
function createAuthioVerifier(options) {
|
|
379
|
-
const
|
|
380
|
-
|
|
558
|
+
const resource = options.resource?.trim() || void 0;
|
|
559
|
+
const given = typeof options.audiences === "string" ? [options.audiences] : [...options.audiences ?? []];
|
|
560
|
+
const audiences = [...new Set([...given, ...resource ? [resource] : []].filter((value) => value?.trim()))];
|
|
561
|
+
if (audiences.length === 0)
|
|
562
|
+
throw new AuthioConfigurationError("The Authio verifier needs at least one audience: the API's resource identifier or client id.");
|
|
381
563
|
const connection = resolveConnection(options);
|
|
382
564
|
const discovery = createAuthioDiscoveryClient(connection);
|
|
383
565
|
const introspection = options.introspection ?? "none";
|
|
@@ -386,13 +568,20 @@ function createAuthioVerifier(options) {
|
|
|
386
568
|
throw new AuthioConfigurationError(
|
|
387
569
|
`Authio introspection ('${introspection}') needs the API's confidential client: pass options.client with its clientId and clientSecret or private key.`
|
|
388
570
|
);
|
|
571
|
+
const resourceMetadataUrl = resolveResourceMetadata(options.resourceMetadata, resource);
|
|
389
572
|
const cacheDurationMs = seconds(options.introspectionCacheDuration, 30, "introspectionCacheDuration");
|
|
390
573
|
const clockSkew = seconds(options.clockSkew, 30, "clockSkew") / 1e3;
|
|
391
|
-
const algorithms = options.validAlgorithms && options.validAlgorithms.length > 0 ? [...options.validAlgorithms] :
|
|
574
|
+
const algorithms = options.validAlgorithms && options.validAlgorithms.length > 0 ? [...options.validAlgorithms] : void 0;
|
|
392
575
|
const rejectNonAccessTokens = options.rejectNonAccessTokens ?? true;
|
|
393
576
|
const claimSettings = { roleClaims: options.roleClaims, groupClaims: options.groupClaims };
|
|
394
577
|
const cache = /* @__PURE__ */ new Map();
|
|
395
|
-
|
|
578
|
+
const signingKeys = createAuthioSigningKeys(connection, discovery);
|
|
579
|
+
const encryptionKey = options.clientEncryptionKey ?? (options.clientEncryptionKeyPath ? encryptionKeyFromFile(options.clientEncryptionKeyPath) : void 0);
|
|
580
|
+
const decrypt = encryptionKey === void 0 ? void 0 : createAuthioTokenDecrypter(encryptionKey, {
|
|
581
|
+
setting: options.clientEncryptionKey === void 0 ? "clientEncryptionKeyPath" : "clientEncryptionKey",
|
|
582
|
+
allowLegacyEncryptionAlgorithms: options.allowLegacyEncryptionAlgorithms
|
|
583
|
+
});
|
|
584
|
+
const requiredScopes = Object.freeze([...new Set((options.requiredScopes ?? []).filter((scope) => scope?.trim()))]);
|
|
396
585
|
const introspect = async (token, key, signal) => {
|
|
397
586
|
const now = connection.clock.now();
|
|
398
587
|
const cached = cache.get(key);
|
|
@@ -407,37 +596,39 @@ function createAuthioVerifier(options) {
|
|
|
407
596
|
}
|
|
408
597
|
return result;
|
|
409
598
|
};
|
|
410
|
-
const signingKeys = async (signal) => {
|
|
411
|
-
const document = await discovery.get(signal);
|
|
412
|
-
const uri = document.jwks_uri;
|
|
413
|
-
if (!uri) throw new AuthioError("The Authio discovery document has no jwks_uri.");
|
|
414
|
-
if (!keys || keys.uri !== uri) keys = { uri, set: createRemoteJWKSet(new URL(uri), { [customFetch]: connection.fetch, timeoutDuration: connection.httpTimeoutMs }) };
|
|
415
|
-
return keys.set;
|
|
416
|
-
};
|
|
417
599
|
const verifyJwt = async (token, signal) => {
|
|
418
|
-
|
|
600
|
+
let signed = token;
|
|
601
|
+
if (isAuthioTokenEncrypted(token)) {
|
|
602
|
+
if (!decrypt) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Encrypted, { reason: "decryption_failed" });
|
|
603
|
+
try {
|
|
604
|
+
signed = await decrypt(token);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (error instanceof AuthioTokenValidationError) throw new AuthioBearerError(error.message, { cause: error, reason: error.reason });
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
419
610
|
let keySet;
|
|
420
611
|
try {
|
|
421
612
|
keySet = await signingKeys(signal);
|
|
422
613
|
} catch (error) {
|
|
423
|
-
throw new AuthioBearerError(AuthioBearerErrorDescriptions.SigningKeysUnavailable, { cause: error });
|
|
614
|
+
throw new AuthioBearerError(AuthioBearerErrorDescriptions.SigningKeysUnavailable, { cause: error, reason: "signing_keys_unavailable" });
|
|
424
615
|
}
|
|
425
616
|
let payload;
|
|
426
617
|
try {
|
|
427
|
-
({ payload } = await
|
|
618
|
+
({ payload } = await verifyAuthioJwt(signed, keySet, {
|
|
428
619
|
issuer: connection.issuer,
|
|
429
|
-
|
|
620
|
+
audiences,
|
|
430
621
|
algorithms,
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
currentDate: new Date(connection.clock.now())
|
|
622
|
+
clockSkew,
|
|
623
|
+
now: connection.clock.now()
|
|
434
624
|
}));
|
|
435
625
|
} catch (error) {
|
|
436
|
-
throw new AuthioBearerError(
|
|
626
|
+
if (error instanceof AuthioTokenValidationError) throw new AuthioBearerError(error.message, { cause: error, reason: error.reason });
|
|
627
|
+
throw error;
|
|
437
628
|
}
|
|
438
629
|
const type = typeof payload.typ === "string" ? payload.typ : void 0;
|
|
439
630
|
if (rejectNonAccessTokens && type !== void 0 && type.toLowerCase() !== "bearer")
|
|
440
|
-
throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'
|
|
631
|
+
throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'.`, { reason: "not_access_token" });
|
|
441
632
|
if (introspection === "activeCheck") await confirmActive(token, payload, signal);
|
|
442
633
|
return new AuthioUser(payload, claimSettings, token);
|
|
443
634
|
};
|
|
@@ -448,9 +639,9 @@ function createAuthioVerifier(options) {
|
|
|
448
639
|
result = await introspect(token, key, signal);
|
|
449
640
|
} catch (error) {
|
|
450
641
|
if (signal?.aborted) throw error;
|
|
451
|
-
throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error });
|
|
642
|
+
throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error, reason: "unconfirmed" });
|
|
452
643
|
}
|
|
453
|
-
if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive);
|
|
644
|
+
if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive, { reason: "inactive" });
|
|
454
645
|
};
|
|
455
646
|
const verifyByIntrospection = async (token, signal) => {
|
|
456
647
|
let result;
|
|
@@ -458,38 +649,42 @@ function createAuthioVerifier(options) {
|
|
|
458
649
|
result = await introspect(token, await keyForToken(token), signal);
|
|
459
650
|
} catch (error) {
|
|
460
651
|
if (signal?.aborted) throw error;
|
|
461
|
-
throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error });
|
|
652
|
+
throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error, reason: "unconfirmed" });
|
|
462
653
|
}
|
|
463
|
-
if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive);
|
|
464
|
-
if (result.iss && result.iss !== connection.issuer) throw new AuthioBearerError(`The issuer '${result.iss}' is invalid
|
|
654
|
+
if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive, { reason: "inactive" });
|
|
655
|
+
if (result.iss && result.iss !== connection.issuer) throw new AuthioBearerError(`The issuer '${result.iss}' is invalid`, { reason: "invalid_issuer" });
|
|
465
656
|
const tokenAudiences = [result.aud ?? []].flat();
|
|
466
|
-
if (!tokenAudiences.some((audience) => audiences.includes(audience)))
|
|
657
|
+
if (!tokenAudiences.some((audience) => audiences.includes(audience)))
|
|
658
|
+
throw new AuthioBearerError(describeAudience(tokenAudiences, audiences), { reason: "invalid_audience" });
|
|
467
659
|
if (typeof result.exp === "number" && (result.exp + clockSkew) * 1e3 <= connection.clock.now())
|
|
468
|
-
throw new AuthioBearerError(`The token expired at '${new Date(result.exp * 1e3).toISOString()}'
|
|
469
|
-
const
|
|
470
|
-
const claims = { ...members };
|
|
471
|
-
if (typeof tokenType === "string" && claims.typ === void 0) claims.typ = tokenType;
|
|
472
|
-
if (typeof username === "string" && claims.preferred_username === void 0) claims.preferred_username = username;
|
|
473
|
-
if (typeof result.client_id === "string" && claims.azp === void 0) claims.azp = result.client_id;
|
|
660
|
+
throw new AuthioBearerError(`The token expired at '${new Date(result.exp * 1e3).toISOString()}'`, { reason: "expired" });
|
|
661
|
+
const claims = claimsOfIntrospection(result);
|
|
474
662
|
const type = typeof claims.typ === "string" ? claims.typ : void 0;
|
|
475
|
-
if (rejectNonAccessTokens && type !== void 0 && type
|
|
476
|
-
throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'
|
|
477
|
-
return new AuthioUser(claims, claimSettings, token);
|
|
663
|
+
if (rejectNonAccessTokens && type !== void 0 && !isAccessTokenType(type))
|
|
664
|
+
throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'.`, { reason: "not_access_token" });
|
|
665
|
+
return new AuthioUser(claims, claimSettings, token, "introspection");
|
|
478
666
|
};
|
|
479
667
|
const verify = async (token, signal) => {
|
|
480
|
-
|
|
481
|
-
|
|
668
|
+
try {
|
|
669
|
+
if (typeof token !== "string" || !token.trim()) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Empty, { reason: "malformed" });
|
|
670
|
+
return introspection === "introspectionOnly" ? await verifyByIntrospection(token.trim(), signal) : await verifyJwt(token.trim(), signal);
|
|
671
|
+
} catch (error) {
|
|
672
|
+
throw error instanceof AuthioBearerError ? error.withResourceMetadata(resourceMetadataUrl) : error;
|
|
673
|
+
}
|
|
482
674
|
};
|
|
483
675
|
return {
|
|
484
676
|
connection,
|
|
485
677
|
discovery,
|
|
486
678
|
audiences,
|
|
679
|
+
resource,
|
|
680
|
+
resourceMetadataUrl,
|
|
681
|
+
requiredScopes,
|
|
487
682
|
introspection,
|
|
488
683
|
client,
|
|
489
684
|
verify,
|
|
490
685
|
async verifyAuthorizationHeader(header, signal) {
|
|
491
686
|
const token = readBearerToken(header);
|
|
492
|
-
if (token === void 0) throw new AuthioBearerError(void 0, { error: void 0 });
|
|
687
|
+
if (token === void 0) throw new AuthioBearerError(void 0, { error: void 0, resourceMetadata: resourceMetadataUrl });
|
|
493
688
|
return verify(token, signal);
|
|
494
689
|
}
|
|
495
690
|
};
|
|
@@ -502,6 +697,13 @@ async function keyForToken(token) {
|
|
|
502
697
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token)));
|
|
503
698
|
return `token:${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
504
699
|
}
|
|
700
|
+
function resolveResourceMetadata(setting, resource) {
|
|
701
|
+
if (!setting) return void 0;
|
|
702
|
+
if (typeof setting === "string") return setting;
|
|
703
|
+
if (!resource)
|
|
704
|
+
throw new AuthioConfigurationError("resourceMetadata: true needs the API's resource identifier: set options.resource, or pass the metadata URL.");
|
|
705
|
+
return protectedResourceMetadataUrl(resource);
|
|
706
|
+
}
|
|
505
707
|
function resolveClient(options) {
|
|
506
708
|
const given = options.client;
|
|
507
709
|
if (!given) return void 0;
|
|
@@ -521,44 +723,11 @@ function resolveClient(options) {
|
|
|
521
723
|
function isClient(value) {
|
|
522
724
|
return typeof value.introspect === "function" && typeof value.tokens === "object";
|
|
523
725
|
}
|
|
524
|
-
function describeJwtFailure(error, audiences, token) {
|
|
525
|
-
if (error instanceof errors.JWTExpired) {
|
|
526
|
-
const exp = error.payload.exp;
|
|
527
|
-
return typeof exp === "number" ? `The token expired at '${new Date(exp * 1e3).toISOString()}'` : "The token has expired";
|
|
528
|
-
}
|
|
529
|
-
if (error instanceof errors.JWTClaimValidationFailed) {
|
|
530
|
-
switch (error.claim) {
|
|
531
|
-
case "aud":
|
|
532
|
-
return describeAudience([error.payload.aud ?? []].flat(), audiences);
|
|
533
|
-
case "iss":
|
|
534
|
-
return `The issuer '${error.payload.iss ?? ""}' is invalid`;
|
|
535
|
-
case "nbf":
|
|
536
|
-
return typeof error.payload.nbf === "number" ? `The token is not valid before '${new Date(error.payload.nbf * 1e3).toISOString()}'` : "The token is not valid yet";
|
|
537
|
-
case "exp":
|
|
538
|
-
return "The token has no expiration";
|
|
539
|
-
default:
|
|
540
|
-
return `The token's ${error.claim} claim is invalid`;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
if (error instanceof errors.JWSSignatureVerificationFailed) return "The signature is invalid";
|
|
544
|
-
if (error instanceof errors.JWKSNoMatchingKey) return "The signature key was not found";
|
|
545
|
-
if (error instanceof errors.JOSEAlgNotAllowed) return `The algorithm '${algorithmOf(token)}' is not available`;
|
|
546
|
-
if (error instanceof errors.JWKSTimeout || error instanceof errors.JWKSInvalid) return AuthioBearerErrorDescriptions.SigningKeysUnavailable;
|
|
547
|
-
if (error instanceof errors.JWTInvalid || error instanceof errors.JWSInvalid) return "The token is malformed";
|
|
548
|
-
return void 0;
|
|
549
|
-
}
|
|
550
726
|
function describeAudience(tokenAudiences, audiences) {
|
|
551
727
|
const known = tokenAudiences.length === 0 ? "" : ` ${tokenAudiences.map((audience) => `'${audience}'`).join(" and ")}`;
|
|
552
728
|
const accepted = audiences.map((audience) => `'${audience}'`).join(" or ");
|
|
553
729
|
return audiences.length === 1 ? `The token is not addressed to this API: its audience${known} is not ${accepted}. Request the token for that resource (RFC 8707).` : `The token is not addressed to this API: its audience${known} is not ${accepted}. Request the token for one of those resources (RFC 8707).`;
|
|
554
730
|
}
|
|
555
|
-
function algorithmOf(token) {
|
|
556
|
-
try {
|
|
557
|
-
return String(decodeProtectedHeader(token).alg ?? "");
|
|
558
|
-
} catch {
|
|
559
|
-
return "";
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
731
|
function createAuthioWebhooks(options) {
|
|
563
732
|
const secrets = [...new Set([options.secret, ...options.secrets ?? []].filter((secret) => !!secret?.trim()))];
|
|
564
733
|
if (secrets.length === 0) throw new AuthioConfigurationError("Authio webhooks need the webhook's secret.");
|
|
@@ -709,6 +878,6 @@ function tokenOf(subject) {
|
|
|
709
878
|
return typeof subject === "string" ? subject : subject.token;
|
|
710
879
|
}
|
|
711
880
|
|
|
712
|
-
export { AuthioBearerError, AuthioBearerErrorDescriptions, AuthioForbiddenError, AuthioUser, allOf, anyOf, authorize, createAuthioVerifier, createAuthioWebhooks, createForwardingFetch, createMemoryDeduplicationStore, createOnBehalfOf, readBearerToken, requireAuthenticationLevel, requireAuthenticationMethod, requireClaim, requireClient, requireGroup, requireOrganization, requirePermission, requireRole, requireScope, toForbiddenChallenge, toProblemDetails };
|
|
881
|
+
export { AuthioBearerError, AuthioBearerErrorDescriptions, AuthioForbiddenError, AuthioUser, allOf, anyOf, authorize, createAuthioVerifier, createAuthioWebhooks, createForwardingFetch, createMemoryDeduplicationStore, createOnBehalfOf, createProtectedResourceMetadata, protectedResourceMetadataPath, protectedResourceMetadataUrl, readBearerToken, requireAuthenticationLevel, requireAuthenticationMethod, requireCallerKind, requireClaim, requireClient, requireGroup, requireOrganization, requirePermission, requireRecentAuthentication, requireRole, requireScope, toForbiddenChallenge, toProblemDetails };
|
|
713
882
|
//# sourceMappingURL=index.js.map
|
|
714
883
|
//# sourceMappingURL=index.js.map
|