@hyper.authio/server 0.8.1 → 0.10.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 +31 -3
- package/dist/index.cjs +16 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -13
- package/dist/index.d.ts +27 -13
- package/dist/index.js +17 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -34,8 +34,9 @@ interface AuthioUserClaimSettings {
|
|
|
34
34
|
groupClaims?: readonly string[];
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* The caller of an API, read from a validated access token
|
|
38
|
-
*
|
|
37
|
+
* The caller of an API, read from a validated access token, an introspection answer, or both (the token's claims and
|
|
38
|
+
* those of the answer the token does not carry, as `activeCheck` reads it): identity, every form of permission Authio
|
|
39
|
+
* issues, and the raw claims for anything else.
|
|
39
40
|
*/
|
|
40
41
|
declare class AuthioUser {
|
|
41
42
|
/** `sub`: the person's id, or the credential's for a machine caller. */
|
|
@@ -75,7 +76,7 @@ declare class AuthioUser {
|
|
|
75
76
|
readonly actorChain: readonly string[];
|
|
76
77
|
/** `auth_time`, in seconds since the epoch. */
|
|
77
78
|
readonly authTime: number | undefined;
|
|
78
|
-
/** The client the token was issued to (`
|
|
79
|
+
/** The client the token was issued to (`client_id`, RFC 9068 §2.2, else `azp`). */
|
|
79
80
|
readonly clientId: string | undefined;
|
|
80
81
|
/** `sid`. */
|
|
81
82
|
readonly sessionId: string | undefined;
|
|
@@ -121,7 +122,10 @@ declare class AuthioUser {
|
|
|
121
122
|
readonly credentialType: string | undefined;
|
|
122
123
|
/** `credential_id`. */
|
|
123
124
|
readonly credentialId: string | undefined;
|
|
124
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Every claim, as the token or the introspection answer carried it (with `activeCheck`, the token's, plus the
|
|
127
|
+
* answer's the token does not carry).
|
|
128
|
+
*/
|
|
125
129
|
readonly claims: Readonly<Record<string, unknown>>;
|
|
126
130
|
/** The access token the user was read from. Not enumerable, so it stays out of logs and JSON. */
|
|
127
131
|
readonly token: string | undefined;
|
|
@@ -203,9 +207,11 @@ interface AuthioRule {
|
|
|
203
207
|
check(user: AuthioUser): AuthioRuleFailure | undefined;
|
|
204
208
|
}
|
|
205
209
|
/**
|
|
206
|
-
* Checks every rule. On refusal the message joins every rule's
|
|
207
|
-
*
|
|
208
|
-
*
|
|
210
|
+
* Checks every rule. On refusal the message joins every rule's. When a rule other than a step-up fails (a scope, role,
|
|
211
|
+
* permission, group, claim, organization, client or caller kind), no stronger sign-in can satisfy the request: the
|
|
212
|
+
* refusal is a `403`, with the first such rule's error (`insufficient_scope`) or none, and never asks for a step-up.
|
|
213
|
+
* Only when every failing rule asks for a stronger or more recent sign-in is the error `insufficient_user_authentication`,
|
|
214
|
+
* with the strongest `acr_values` and the shortest `max_age` they ask for.
|
|
209
215
|
*/
|
|
210
216
|
declare function authorize(user: AuthioUser, ...rules: AuthioRule[]): AuthioRuleFailure | undefined;
|
|
211
217
|
/**
|
|
@@ -264,7 +270,7 @@ declare function requireRecentAuthentication(maxAge: number, options?: {
|
|
|
264
270
|
*/
|
|
265
271
|
declare function requireCallerKind(kinds: AuthioCallerKind | readonly AuthioCallerKind[]): AuthioRule;
|
|
266
272
|
/**
|
|
267
|
-
* Requires the token to have been issued to one of the clients (`
|
|
273
|
+
* Requires the token to have been issued to one of the clients (`client_id`, else `azp`).
|
|
268
274
|
*/
|
|
269
275
|
declare function requireClient(clientIds: string | readonly string[]): AuthioRule;
|
|
270
276
|
/**
|
|
@@ -337,10 +343,15 @@ declare function toProblemDetails(error: AuthioBearerError | AuthioForbiddenErro
|
|
|
337
343
|
/**
|
|
338
344
|
* Whether and how Authio's introspection endpoint takes part in validating a token.
|
|
339
345
|
*
|
|
340
|
-
* - `none`: the JWT is validated locally (signature, issuer, audience, lifetime)
|
|
341
|
-
* valid until it expires.
|
|
342
|
-
*
|
|
343
|
-
* - `
|
|
346
|
+
* - `none`: the JWT is validated locally (signature, issuer, audience, lifetime) and the caller is read from it alone.
|
|
347
|
+
* Fastest; a revoked token stays valid until it expires. A lightweight access token carries no roles, permissions,
|
|
348
|
+
* plan, profile or email, so it needs one of the modes below.
|
|
349
|
+
* - `activeCheck`: validated locally, then confirmed active by introspection (cached per `jti`). The caller is read
|
|
350
|
+
* from the token, and the introspection answer adds the claims the token does not carry: the answer has every claim
|
|
351
|
+
* of the complete token, so a lightweight access token's roles and permissions are read too. A claim the token
|
|
352
|
+
* carries keeps the token's value, which its signature vouches for (the answer names a single `aud`).
|
|
353
|
+
* - `introspectionOnly`: introspection alone, for opaque (reference) tokens, or encrypted ones without the key. The
|
|
354
|
+
* caller is read from the answer.
|
|
344
355
|
*/
|
|
345
356
|
type AuthioIntrospectionMode = "none" | "activeCheck" | "introspectionOnly";
|
|
346
357
|
/**
|
|
@@ -381,7 +392,10 @@ interface AuthioVerifierOptions extends AuthioConnectionOptions, AuthioUserClaim
|
|
|
381
392
|
* realm's keys are accepted, and never HS*.
|
|
382
393
|
*/
|
|
383
394
|
accessTokenSignedResponseAlg?: string | readonly string[];
|
|
384
|
-
/**
|
|
395
|
+
/**
|
|
396
|
+
* Refuses a token whose `typ` claim is present and not `Bearer` (an ID token, for one). Defaults to `true`. A JWT
|
|
397
|
+
* whose header `typ` is not `at+jwt` (RFC 9068 §4) is refused whatever this says.
|
|
398
|
+
*/
|
|
385
399
|
rejectNonAccessTokens?: boolean;
|
|
386
400
|
/**
|
|
387
401
|
* The private half of the encryption key Authio encrypts this API's access tokens to (the resource server's or its
|
package/dist/index.d.ts
CHANGED
|
@@ -34,8 +34,9 @@ interface AuthioUserClaimSettings {
|
|
|
34
34
|
groupClaims?: readonly string[];
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* The caller of an API, read from a validated access token
|
|
38
|
-
*
|
|
37
|
+
* The caller of an API, read from a validated access token, an introspection answer, or both (the token's claims and
|
|
38
|
+
* those of the answer the token does not carry, as `activeCheck` reads it): identity, every form of permission Authio
|
|
39
|
+
* issues, and the raw claims for anything else.
|
|
39
40
|
*/
|
|
40
41
|
declare class AuthioUser {
|
|
41
42
|
/** `sub`: the person's id, or the credential's for a machine caller. */
|
|
@@ -75,7 +76,7 @@ declare class AuthioUser {
|
|
|
75
76
|
readonly actorChain: readonly string[];
|
|
76
77
|
/** `auth_time`, in seconds since the epoch. */
|
|
77
78
|
readonly authTime: number | undefined;
|
|
78
|
-
/** The client the token was issued to (`
|
|
79
|
+
/** The client the token was issued to (`client_id`, RFC 9068 §2.2, else `azp`). */
|
|
79
80
|
readonly clientId: string | undefined;
|
|
80
81
|
/** `sid`. */
|
|
81
82
|
readonly sessionId: string | undefined;
|
|
@@ -121,7 +122,10 @@ declare class AuthioUser {
|
|
|
121
122
|
readonly credentialType: string | undefined;
|
|
122
123
|
/** `credential_id`. */
|
|
123
124
|
readonly credentialId: string | undefined;
|
|
124
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Every claim, as the token or the introspection answer carried it (with `activeCheck`, the token's, plus the
|
|
127
|
+
* answer's the token does not carry).
|
|
128
|
+
*/
|
|
125
129
|
readonly claims: Readonly<Record<string, unknown>>;
|
|
126
130
|
/** The access token the user was read from. Not enumerable, so it stays out of logs and JSON. */
|
|
127
131
|
readonly token: string | undefined;
|
|
@@ -203,9 +207,11 @@ interface AuthioRule {
|
|
|
203
207
|
check(user: AuthioUser): AuthioRuleFailure | undefined;
|
|
204
208
|
}
|
|
205
209
|
/**
|
|
206
|
-
* Checks every rule. On refusal the message joins every rule's
|
|
207
|
-
*
|
|
208
|
-
*
|
|
210
|
+
* Checks every rule. On refusal the message joins every rule's. When a rule other than a step-up fails (a scope, role,
|
|
211
|
+
* permission, group, claim, organization, client or caller kind), no stronger sign-in can satisfy the request: the
|
|
212
|
+
* refusal is a `403`, with the first such rule's error (`insufficient_scope`) or none, and never asks for a step-up.
|
|
213
|
+
* Only when every failing rule asks for a stronger or more recent sign-in is the error `insufficient_user_authentication`,
|
|
214
|
+
* with the strongest `acr_values` and the shortest `max_age` they ask for.
|
|
209
215
|
*/
|
|
210
216
|
declare function authorize(user: AuthioUser, ...rules: AuthioRule[]): AuthioRuleFailure | undefined;
|
|
211
217
|
/**
|
|
@@ -264,7 +270,7 @@ declare function requireRecentAuthentication(maxAge: number, options?: {
|
|
|
264
270
|
*/
|
|
265
271
|
declare function requireCallerKind(kinds: AuthioCallerKind | readonly AuthioCallerKind[]): AuthioRule;
|
|
266
272
|
/**
|
|
267
|
-
* Requires the token to have been issued to one of the clients (`
|
|
273
|
+
* Requires the token to have been issued to one of the clients (`client_id`, else `azp`).
|
|
268
274
|
*/
|
|
269
275
|
declare function requireClient(clientIds: string | readonly string[]): AuthioRule;
|
|
270
276
|
/**
|
|
@@ -337,10 +343,15 @@ declare function toProblemDetails(error: AuthioBearerError | AuthioForbiddenErro
|
|
|
337
343
|
/**
|
|
338
344
|
* Whether and how Authio's introspection endpoint takes part in validating a token.
|
|
339
345
|
*
|
|
340
|
-
* - `none`: the JWT is validated locally (signature, issuer, audience, lifetime)
|
|
341
|
-
* valid until it expires.
|
|
342
|
-
*
|
|
343
|
-
* - `
|
|
346
|
+
* - `none`: the JWT is validated locally (signature, issuer, audience, lifetime) and the caller is read from it alone.
|
|
347
|
+
* Fastest; a revoked token stays valid until it expires. A lightweight access token carries no roles, permissions,
|
|
348
|
+
* plan, profile or email, so it needs one of the modes below.
|
|
349
|
+
* - `activeCheck`: validated locally, then confirmed active by introspection (cached per `jti`). The caller is read
|
|
350
|
+
* from the token, and the introspection answer adds the claims the token does not carry: the answer has every claim
|
|
351
|
+
* of the complete token, so a lightweight access token's roles and permissions are read too. A claim the token
|
|
352
|
+
* carries keeps the token's value, which its signature vouches for (the answer names a single `aud`).
|
|
353
|
+
* - `introspectionOnly`: introspection alone, for opaque (reference) tokens, or encrypted ones without the key. The
|
|
354
|
+
* caller is read from the answer.
|
|
344
355
|
*/
|
|
345
356
|
type AuthioIntrospectionMode = "none" | "activeCheck" | "introspectionOnly";
|
|
346
357
|
/**
|
|
@@ -381,7 +392,10 @@ interface AuthioVerifierOptions extends AuthioConnectionOptions, AuthioUserClaim
|
|
|
381
392
|
* realm's keys are accepted, and never HS*.
|
|
382
393
|
*/
|
|
383
394
|
accessTokenSignedResponseAlg?: string | readonly string[];
|
|
384
|
-
/**
|
|
395
|
+
/**
|
|
396
|
+
* Refuses a token whose `typ` claim is present and not `Bearer` (an ID token, for one). Defaults to `true`. A JWT
|
|
397
|
+
* whose header `typ` is not `at+jwt` (RFC 9068 §4) is refused whatever this says.
|
|
398
|
+
*/
|
|
385
399
|
rejectNonAccessTokens?: boolean;
|
|
386
400
|
/**
|
|
387
401
|
* The private half of the encryption key Authio encrypts this API's access tokens to (the resource server's or its
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { systemClock, AuthioClaimTypes, parseResourceAccess, AuthioError, AuthioDefaults, resolveIssuer, AuthioConfigurationError, resolveConnection, createAuthioDiscoveryClient, seconds, AuthioHmacAlgorithms, createAuthioSigningKeys, encryptionKeyFromFile, createAuthioTokenDecrypter, AuthioAsymmetricAlgorithms, requireHmacSecretLength, createAuthioClient, toScopeString, parseAuthioBearerChallenge, AuthioOAuthErrors, AuthioWebhookHeaders, verifyWebhookSignature, tryParseWebhookEvent, toClientCredentialsToken, isAuthioTokenEncrypted, AuthioTokenValidationError, withClientSecret, verifyAuthioJwt, AuthioTokenTypes } from '@hyper.authio/client';
|
|
1
|
+
import { systemClock, AuthioClaimTypes, parseResourceAccess, AuthioError, AuthioDefaults, resolveIssuer, AuthioConfigurationError, resolveConnection, createAuthioDiscoveryClient, seconds, AuthioHmacAlgorithms, createAuthioSigningKeys, encryptionKeyFromFile, createAuthioTokenDecrypter, AuthioAsymmetricAlgorithms, requireHmacSecretLength, createAuthioClient, toScopeString, parseAuthioBearerChallenge, AuthioOAuthErrors, AuthioWebhookHeaders, verifyWebhookSignature, tryParseWebhookEvent, toClientCredentialsToken, isAuthioTokenEncrypted, AuthioTokenValidationError, withClientSecret, verifyAuthioJwt, requireAccessTokenType, AuthioTokenTypes } from '@hyper.authio/client';
|
|
2
2
|
import { decodeProtectedHeader } from 'jose';
|
|
3
3
|
|
|
4
4
|
// src/user.ts
|
|
@@ -41,7 +41,7 @@ var AuthioUser = class _AuthioUser {
|
|
|
41
41
|
actorChain;
|
|
42
42
|
/** `auth_time`, in seconds since the epoch. */
|
|
43
43
|
authTime;
|
|
44
|
-
/** The client the token was issued to (`
|
|
44
|
+
/** The client the token was issued to (`client_id`, RFC 9068 §2.2, else `azp`). */
|
|
45
45
|
clientId;
|
|
46
46
|
/** `sid`. */
|
|
47
47
|
sessionId;
|
|
@@ -87,7 +87,10 @@ var AuthioUser = class _AuthioUser {
|
|
|
87
87
|
credentialType;
|
|
88
88
|
/** `credential_id`. */
|
|
89
89
|
credentialId;
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Every claim, as the token or the introspection answer carried it (with `activeCheck`, the token's, plus the
|
|
92
|
+
* answer's the token does not carry).
|
|
93
|
+
*/
|
|
91
94
|
claims;
|
|
92
95
|
flatRoles;
|
|
93
96
|
/**
|
|
@@ -102,7 +105,7 @@ var AuthioUser = class _AuthioUser {
|
|
|
102
105
|
const text = (name) => typeof claims[name] === "string" && claims[name] ? claims[name] : void 0;
|
|
103
106
|
this.claims = Object.freeze({ ...claims });
|
|
104
107
|
this.subject = text(AuthioClaimTypes.Subject) ?? "";
|
|
105
|
-
this.clientId = text(AuthioClaimTypes.
|
|
108
|
+
this.clientId = text(AuthioClaimTypes.ClientId) ?? text(AuthioClaimTypes.AuthorizedParty);
|
|
106
109
|
this.sessionId = text(AuthioClaimTypes.SessionId);
|
|
107
110
|
this.tokenId = text(AuthioClaimTypes.JwtId);
|
|
108
111
|
this.audiences = Object.freeze(distinct(valuesOf(claims[AuthioClaimTypes.Audience])));
|
|
@@ -275,8 +278,9 @@ function distinctIgnoringCase(values) {
|
|
|
275
278
|
function authorize(user, ...rules) {
|
|
276
279
|
const failures = rules.map((rule) => rule.check(user)).filter((failure) => failure !== void 0);
|
|
277
280
|
if (failures.length === 0) return void 0;
|
|
278
|
-
const
|
|
279
|
-
const
|
|
281
|
+
const others = failures.filter((failure) => failure.error !== "insufficient_user_authentication");
|
|
282
|
+
const stepUps = others.length === 0 ? failures : [];
|
|
283
|
+
const bearer = stepUps[0] ?? others.find((failure) => failure.error);
|
|
280
284
|
const acrValues = strongestAcr(stepUps.map((failure) => failure.acrValues));
|
|
281
285
|
const maxAges = stepUps.map((failure) => failure.maxAge).filter((value) => value !== void 0);
|
|
282
286
|
return {
|
|
@@ -636,13 +640,15 @@ function createAuthioVerifier(options) {
|
|
|
636
640
|
}
|
|
637
641
|
let payload;
|
|
638
642
|
try {
|
|
639
|
-
|
|
643
|
+
let header;
|
|
644
|
+
({ payload, header } = await verifyAuthioJwt(signed, keySet, {
|
|
640
645
|
issuer: connection.issuer,
|
|
641
646
|
audiences,
|
|
642
647
|
algorithms,
|
|
643
648
|
clockSkew,
|
|
644
649
|
now: connection.clock.now()
|
|
645
650
|
}));
|
|
651
|
+
requireAccessTokenType(header);
|
|
646
652
|
} catch (error) {
|
|
647
653
|
if (error instanceof AuthioTokenValidationError && error.reason === "invalid_algorithm" && symmetric.length === 0 && isSymmetricToken(signed))
|
|
648
654
|
throw new AuthioBearerError(AuthioBearerErrorDescriptions.Symmetric, { cause: error, reason: error.reason });
|
|
@@ -652,8 +658,9 @@ function createAuthioVerifier(options) {
|
|
|
652
658
|
const type = typeof payload.typ === "string" ? payload.typ : void 0;
|
|
653
659
|
if (rejectNonAccessTokens && type !== void 0 && type.toLowerCase() !== "bearer")
|
|
654
660
|
throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'.`, { reason: "not_access_token" });
|
|
655
|
-
if (introspection
|
|
656
|
-
|
|
661
|
+
if (introspection !== "activeCheck") return new AuthioUser(payload, claimSettings, token);
|
|
662
|
+
const answer = await confirmActive(token, payload, signal);
|
|
663
|
+
return new AuthioUser({ ...claimsOfIntrospection(answer), ...payload }, claimSettings, token);
|
|
657
664
|
};
|
|
658
665
|
const confirmActive = async (token, payload, signal) => {
|
|
659
666
|
const key = payload.jti ? `jti:${payload.jti}` : await keyForToken(token);
|
|
@@ -665,6 +672,7 @@ function createAuthioVerifier(options) {
|
|
|
665
672
|
throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error, reason: "unconfirmed" });
|
|
666
673
|
}
|
|
667
674
|
if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive, { reason: "inactive" });
|
|
675
|
+
return result;
|
|
668
676
|
};
|
|
669
677
|
const verifyByIntrospection = async (token, signal) => {
|
|
670
678
|
let result;
|