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