@hyper.authio/server 0.0.0-stage → 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 +144 -2
- package/dist/index.cjs +913 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +588 -0
- package/dist/index.d.ts +588 -0
- package/dist/index.js +883 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var client = require('@hyper.authio/client');
|
|
4
|
+
|
|
5
|
+
// src/user.ts
|
|
6
|
+
var accessTokenTypes = /* @__PURE__ */ new Set(["bearer", "api_key"]);
|
|
7
|
+
var AuthioUser = class _AuthioUser {
|
|
8
|
+
/** `sub`: the person's id, or the credential's for a machine caller. */
|
|
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;
|
|
45
|
+
/** The client the token was issued to (`azp`, else `client_id`). */
|
|
46
|
+
clientId;
|
|
47
|
+
/** `sid`. */
|
|
48
|
+
sessionId;
|
|
49
|
+
/** `jti`. */
|
|
50
|
+
tokenId;
|
|
51
|
+
/** `aud`. */
|
|
52
|
+
audiences;
|
|
53
|
+
/** The granted scopes. */
|
|
54
|
+
scopes;
|
|
55
|
+
/** The roles of the seven `*_resource_access` claims, with their source and permissions. */
|
|
56
|
+
resourceAccess;
|
|
57
|
+
/** Every role name: the `*_resource_access` roles, then the flat role claims. */
|
|
58
|
+
roles;
|
|
59
|
+
/** Resource → every method any role allows on it. */
|
|
60
|
+
permissions;
|
|
61
|
+
/** The group names of the group claims. */
|
|
62
|
+
groups;
|
|
63
|
+
/** `org_id`. */
|
|
64
|
+
organizationId;
|
|
65
|
+
/** `org_alias`. */
|
|
66
|
+
organizationAlias;
|
|
67
|
+
/** `account_id`. */
|
|
68
|
+
accountId;
|
|
69
|
+
/** `acr`. */
|
|
70
|
+
acr;
|
|
71
|
+
/** `acr` as a number: 1 one factor, 2 several, 3 one of them phishing-resistant. */
|
|
72
|
+
authenticationLevel;
|
|
73
|
+
/** `amr`. */
|
|
74
|
+
authenticationMethods;
|
|
75
|
+
/** `idp`. */
|
|
76
|
+
identityProvider;
|
|
77
|
+
/** `auth_time`. */
|
|
78
|
+
authenticatedAt;
|
|
79
|
+
/** `act`. */
|
|
80
|
+
actor;
|
|
81
|
+
/** `delegation_id`. */
|
|
82
|
+
delegationId;
|
|
83
|
+
email;
|
|
84
|
+
emailVerified;
|
|
85
|
+
name;
|
|
86
|
+
preferredUsername;
|
|
87
|
+
/** `credential_type`, on a token issued to an account credential or a consumer. */
|
|
88
|
+
credentialType;
|
|
89
|
+
/** `credential_id`. */
|
|
90
|
+
credentialId;
|
|
91
|
+
/** Every claim, as the token or the introspection answer carried it. */
|
|
92
|
+
claims;
|
|
93
|
+
flatRoles;
|
|
94
|
+
/**
|
|
95
|
+
* @param claims The token's payload, or the introspection answer's members.
|
|
96
|
+
* @param settings Where flat roles and groups are read from.
|
|
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`.
|
|
101
|
+
*/
|
|
102
|
+
constructor(claims, settings = {}, token, source = "token") {
|
|
103
|
+
const text = (name) => typeof claims[name] === "string" && claims[name] ? claims[name] : void 0;
|
|
104
|
+
this.claims = Object.freeze({ ...claims });
|
|
105
|
+
this.subject = text(client.AuthioClaimTypes.Subject) ?? "";
|
|
106
|
+
this.clientId = text(client.AuthioClaimTypes.AuthorizedParty) ?? text(client.AuthioClaimTypes.ClientId);
|
|
107
|
+
this.sessionId = text(client.AuthioClaimTypes.SessionId);
|
|
108
|
+
this.tokenId = text(client.AuthioClaimTypes.JwtId);
|
|
109
|
+
this.audiences = Object.freeze(distinct(valuesOf(claims[client.AuthioClaimTypes.Audience])));
|
|
110
|
+
this.scopes = Object.freeze(distinct(valuesOf(claims[client.AuthioClaimTypes.Scope]).flatMap((scope) => scope.split(/\s+/)).filter(Boolean)));
|
|
111
|
+
this.resourceAccess = client.parseResourceAccess(claims);
|
|
112
|
+
this.flatRoles = Object.freeze((settings.roleClaims ?? ["role"]).flatMap((name) => valuesOf(claims[name])));
|
|
113
|
+
this.roles = Object.freeze(distinctIgnoringCase([...this.resourceAccess.roles, ...this.flatRoles]));
|
|
114
|
+
this.permissions = this.resourceAccess.permissions;
|
|
115
|
+
this.groups = Object.freeze(distinct((settings.groupClaims ?? ["groups"]).flatMap((name) => valuesOf(claims[name]))));
|
|
116
|
+
this.organizationId = text(client.AuthioClaimTypes.OrganizationId);
|
|
117
|
+
this.organizationAlias = text(client.AuthioClaimTypes.OrganizationAlias);
|
|
118
|
+
this.accountId = text(client.AuthioClaimTypes.AccountId);
|
|
119
|
+
this.acr = valuesOf(claims[client.AuthioClaimTypes.AuthenticationContextClass])[0];
|
|
120
|
+
this.authenticationLevel = this.acr !== void 0 && /^\d+$/.test(this.acr) ? Number(this.acr) : void 0;
|
|
121
|
+
this.authenticationMethods = Object.freeze(distinctIgnoringCase(valuesOf(claims[client.AuthioClaimTypes.AuthenticationMethods])));
|
|
122
|
+
this.identityProvider = text(client.AuthioClaimTypes.IdentityProvider);
|
|
123
|
+
this.authenticatedAt = toDate(claims[client.AuthioClaimTypes.AuthTime]);
|
|
124
|
+
this.actor = readActor(claims[client.AuthioClaimTypes.Actor]);
|
|
125
|
+
this.delegationId = text(client.AuthioClaimTypes.DelegationId);
|
|
126
|
+
this.email = text(client.AuthioClaimTypes.Email);
|
|
127
|
+
this.emailVerified = toBoolean(claims[client.AuthioClaimTypes.EmailVerified]);
|
|
128
|
+
this.name = text(client.AuthioClaimTypes.Name);
|
|
129
|
+
this.preferredUsername = text(client.AuthioClaimTypes.PreferredUsername);
|
|
130
|
+
this.credentialType = text(client.AuthioClaimTypes.CredentialType);
|
|
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;
|
|
147
|
+
Object.defineProperty(this, "token", { value: token, enumerable: false });
|
|
148
|
+
}
|
|
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");
|
|
155
|
+
}
|
|
156
|
+
/** Whether the scope was granted. */
|
|
157
|
+
hasScope(scope) {
|
|
158
|
+
return this.scopes.includes(scope);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Whether the user holds the role. Without sources, from any `*_resource_access` claim or a flat role claim; with
|
|
162
|
+
* sources, only from those `*_resource_access` claims.
|
|
163
|
+
*/
|
|
164
|
+
hasRole(role, options = {}) {
|
|
165
|
+
if (!role) return false;
|
|
166
|
+
if (options.sources && options.sources.length > 0) return this.resourceAccess.hasRole(role, options.sources);
|
|
167
|
+
const wanted = role.toLowerCase();
|
|
168
|
+
return this.resourceAccess.hasRole(role) || this.flatRoles.some((name) => name.toLowerCase() === wanted);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Authio's grant rule: whether a role (from any source, or only from the given ones) allows `method` on
|
|
172
|
+
* `resource`. Case-insensitive.
|
|
173
|
+
*/
|
|
174
|
+
hasPermission(resource, method, options = {}) {
|
|
175
|
+
return this.resourceAccess.grants(resource, method, options.sources);
|
|
176
|
+
}
|
|
177
|
+
/** Whether the user is in the group (exact match). */
|
|
178
|
+
hasGroup(group) {
|
|
179
|
+
return this.groups.includes(group);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Whether the claim is present (not `null`), and, given values, whether it holds one of them (exact match; an
|
|
183
|
+
* array claim matches when any item does).
|
|
184
|
+
*/
|
|
185
|
+
hasClaim(type, ...values) {
|
|
186
|
+
const present = this.claimValues(type);
|
|
187
|
+
return values.length === 0 ? present.length > 0 : present.some((value) => values.includes(value));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The claim's values as text: one for a scalar, one per item for an array, JSON text for an object.
|
|
191
|
+
*/
|
|
192
|
+
claimValues(type) {
|
|
193
|
+
return valuesOf(this.claims[type]);
|
|
194
|
+
}
|
|
195
|
+
/** Every member but the raw token. */
|
|
196
|
+
toJSON() {
|
|
197
|
+
const { flatRoles: _flatRoles, ...members } = this;
|
|
198
|
+
return { ...members, resourceAccess: this.resourceAccess.entries };
|
|
199
|
+
}
|
|
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
|
+
}
|
|
212
|
+
function valuesOf(value) {
|
|
213
|
+
if (value === void 0 || value === null) return [];
|
|
214
|
+
if (Array.isArray(value)) return value.flatMap((item) => item === null || item === void 0 ? [] : [toText(item)]);
|
|
215
|
+
if (typeof value === "string" && value.startsWith("[")) {
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(value);
|
|
218
|
+
if (Array.isArray(parsed)) return valuesOf(parsed);
|
|
219
|
+
} catch {
|
|
220
|
+
return [value];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return [toText(value)];
|
|
224
|
+
}
|
|
225
|
+
function toText(value) {
|
|
226
|
+
return typeof value === "string" ? value : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
227
|
+
}
|
|
228
|
+
function readActor(value, depth = 0) {
|
|
229
|
+
let actor = value;
|
|
230
|
+
if (typeof actor === "string") {
|
|
231
|
+
try {
|
|
232
|
+
actor = JSON.parse(actor);
|
|
233
|
+
} catch {
|
|
234
|
+
return void 0;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!actor || typeof actor !== "object" || Array.isArray(actor) || depth > 16) return void 0;
|
|
238
|
+
const record = actor;
|
|
239
|
+
return Object.freeze({
|
|
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,
|
|
243
|
+
actor: readActor(record.act, depth + 1)
|
|
244
|
+
});
|
|
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
|
+
}
|
|
254
|
+
function toDate(value) {
|
|
255
|
+
const seconds3 = typeof value === "number" ? value : typeof value === "string" && /^\d+$/.test(value) ? Number(value) : void 0;
|
|
256
|
+
return seconds3 === void 0 ? void 0 : new Date(seconds3 * 1e3);
|
|
257
|
+
}
|
|
258
|
+
function toBoolean(value) {
|
|
259
|
+
if (typeof value === "boolean") return value;
|
|
260
|
+
if (value === "true") return true;
|
|
261
|
+
if (value === "false") return false;
|
|
262
|
+
return void 0;
|
|
263
|
+
}
|
|
264
|
+
function distinct(values) {
|
|
265
|
+
return [...new Set(values)];
|
|
266
|
+
}
|
|
267
|
+
function distinctIgnoringCase(values) {
|
|
268
|
+
const seen = /* @__PURE__ */ new Set();
|
|
269
|
+
return values.filter((value) => {
|
|
270
|
+
const key = value.toLowerCase();
|
|
271
|
+
if (seen.has(key)) return false;
|
|
272
|
+
seen.add(key);
|
|
273
|
+
return true;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function authorize(user, ...rules) {
|
|
277
|
+
const failures = rules.map((rule) => rule.check(user)).filter((failure) => failure !== void 0);
|
|
278
|
+
if (failures.length === 0) return void 0;
|
|
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);
|
|
283
|
+
return {
|
|
284
|
+
message: failures.map((failure) => failure.message).join(" "),
|
|
285
|
+
...bearer?.error ? { error: bearer.error } : {},
|
|
286
|
+
...bearer?.scope ? { scope: bearer.scope } : {},
|
|
287
|
+
...acrValues ? { acrValues } : {},
|
|
288
|
+
...maxAges.length > 0 ? { maxAge: Math.min(...maxAges) } : {}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function requireScope(scopes, match = "any") {
|
|
292
|
+
const required = listOf(scopes, "scope");
|
|
293
|
+
return {
|
|
294
|
+
description: `Authio scope (${match}): ${required.join(" ")}`,
|
|
295
|
+
check: (user) => satisfies(required, match, (scope) => user.hasScope(scope)) ? void 0 : {
|
|
296
|
+
message: `The token lacks the required scope (${match}): ${required.join(" ")}.`,
|
|
297
|
+
error: "insufficient_scope",
|
|
298
|
+
scope: required.join(" ")
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function requireRole(roles, options = {}) {
|
|
303
|
+
const required = listOf(roles, "role");
|
|
304
|
+
const match = options.match ?? "any";
|
|
305
|
+
const from = describeSources(options.sources);
|
|
306
|
+
return {
|
|
307
|
+
description: `Authio role (${match}): ${required.join(", ")}${from}`,
|
|
308
|
+
check: (user) => satisfies(required, match, (role) => user.hasRole(role, { sources: options.sources })) ? void 0 : { message: `The caller lacks the required role (${match}): ${required.join(", ")}${from}.` }
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function requirePermission(resource, method, options = {}) {
|
|
312
|
+
if (!resource?.trim() || !method?.trim()) throw new TypeError("A permission needs a resource and a method.");
|
|
313
|
+
const from = describeSources(options.sources);
|
|
314
|
+
return {
|
|
315
|
+
description: `Authio permission: ${resource}:${method}${from}`,
|
|
316
|
+
check: (user) => user.hasPermission(resource, method, { sources: options.sources }) ? void 0 : { message: `The caller lacks the permission ${resource}:${method}${from}.` }
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function requireGroup(groups, match = "any") {
|
|
320
|
+
const required = listOf(groups, "group");
|
|
321
|
+
return {
|
|
322
|
+
description: `Authio group (${match}): ${required.join(", ")}`,
|
|
323
|
+
check: (user) => satisfies(required, match, (group) => user.hasGroup(group)) ? void 0 : { message: `The caller is not in the required group (${match}): ${required.join(", ")}.` }
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function requireClaim(type, ...values) {
|
|
327
|
+
if (!type?.trim()) throw new TypeError("A claim rule needs a claim type.");
|
|
328
|
+
return {
|
|
329
|
+
description: values.length === 0 ? `Authio claim: ${type}` : `Authio claim: ${type} in ${values.join(", ")}`,
|
|
330
|
+
check: (user) => user.hasClaim(type, ...values) ? void 0 : { message: values.length === 0 ? `The token lacks the claim ${type}.` : `The token's claim ${type} is not one of: ${values.join(", ")}.` }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function requireOrganization(organization) {
|
|
334
|
+
const wanted = organization?.trim() || void 0;
|
|
335
|
+
return {
|
|
336
|
+
description: `Authio organization: ${wanted ?? "any"}`,
|
|
337
|
+
check: (user) => {
|
|
338
|
+
const satisfied = !!user.organizationId && (!wanted || user.organizationAlias?.toLowerCase() === wanted.toLowerCase() || user.organizationId.toLowerCase() === wanted.toLowerCase());
|
|
339
|
+
return satisfied ? void 0 : { message: wanted ? `The token is not in the organization ${wanted}.` : "The token has no organization context." };
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function requireAuthenticationLevel(minimumLevel, options = {}) {
|
|
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 });
|
|
346
|
+
return {
|
|
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
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
var methodLevels = { mfa: "2", otp: "2", mca: "2", sms: "2", swk: "3", hwk: "3" };
|
|
362
|
+
function requireAuthenticationMethod(methods, match = "any", options = {}) {
|
|
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));
|
|
366
|
+
return {
|
|
367
|
+
description: `Authio authentication method (${match}): ${required.join(", ")}`,
|
|
368
|
+
check: (user) => {
|
|
369
|
+
const used = user.authenticationMethods.map((method) => method.toLowerCase());
|
|
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
|
+
};
|
|
375
|
+
}
|
|
376
|
+
};
|
|
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
|
+
}
|
|
397
|
+
function requireClient(clientIds) {
|
|
398
|
+
const required = listOf(clientIds, "client id");
|
|
399
|
+
return {
|
|
400
|
+
description: `Authio client: ${required.join(", ")}`,
|
|
401
|
+
check: (user) => user.clientId !== void 0 && required.includes(user.clientId) ? void 0 : { message: `The token was not issued to an accepted client (${required.join(", ")}).` }
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function allOf(...rules) {
|
|
405
|
+
return {
|
|
406
|
+
description: rules.map((rule) => rule.description).join(" and "),
|
|
407
|
+
check: (user) => authorize(user, ...rules)
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function anyOf(...rules) {
|
|
411
|
+
if (rules.length === 0) throw new TypeError("anyOf needs at least one rule.");
|
|
412
|
+
return {
|
|
413
|
+
description: rules.map((rule) => rule.description).join(" or "),
|
|
414
|
+
check: (user) => {
|
|
415
|
+
const failures = rules.map((rule) => rule.check(user));
|
|
416
|
+
return failures.some((failure) => failure === void 0) ? void 0 : failures[0];
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function listOf(values, name) {
|
|
421
|
+
const list = [...new Set((typeof values === "string" ? [values] : [...values]).filter((value) => value?.trim()))];
|
|
422
|
+
if (list.length === 0) throw new TypeError(`At least one ${name} is required.`);
|
|
423
|
+
return list;
|
|
424
|
+
}
|
|
425
|
+
function satisfies(required, match, holds) {
|
|
426
|
+
return match === "all" ? required.every(holds) : required.some(holds);
|
|
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
|
+
}
|
|
439
|
+
function describeSources(sources) {
|
|
440
|
+
return sources && sources.length > 0 ? ` from ${[...new Set(sources)].join(", ")}` : "";
|
|
441
|
+
}
|
|
442
|
+
var AuthioBearerError = class _AuthioBearerError extends client.AuthioError {
|
|
443
|
+
status = 401;
|
|
444
|
+
/** `invalid_token`, or `undefined` when the request had no token at all (RFC 6750 §3.1). */
|
|
445
|
+
error;
|
|
446
|
+
/** Why the token was refused, safe to send to the caller. */
|
|
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;
|
|
452
|
+
constructor(description, options = {}) {
|
|
453
|
+
super(description ?? "A valid Authio access token is required.", { cause: options.cause });
|
|
454
|
+
this.error = "error" in options ? options.error : "invalid_token";
|
|
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 });
|
|
463
|
+
}
|
|
464
|
+
/** The `WWW-Authenticate` value. */
|
|
465
|
+
get wwwAuthenticate() {
|
|
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(", ")}`;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
var AuthioForbiddenError = class extends client.AuthioError {
|
|
474
|
+
status = 403;
|
|
475
|
+
failure;
|
|
476
|
+
constructor(failure) {
|
|
477
|
+
super(failure.message);
|
|
478
|
+
this.failure = failure;
|
|
479
|
+
}
|
|
480
|
+
/** The `WWW-Authenticate` value, or `undefined` when the refusal is not about scopes or the sign-in. */
|
|
481
|
+
get wwwAuthenticate() {
|
|
482
|
+
return toForbiddenChallenge(this.failure);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
function toForbiddenChallenge(failure) {
|
|
486
|
+
if (!failure.error) return void 0;
|
|
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(", ")}`;
|
|
492
|
+
}
|
|
493
|
+
function toProblemDetails(error) {
|
|
494
|
+
if (error instanceof AuthioBearerError)
|
|
495
|
+
return {
|
|
496
|
+
type: "https://tools.ietf.org/html/rfc9110#section-15.5.2",
|
|
497
|
+
title: "Unauthorized",
|
|
498
|
+
status: 401,
|
|
499
|
+
detail: "A valid Authio access token is required."
|
|
500
|
+
};
|
|
501
|
+
return {
|
|
502
|
+
type: "https://tools.ietf.org/html/rfc9110#section-15.5.4",
|
|
503
|
+
title: "Forbidden",
|
|
504
|
+
status: 403,
|
|
505
|
+
detail: error.failure.message || "The caller is not allowed to access this resource.",
|
|
506
|
+
...error.failure.error ? { error: error.failure.error } : {},
|
|
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 } : {}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function toHeaderText(text) {
|
|
513
|
+
return [...text].map((character) => character >= " " && character <= "~" && character !== '"' && character !== "\\" ? character : "?").join("");
|
|
514
|
+
}
|
|
515
|
+
function escapeQuoted(value) {
|
|
516
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
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
|
|
551
|
+
var AuthioBearerErrorDescriptions = {
|
|
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.",
|
|
553
|
+
Unconfirmed: "Authio could not be reached to confirm that the token is active.",
|
|
554
|
+
Empty: "The token is empty.",
|
|
555
|
+
Encrypted: "The token is encrypted; set clientEncryptionKey to decrypt it, or validate it with introspection ('introspectionOnly').",
|
|
556
|
+
SigningKeysUnavailable: "Authio could not be reached to read its signing keys."
|
|
557
|
+
};
|
|
558
|
+
var maxCachedAnswers = 1e4;
|
|
559
|
+
function createAuthioVerifier(options) {
|
|
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.");
|
|
565
|
+
const connection = client.resolveConnection(options);
|
|
566
|
+
const discovery = client.createAuthioDiscoveryClient(connection);
|
|
567
|
+
const introspection = options.introspection ?? "none";
|
|
568
|
+
const client$1 = resolveClient(options);
|
|
569
|
+
if (introspection !== "none" && !client$1)
|
|
570
|
+
throw new client.AuthioConfigurationError(
|
|
571
|
+
`Authio introspection ('${introspection}') needs the API's confidential client: pass options.client with its clientId and clientSecret or private key.`
|
|
572
|
+
);
|
|
573
|
+
const resourceMetadataUrl = resolveResourceMetadata(options.resourceMetadata, resource);
|
|
574
|
+
const cacheDurationMs = client.seconds(options.introspectionCacheDuration, 30, "introspectionCacheDuration");
|
|
575
|
+
const clockSkew = client.seconds(options.clockSkew, 30, "clockSkew") / 1e3;
|
|
576
|
+
const algorithms = options.validAlgorithms && options.validAlgorithms.length > 0 ? [...options.validAlgorithms] : void 0;
|
|
577
|
+
const rejectNonAccessTokens = options.rejectNonAccessTokens ?? true;
|
|
578
|
+
const claimSettings = { roleClaims: options.roleClaims, groupClaims: options.groupClaims };
|
|
579
|
+
const cache = /* @__PURE__ */ new Map();
|
|
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()))]);
|
|
587
|
+
const introspect = async (token, key, signal) => {
|
|
588
|
+
const now = connection.clock.now();
|
|
589
|
+
const cached = cache.get(key);
|
|
590
|
+
if (cached && cached.until > now) return cached.result;
|
|
591
|
+
cache.delete(key);
|
|
592
|
+
const result = await client$1.introspect(token, { tokenTypeHint: client.AuthioTokenTypes.AccessToken, signal });
|
|
593
|
+
if (!result.active) return void 0;
|
|
594
|
+
const until = Math.min(now + cacheDurationMs, typeof result.exp === "number" ? result.exp * 1e3 : Number.POSITIVE_INFINITY);
|
|
595
|
+
if (until > now) {
|
|
596
|
+
if (cache.size >= maxCachedAnswers) cache.delete(cache.keys().next().value);
|
|
597
|
+
cache.set(key, { result, until });
|
|
598
|
+
}
|
|
599
|
+
return result;
|
|
600
|
+
};
|
|
601
|
+
const verifyJwt = async (token, signal) => {
|
|
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
|
+
}
|
|
612
|
+
let keySet;
|
|
613
|
+
try {
|
|
614
|
+
keySet = await signingKeys(signal);
|
|
615
|
+
} catch (error) {
|
|
616
|
+
throw new AuthioBearerError(AuthioBearerErrorDescriptions.SigningKeysUnavailable, { cause: error, reason: "signing_keys_unavailable" });
|
|
617
|
+
}
|
|
618
|
+
let payload;
|
|
619
|
+
try {
|
|
620
|
+
({ payload } = await client.verifyAuthioJwt(signed, keySet, {
|
|
621
|
+
issuer: connection.issuer,
|
|
622
|
+
audiences,
|
|
623
|
+
algorithms,
|
|
624
|
+
clockSkew,
|
|
625
|
+
now: connection.clock.now()
|
|
626
|
+
}));
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (error instanceof client.AuthioTokenValidationError) throw new AuthioBearerError(error.message, { cause: error, reason: error.reason });
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
const type = typeof payload.typ === "string" ? payload.typ : void 0;
|
|
632
|
+
if (rejectNonAccessTokens && type !== void 0 && type.toLowerCase() !== "bearer")
|
|
633
|
+
throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'.`, { reason: "not_access_token" });
|
|
634
|
+
if (introspection === "activeCheck") await confirmActive(token, payload, signal);
|
|
635
|
+
return new AuthioUser(payload, claimSettings, token);
|
|
636
|
+
};
|
|
637
|
+
const confirmActive = async (token, payload, signal) => {
|
|
638
|
+
const key = payload.jti ? `jti:${payload.jti}` : await keyForToken(token);
|
|
639
|
+
let result;
|
|
640
|
+
try {
|
|
641
|
+
result = await introspect(token, key, signal);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (signal?.aborted) throw error;
|
|
644
|
+
throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error, reason: "unconfirmed" });
|
|
645
|
+
}
|
|
646
|
+
if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive, { reason: "inactive" });
|
|
647
|
+
};
|
|
648
|
+
const verifyByIntrospection = async (token, signal) => {
|
|
649
|
+
let result;
|
|
650
|
+
try {
|
|
651
|
+
result = await introspect(token, await keyForToken(token), signal);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
if (signal?.aborted) throw error;
|
|
654
|
+
throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error, reason: "unconfirmed" });
|
|
655
|
+
}
|
|
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" });
|
|
658
|
+
const tokenAudiences = [result.aud ?? []].flat();
|
|
659
|
+
if (!tokenAudiences.some((audience) => audiences.includes(audience)))
|
|
660
|
+
throw new AuthioBearerError(describeAudience(tokenAudiences, audiences), { reason: "invalid_audience" });
|
|
661
|
+
if (typeof result.exp === "number" && (result.exp + clockSkew) * 1e3 <= connection.clock.now())
|
|
662
|
+
throw new AuthioBearerError(`The token expired at '${new Date(result.exp * 1e3).toISOString()}'`, { reason: "expired" });
|
|
663
|
+
const claims = claimsOfIntrospection(result);
|
|
664
|
+
const type = typeof claims.typ === "string" ? claims.typ : void 0;
|
|
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");
|
|
668
|
+
};
|
|
669
|
+
const verify = async (token, signal) => {
|
|
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
|
+
}
|
|
676
|
+
};
|
|
677
|
+
return {
|
|
678
|
+
connection,
|
|
679
|
+
discovery,
|
|
680
|
+
audiences,
|
|
681
|
+
resource,
|
|
682
|
+
resourceMetadataUrl,
|
|
683
|
+
requiredScopes,
|
|
684
|
+
introspection,
|
|
685
|
+
client: client$1,
|
|
686
|
+
verify,
|
|
687
|
+
async verifyAuthorizationHeader(header, signal) {
|
|
688
|
+
const token = readBearerToken(header);
|
|
689
|
+
if (token === void 0) throw new AuthioBearerError(void 0, { error: void 0, resourceMetadata: resourceMetadataUrl });
|
|
690
|
+
return verify(token, signal);
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function readBearerToken(header) {
|
|
695
|
+
const match = /^\s*Bearer\s+(\S+)\s*$/i.exec(header ?? "");
|
|
696
|
+
return match?.[1];
|
|
697
|
+
}
|
|
698
|
+
async function keyForToken(token) {
|
|
699
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token)));
|
|
700
|
+
return `token:${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
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
|
+
}
|
|
709
|
+
function resolveClient(options) {
|
|
710
|
+
const given = options.client;
|
|
711
|
+
if (!given) return void 0;
|
|
712
|
+
if (isClient(given)) return given;
|
|
713
|
+
return client.createAuthioClient({
|
|
714
|
+
...given,
|
|
715
|
+
issuer: options.issuer,
|
|
716
|
+
authority: options.authority,
|
|
717
|
+
realm: options.realm,
|
|
718
|
+
requireHttps: options.requireHttps,
|
|
719
|
+
discoveryCacheDuration: options.discoveryCacheDuration,
|
|
720
|
+
httpTimeout: options.httpTimeout,
|
|
721
|
+
fetch: options.fetch,
|
|
722
|
+
clock: options.clock
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function isClient(value) {
|
|
726
|
+
return typeof value.introspect === "function" && typeof value.tokens === "object";
|
|
727
|
+
}
|
|
728
|
+
function describeAudience(tokenAudiences, audiences) {
|
|
729
|
+
const known = tokenAudiences.length === 0 ? "" : ` ${tokenAudiences.map((audience) => `'${audience}'`).join(" and ")}`;
|
|
730
|
+
const accepted = audiences.map((audience) => `'${audience}'`).join(" or ");
|
|
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).`;
|
|
732
|
+
}
|
|
733
|
+
function createAuthioWebhooks(options) {
|
|
734
|
+
const secrets = [...new Set([options.secret, ...options.secrets ?? []].filter((secret) => !!secret?.trim()))];
|
|
735
|
+
if (secrets.length === 0) throw new client.AuthioConfigurationError("Authio webhooks need the webhook's secret.");
|
|
736
|
+
const clock = options.clock ?? client.systemClock;
|
|
737
|
+
const tolerance = options.tolerance ?? 300;
|
|
738
|
+
const windowMs = (options.deduplicationWindow ?? 86400) * 1e3;
|
|
739
|
+
const maxBodySize = options.maxBodySize ?? 1024 * 1024;
|
|
740
|
+
const store = resolveStore(options.deduplication ?? "memory", clock);
|
|
741
|
+
const handlers = [];
|
|
742
|
+
const webhooks = {
|
|
743
|
+
maxBodySize,
|
|
744
|
+
on(type, handler) {
|
|
745
|
+
if (!type) throw new TypeError("An event type (or '*') is required.");
|
|
746
|
+
handlers.push({ type, handler });
|
|
747
|
+
return webhooks;
|
|
748
|
+
},
|
|
749
|
+
async handle(delivery) {
|
|
750
|
+
const body = typeof delivery.body === "string" ? new TextEncoder().encode(delivery.body) : delivery.body;
|
|
751
|
+
if (body.byteLength > maxBodySize) return { status: 413, reason: "too_large" };
|
|
752
|
+
const signature = headerOf(delivery.headers, client.AuthioWebhookHeaders.Signature);
|
|
753
|
+
let refusal = "no webhook secret matched";
|
|
754
|
+
for (const secret of secrets) {
|
|
755
|
+
const result = await client.verifyWebhookSignature(signature, body, secret, { tolerance, now: clock.now() });
|
|
756
|
+
if (result.valid) {
|
|
757
|
+
refusal = "";
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
refusal = result.reason;
|
|
761
|
+
}
|
|
762
|
+
if (refusal) {
|
|
763
|
+
options.onError?.(new Error(refusal), { reason: "unsigned" });
|
|
764
|
+
return { status: 401, reason: "unsigned" };
|
|
765
|
+
}
|
|
766
|
+
const event = client.tryParseWebhookEvent(body);
|
|
767
|
+
if (!event) return { status: 400, reason: "unreadable" };
|
|
768
|
+
const eventId = headerOf(delivery.headers, client.AuthioWebhookHeaders.EventId)?.trim() || String(event.id ?? "");
|
|
769
|
+
if (eventId && !await store.tryBegin(eventId, windowMs)) return { status: 200, reason: "duplicate", event };
|
|
770
|
+
try {
|
|
771
|
+
for (const { type, handler } of handlers) if (type === "*" || type === event.type) await handler(event);
|
|
772
|
+
} catch (error) {
|
|
773
|
+
if (eventId) await store.release(eventId);
|
|
774
|
+
options.onError?.(error, { reason: "handler_failed", eventId, eventType: event.type });
|
|
775
|
+
return { status: 500, reason: "handler_failed", event };
|
|
776
|
+
}
|
|
777
|
+
return { status: 200, reason: "handled", event };
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
return webhooks;
|
|
781
|
+
}
|
|
782
|
+
function createMemoryDeduplicationStore(clock = client.systemClock) {
|
|
783
|
+
const seen = /* @__PURE__ */ new Map();
|
|
784
|
+
return {
|
|
785
|
+
tryBegin(eventId, windowMs) {
|
|
786
|
+
const now = clock.now();
|
|
787
|
+
for (const [id, until2] of seen) {
|
|
788
|
+
if (until2 > now) break;
|
|
789
|
+
seen.delete(id);
|
|
790
|
+
}
|
|
791
|
+
const until = seen.get(eventId);
|
|
792
|
+
if (until !== void 0 && until > now) return false;
|
|
793
|
+
seen.delete(eventId);
|
|
794
|
+
seen.set(eventId, now + windowMs);
|
|
795
|
+
return true;
|
|
796
|
+
},
|
|
797
|
+
release(eventId) {
|
|
798
|
+
seen.delete(eventId);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function resolveStore(deduplication, clock) {
|
|
803
|
+
if (deduplication === "none") return { tryBegin: () => true, release: () => void 0 };
|
|
804
|
+
if (deduplication === "memory") return createMemoryDeduplicationStore(clock);
|
|
805
|
+
return deduplication;
|
|
806
|
+
}
|
|
807
|
+
function headerOf(headers, name) {
|
|
808
|
+
if (typeof headers.get === "function") return headers.get(name) ?? void 0;
|
|
809
|
+
const wanted = name.toLowerCase();
|
|
810
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
811
|
+
if (key.toLowerCase() === wanted) return Array.isArray(value) ? value[0] : value;
|
|
812
|
+
}
|
|
813
|
+
return void 0;
|
|
814
|
+
}
|
|
815
|
+
function createForwardingFetch(subject, inner = globalThis.fetch) {
|
|
816
|
+
const token = tokenOf(subject);
|
|
817
|
+
return (input, init) => {
|
|
818
|
+
if (!token) return inner(input, init);
|
|
819
|
+
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
|
|
820
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
821
|
+
return inner(input, { ...init, headers });
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function createOnBehalfOf(options) {
|
|
825
|
+
const { client: client$1 } = options;
|
|
826
|
+
const scope = client.toScopeString(options.scopes);
|
|
827
|
+
const sortedScope = (scope ?? "").split(" ").filter(Boolean).sort().join(" ");
|
|
828
|
+
const skewMs = client.seconds(options.tokenRefreshSkew, client.AuthioDefaults.TokenRefreshSkew, "tokenRefreshSkew");
|
|
829
|
+
const entries = /* @__PURE__ */ new Map();
|
|
830
|
+
const pending = /* @__PURE__ */ new Map();
|
|
831
|
+
const keyOf = async (subject) => {
|
|
832
|
+
const token = tokenOf(subject);
|
|
833
|
+
const jti = typeof subject === "string" ? void 0 : subject.tokenId;
|
|
834
|
+
return `${jti ? `jti:${jti}` : await keyForToken(token)}|${options.resource ?? ""}|${sortedScope}`;
|
|
835
|
+
};
|
|
836
|
+
const getToken = async (subject, signal) => {
|
|
837
|
+
const incoming = tokenOf(subject);
|
|
838
|
+
if (!incoming) throw new client.AuthioError("On-behalf-of needs the caller's access token, and the request has none.");
|
|
839
|
+
const key = await keyOf(subject);
|
|
840
|
+
const cached = entries.get(key);
|
|
841
|
+
if (cached && client$1.connection.clock.now() < cached.refreshAt) return cached;
|
|
842
|
+
let request = pending.get(key);
|
|
843
|
+
if (!request) {
|
|
844
|
+
request = client$1.tokens.exchange({ subjectToken: incoming, scopes: scope, resource: options.resource, signal }).then((response) => {
|
|
845
|
+
const token = client.toClientCredentialsToken(response, client$1.connection.clock.now(), skewMs);
|
|
846
|
+
entries.set(key, token);
|
|
847
|
+
return token;
|
|
848
|
+
}).finally(() => pending.delete(key));
|
|
849
|
+
pending.set(key, request);
|
|
850
|
+
}
|
|
851
|
+
return request;
|
|
852
|
+
};
|
|
853
|
+
const invalidate = async (subject, accessToken) => {
|
|
854
|
+
const key = await keyOf(subject);
|
|
855
|
+
if (accessToken === void 0 || entries.get(key)?.accessToken === accessToken) entries.delete(key);
|
|
856
|
+
};
|
|
857
|
+
return {
|
|
858
|
+
getToken,
|
|
859
|
+
invalidate,
|
|
860
|
+
fetch(subject, inner = client$1.connection.fetch) {
|
|
861
|
+
return async (input, init) => {
|
|
862
|
+
const request = new Request(input, init);
|
|
863
|
+
const body = request.body ? await request.arrayBuffer() : void 0;
|
|
864
|
+
const attempt = (token2) => {
|
|
865
|
+
const headers = new Headers(request.headers);
|
|
866
|
+
headers.set("authorization", `${token2.tokenType} ${token2.accessToken}`);
|
|
867
|
+
return inner(request.url, { method: request.method, headers, body, signal: request.signal, redirect: request.redirect });
|
|
868
|
+
};
|
|
869
|
+
const token = await getToken(subject, request.signal);
|
|
870
|
+
const response = await attempt(token);
|
|
871
|
+
if (response.status !== 401) return response;
|
|
872
|
+
await invalidate(subject, token.accessToken);
|
|
873
|
+
return attempt(await getToken(subject, request.signal));
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function tokenOf(subject) {
|
|
879
|
+
if (!subject) return void 0;
|
|
880
|
+
return typeof subject === "string" ? subject : subject.token;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
exports.AuthioBearerError = AuthioBearerError;
|
|
884
|
+
exports.AuthioBearerErrorDescriptions = AuthioBearerErrorDescriptions;
|
|
885
|
+
exports.AuthioForbiddenError = AuthioForbiddenError;
|
|
886
|
+
exports.AuthioUser = AuthioUser;
|
|
887
|
+
exports.allOf = allOf;
|
|
888
|
+
exports.anyOf = anyOf;
|
|
889
|
+
exports.authorize = authorize;
|
|
890
|
+
exports.createAuthioVerifier = createAuthioVerifier;
|
|
891
|
+
exports.createAuthioWebhooks = createAuthioWebhooks;
|
|
892
|
+
exports.createForwardingFetch = createForwardingFetch;
|
|
893
|
+
exports.createMemoryDeduplicationStore = createMemoryDeduplicationStore;
|
|
894
|
+
exports.createOnBehalfOf = createOnBehalfOf;
|
|
895
|
+
exports.createProtectedResourceMetadata = createProtectedResourceMetadata;
|
|
896
|
+
exports.protectedResourceMetadataPath = protectedResourceMetadataPath;
|
|
897
|
+
exports.protectedResourceMetadataUrl = protectedResourceMetadataUrl;
|
|
898
|
+
exports.readBearerToken = readBearerToken;
|
|
899
|
+
exports.requireAuthenticationLevel = requireAuthenticationLevel;
|
|
900
|
+
exports.requireAuthenticationMethod = requireAuthenticationMethod;
|
|
901
|
+
exports.requireCallerKind = requireCallerKind;
|
|
902
|
+
exports.requireClaim = requireClaim;
|
|
903
|
+
exports.requireClient = requireClient;
|
|
904
|
+
exports.requireGroup = requireGroup;
|
|
905
|
+
exports.requireOrganization = requireOrganization;
|
|
906
|
+
exports.requirePermission = requirePermission;
|
|
907
|
+
exports.requireRecentAuthentication = requireRecentAuthentication;
|
|
908
|
+
exports.requireRole = requireRole;
|
|
909
|
+
exports.requireScope = requireScope;
|
|
910
|
+
exports.toForbiddenChallenge = toForbiddenChallenge;
|
|
911
|
+
exports.toProblemDetails = toProblemDetails;
|
|
912
|
+
//# sourceMappingURL=index.cjs.map
|
|
913
|
+
//# sourceMappingURL=index.cjs.map
|