@hyper.authio/bff 0.9.0 → 0.10.1
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 +18 -2
- package/dist/index.cjs +51 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -2
- package/dist/index.d.ts +25 -2
- package/dist/index.js +51 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -93,6 +93,15 @@ declare const AuthioBffDefaults: {
|
|
|
93
93
|
* refresh token (its access token when there is none) every `sessionValidationInterval`.
|
|
94
94
|
*/
|
|
95
95
|
type AuthioBffSessionValidation = "none" | "introspection";
|
|
96
|
+
/**
|
|
97
|
+
* Where the BFF reads the claims only the access token carries (the `*_resource_access` roles and permissions,
|
|
98
|
+
* `org_id`, `org_alias`, `account_id`): `token`, from the access token once its signature verifies; or
|
|
99
|
+
* `introspection`, from Authio's introspection of the access token, asked as the BFF's own client. Mirror the client's
|
|
100
|
+
* access token format: `introspection` for `Lightweight`, whose token leaves those claims to introspection. Under
|
|
101
|
+
* `token`, an opaque (`Reference`) access token is introspected too, and contributes nothing when it is not active to
|
|
102
|
+
* the BFF's client.
|
|
103
|
+
*/
|
|
104
|
+
type AuthioBffAccessTokenClaims = "token" | "introspection";
|
|
96
105
|
/**
|
|
97
106
|
* The `SameSite` mode of the session cookie.
|
|
98
107
|
*/
|
|
@@ -128,6 +137,17 @@ interface AuthioBffOptions extends Partial<AuthioClientOptions> {
|
|
|
128
137
|
scopes?: readonly string[];
|
|
129
138
|
/** Adds the `roles` scope, so the session carries the `*_resource_access` claims flattened on `/bff/user`. */
|
|
130
139
|
requestRoles?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Where the session's roles, permissions and organization come from, read at sign-in and again on every refresh:
|
|
142
|
+
* `token` (default), the access token once its signature verifies; `introspection`, Authio's introspection of the
|
|
143
|
+
* access token, asked as the BFF's own confidential client. Use `introspection` when the client's access token
|
|
144
|
+
* format is `Lightweight`: then a failed or inactive introspection fails the sign-in, and on a refresh an inactive
|
|
145
|
+
* answer ends the session while any other failure keeps the session's previous claims until the next refresh and
|
|
146
|
+
* goes to `onError`. Under `token`, an opaque (`Reference`) access token is introspected too: an inactive answer
|
|
147
|
+
* (a token requested for another resource server's introspection client) leaves its claims out and keeps the session
|
|
148
|
+
* on a refresh, while a failed introspection fails the sign-in and, on a refresh, keeps the previous claims.
|
|
149
|
+
*/
|
|
150
|
+
accessTokenClaims?: AuthioBffAccessTokenClaims;
|
|
131
151
|
/** Calls Authio's UserInfo endpoint after sign-in and adds its claims to the session. Defaults to `false`. */
|
|
132
152
|
getClaimsFromUserInfoEndpoint?: boolean;
|
|
133
153
|
/** The `organization` sent on every sign-in unless the login request names one. */
|
|
@@ -234,7 +254,10 @@ interface AuthioBffSessionInfo {
|
|
|
234
254
|
readonly subject: string | undefined;
|
|
235
255
|
/** The Authio session (`sid`). */
|
|
236
256
|
readonly sessionId: string | undefined;
|
|
237
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* The session's claims: the ID token's, the access token's `*_resource_access` and organization (read from the
|
|
259
|
+
* token or its introspection, as `accessTokenClaims` says), UserInfo's.
|
|
260
|
+
*/
|
|
238
261
|
readonly claims: Readonly<Record<string, unknown>>;
|
|
239
262
|
/** When the BFF session expires, in milliseconds since the epoch. */
|
|
240
263
|
readonly expiresAt: number;
|
|
@@ -337,4 +360,4 @@ interface AuthioBffError {
|
|
|
337
360
|
readonly error_description?: string;
|
|
338
361
|
}
|
|
339
362
|
|
|
340
|
-
export { type AuthioBff, type AuthioBffApiOptions, type AuthioBffClaim, AuthioBffClaimTypes, type AuthioBffCookieSameSite, AuthioBffDefaults, type AuthioBffError, type AuthioBffMemoryStore, type AuthioBffMemoryStoreOptions, type AuthioBffOptions, type AuthioBffRemoteApiOptions, type AuthioBffRemoteApiTokenMode, type AuthioBffSessionInfo, type AuthioBffSessionStore, type AuthioBffSessionValidation, type AuthioBffStoredSession, type AuthioBffUserTokenOptions, authioBff, createAuthioBffMemoryStore, isLocalUrl };
|
|
363
|
+
export { type AuthioBff, type AuthioBffAccessTokenClaims, type AuthioBffApiOptions, type AuthioBffClaim, AuthioBffClaimTypes, type AuthioBffCookieSameSite, AuthioBffDefaults, type AuthioBffError, type AuthioBffMemoryStore, type AuthioBffMemoryStoreOptions, type AuthioBffOptions, type AuthioBffRemoteApiOptions, type AuthioBffRemoteApiTokenMode, type AuthioBffSessionInfo, type AuthioBffSessionStore, type AuthioBffSessionValidation, type AuthioBffStoredSession, type AuthioBffUserTokenOptions, authioBff, createAuthioBffMemoryStore, isLocalUrl };
|
package/dist/index.d.ts
CHANGED
|
@@ -93,6 +93,15 @@ declare const AuthioBffDefaults: {
|
|
|
93
93
|
* refresh token (its access token when there is none) every `sessionValidationInterval`.
|
|
94
94
|
*/
|
|
95
95
|
type AuthioBffSessionValidation = "none" | "introspection";
|
|
96
|
+
/**
|
|
97
|
+
* Where the BFF reads the claims only the access token carries (the `*_resource_access` roles and permissions,
|
|
98
|
+
* `org_id`, `org_alias`, `account_id`): `token`, from the access token once its signature verifies; or
|
|
99
|
+
* `introspection`, from Authio's introspection of the access token, asked as the BFF's own client. Mirror the client's
|
|
100
|
+
* access token format: `introspection` for `Lightweight`, whose token leaves those claims to introspection. Under
|
|
101
|
+
* `token`, an opaque (`Reference`) access token is introspected too, and contributes nothing when it is not active to
|
|
102
|
+
* the BFF's client.
|
|
103
|
+
*/
|
|
104
|
+
type AuthioBffAccessTokenClaims = "token" | "introspection";
|
|
96
105
|
/**
|
|
97
106
|
* The `SameSite` mode of the session cookie.
|
|
98
107
|
*/
|
|
@@ -128,6 +137,17 @@ interface AuthioBffOptions extends Partial<AuthioClientOptions> {
|
|
|
128
137
|
scopes?: readonly string[];
|
|
129
138
|
/** Adds the `roles` scope, so the session carries the `*_resource_access` claims flattened on `/bff/user`. */
|
|
130
139
|
requestRoles?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Where the session's roles, permissions and organization come from, read at sign-in and again on every refresh:
|
|
142
|
+
* `token` (default), the access token once its signature verifies; `introspection`, Authio's introspection of the
|
|
143
|
+
* access token, asked as the BFF's own confidential client. Use `introspection` when the client's access token
|
|
144
|
+
* format is `Lightweight`: then a failed or inactive introspection fails the sign-in, and on a refresh an inactive
|
|
145
|
+
* answer ends the session while any other failure keeps the session's previous claims until the next refresh and
|
|
146
|
+
* goes to `onError`. Under `token`, an opaque (`Reference`) access token is introspected too: an inactive answer
|
|
147
|
+
* (a token requested for another resource server's introspection client) leaves its claims out and keeps the session
|
|
148
|
+
* on a refresh, while a failed introspection fails the sign-in and, on a refresh, keeps the previous claims.
|
|
149
|
+
*/
|
|
150
|
+
accessTokenClaims?: AuthioBffAccessTokenClaims;
|
|
131
151
|
/** Calls Authio's UserInfo endpoint after sign-in and adds its claims to the session. Defaults to `false`. */
|
|
132
152
|
getClaimsFromUserInfoEndpoint?: boolean;
|
|
133
153
|
/** The `organization` sent on every sign-in unless the login request names one. */
|
|
@@ -234,7 +254,10 @@ interface AuthioBffSessionInfo {
|
|
|
234
254
|
readonly subject: string | undefined;
|
|
235
255
|
/** The Authio session (`sid`). */
|
|
236
256
|
readonly sessionId: string | undefined;
|
|
237
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* The session's claims: the ID token's, the access token's `*_resource_access` and organization (read from the
|
|
259
|
+
* token or its introspection, as `accessTokenClaims` says), UserInfo's.
|
|
260
|
+
*/
|
|
238
261
|
readonly claims: Readonly<Record<string, unknown>>;
|
|
239
262
|
/** When the BFF session expires, in milliseconds since the epoch. */
|
|
240
263
|
readonly expiresAt: number;
|
|
@@ -337,4 +360,4 @@ interface AuthioBffError {
|
|
|
337
360
|
readonly error_description?: string;
|
|
338
361
|
}
|
|
339
362
|
|
|
340
|
-
export { type AuthioBff, type AuthioBffApiOptions, type AuthioBffClaim, AuthioBffClaimTypes, type AuthioBffCookieSameSite, AuthioBffDefaults, type AuthioBffError, type AuthioBffMemoryStore, type AuthioBffMemoryStoreOptions, type AuthioBffOptions, type AuthioBffRemoteApiOptions, type AuthioBffRemoteApiTokenMode, type AuthioBffSessionInfo, type AuthioBffSessionStore, type AuthioBffSessionValidation, type AuthioBffStoredSession, type AuthioBffUserTokenOptions, authioBff, createAuthioBffMemoryStore, isLocalUrl };
|
|
363
|
+
export { type AuthioBff, type AuthioBffAccessTokenClaims, type AuthioBffApiOptions, type AuthioBffClaim, AuthioBffClaimTypes, type AuthioBffCookieSameSite, AuthioBffDefaults, type AuthioBffError, type AuthioBffMemoryStore, type AuthioBffMemoryStoreOptions, type AuthioBffOptions, type AuthioBffRemoteApiOptions, type AuthioBffRemoteApiTokenMode, type AuthioBffSessionInfo, type AuthioBffSessionStore, type AuthioBffSessionValidation, type AuthioBffStoredSession, type AuthioBffUserTokenOptions, authioBff, createAuthioBffMemoryStore, isLocalUrl };
|
package/dist/index.js
CHANGED
|
@@ -184,6 +184,7 @@ function resolveBffSettings(options) {
|
|
|
184
184
|
cookieSecrets,
|
|
185
185
|
endpointPrefix: `/${(options.endpointPrefix ?? AuthioBffDefaults.EndpointPrefix).replace(/^\/+|\/+$/g, "")}`,
|
|
186
186
|
scopes: distinct([...options.scopes ?? AuthioBffDefaults.Scopes, ...options.requestRoles ? ["roles"] : []]),
|
|
187
|
+
accessTokenClaims: options.accessTokenClaims ?? "token",
|
|
187
188
|
getClaimsFromUserInfoEndpoint: options.getClaimsFromUserInfoEndpoint ?? false,
|
|
188
189
|
organization: nonEmpty(options.organization),
|
|
189
190
|
resource: nonEmpty(options.resource),
|
|
@@ -221,6 +222,12 @@ function resolveBffSettings(options) {
|
|
|
221
222
|
else if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(settings.cookieName)) problems.push("cookieName holds characters a cookie name cannot.");
|
|
222
223
|
if (settings.cookieSameSite !== "lax" && settings.cookieSameSite !== "strict") problems.push("cookieSameSite must be lax or strict.");
|
|
223
224
|
if (!isPositive(settings.sessionLifetimeMs)) problems.push("sessionLifetime must be a positive number of seconds.");
|
|
225
|
+
if (settings.accessTokenClaims !== "token" && settings.accessTokenClaims !== "introspection")
|
|
226
|
+
problems.push("accessTokenClaims must be token or introspection.");
|
|
227
|
+
else if (settings.accessTokenClaims === "introspection" && !options.client && isPublicClient(options))
|
|
228
|
+
problems.push(
|
|
229
|
+
"accessTokenClaims introspection needs a confidential client: Authio answers introspection only to a client that authenticates, so set clientSecret or clientPrivateKey."
|
|
230
|
+
);
|
|
224
231
|
if (settings.sessionValidation !== "none" && settings.sessionValidation !== "introspection")
|
|
225
232
|
problems.push("sessionValidation must be none or introspection.");
|
|
226
233
|
else if (settings.sessionValidation === "introspection" && !isPositive(settings.sessionValidationIntervalMs))
|
|
@@ -247,6 +254,10 @@ function isLocalUrl(url) {
|
|
|
247
254
|
if (typeof url !== "string" || !url || /[\u0000-\u001f\u007f-\u009f]/.test(url)) return false;
|
|
248
255
|
return url[0] === "/" && (url.length === 1 || url[1] !== "/" && url[1] !== "\\");
|
|
249
256
|
}
|
|
257
|
+
function isPublicClient(options) {
|
|
258
|
+
if (options.tokenEndpointAuthMethod !== void 0) return options.tokenEndpointAuthMethod === "none";
|
|
259
|
+
return !options.clientSecret && options.clientPrivateKey === void 0 && !options.clientPrivateKeyPath;
|
|
260
|
+
}
|
|
250
261
|
function distinct(scopes) {
|
|
251
262
|
return Object.freeze([...new Set(scopes.map((scope) => scope.trim()).filter(Boolean))]);
|
|
252
263
|
}
|
|
@@ -564,7 +575,6 @@ function authioBff(options) {
|
|
|
564
575
|
}
|
|
565
576
|
};
|
|
566
577
|
const readAccessToken = async (accessToken) => {
|
|
567
|
-
if (!accessToken) return void 0;
|
|
568
578
|
try {
|
|
569
579
|
return await client.readAccessTokenClaims(accessToken);
|
|
570
580
|
} catch (error) {
|
|
@@ -572,6 +582,25 @@ function authioBff(options) {
|
|
|
572
582
|
return void 0;
|
|
573
583
|
}
|
|
574
584
|
};
|
|
585
|
+
const introspectAccessToken = async (accessToken) => {
|
|
586
|
+
const { active, ...claims } = await client.introspect(accessToken, { tokenTypeHint: AuthioTokenTypes.AccessToken });
|
|
587
|
+
if (active !== true) throw new InactiveAccessTokenError();
|
|
588
|
+
return claims;
|
|
589
|
+
};
|
|
590
|
+
const introspectOpaqueAccessToken = async (accessToken) => {
|
|
591
|
+
try {
|
|
592
|
+
return await introspectAccessToken(accessToken);
|
|
593
|
+
} catch (error) {
|
|
594
|
+
if (error instanceof InactiveAccessTokenError) return void 0;
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
const accessTokenClaimsOf = async (accessToken) => {
|
|
599
|
+
if (!accessToken) return void 0;
|
|
600
|
+
if (settings.accessTokenClaims === "introspection") return introspectAccessToken(accessToken);
|
|
601
|
+
if (isReferenceToken(accessToken)) return introspectOpaqueAccessToken(accessToken);
|
|
602
|
+
return readAccessToken(accessToken);
|
|
603
|
+
};
|
|
575
604
|
const refusesIdToken = async (idToken, accessToken, subject) => {
|
|
576
605
|
try {
|
|
577
606
|
const claims = await client.validateIdToken(idToken, { accessToken });
|
|
@@ -636,6 +665,15 @@ function authioBff(options) {
|
|
|
636
665
|
return loading;
|
|
637
666
|
};
|
|
638
667
|
const isFresh = (data, skewMs) => Boolean(data.accessToken) && (data.accessTokenExpiresAt === void 0 || clock.now() < data.accessTokenExpiresAt - skewMs);
|
|
668
|
+
const refreshedAccessTokenClaims = async (accessToken) => {
|
|
669
|
+
try {
|
|
670
|
+
return await accessTokenClaimsOf(accessToken);
|
|
671
|
+
} catch (error) {
|
|
672
|
+
if (error instanceof InactiveAccessTokenError) return "inactive";
|
|
673
|
+
report(error);
|
|
674
|
+
return void 0;
|
|
675
|
+
}
|
|
676
|
+
};
|
|
639
677
|
const refresh = async (request, response, session) => {
|
|
640
678
|
const data = { ...session.data, claims: { ...session.data.claims } };
|
|
641
679
|
const unexpired = isFresh(data, 0) ? data.accessToken : void 0;
|
|
@@ -652,11 +690,12 @@ function authioBff(options) {
|
|
|
652
690
|
data.tokenType = answer.token_type ?? data.tokenType;
|
|
653
691
|
if (answer.refresh_token) data.refreshToken = answer.refresh_token;
|
|
654
692
|
if (answer.id_token) data.idToken = answer.id_token;
|
|
655
|
-
|
|
656
|
-
if (!await store.get(session.key)) {
|
|
693
|
+
const accessClaims = await refreshedAccessTokenClaims(answer.access_token);
|
|
694
|
+
if (accessClaims === "inactive" || !await store.get(session.key)) {
|
|
657
695
|
await endSessionByKey(request, response, session.key);
|
|
658
696
|
return void 0;
|
|
659
697
|
}
|
|
698
|
+
replaceAccessTokenClaims(data.claims, accessClaims);
|
|
660
699
|
expose(request, await write(session.key, data, session.expiresAt));
|
|
661
700
|
return answer.access_token;
|
|
662
701
|
} catch (error) {
|
|
@@ -777,7 +816,7 @@ function authioBff(options) {
|
|
|
777
816
|
try {
|
|
778
817
|
const { tokens, idToken } = await client.handleAuthorizationCallback(query, pending.transaction);
|
|
779
818
|
const now = clock.now();
|
|
780
|
-
const accessClaims = await
|
|
819
|
+
const accessClaims = await accessTokenClaimsOf(tokens.access_token);
|
|
781
820
|
const claims = toSessionClaims(idToken, textOf(accessClaims?.sub));
|
|
782
821
|
replaceAccessTokenClaims(claims, accessClaims);
|
|
783
822
|
if (settings.getClaimsFromUserInfoEndpoint) mergeUserInfo(claims, await client.userInfo(tokens.access_token));
|
|
@@ -974,6 +1013,14 @@ function lifetimeOf(expiresIn) {
|
|
|
974
1013
|
function textOf(value) {
|
|
975
1014
|
return typeof value === "string" && value ? value : void 0;
|
|
976
1015
|
}
|
|
1016
|
+
var InactiveAccessTokenError = class extends AuthioError {
|
|
1017
|
+
constructor() {
|
|
1018
|
+
super("Authio reports the session's access token as not active, so its claims cannot be read.");
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
function isReferenceToken(token) {
|
|
1022
|
+
return !token.includes(".");
|
|
1023
|
+
}
|
|
977
1024
|
function fixedTimeEquals(left, right) {
|
|
978
1025
|
const a = Buffer.from(left, "utf8");
|
|
979
1026
|
const b = Buffer.from(right, "utf8");
|