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