@hyper.authio/bff 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -9
- package/dist/index.cjs +69 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -6
- package/dist/index.d.ts +19 -6
- package/dist/index.js +70 -35
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -99,13 +99,16 @@ type AuthioBffSessionValidation = "none" | "introspection";
|
|
|
99
99
|
type AuthioBffCookieSameSite = "lax" | "strict";
|
|
100
100
|
/**
|
|
101
101
|
* Settings of the Authio BFF. The client itself (`issuer` or `authority` + `realm`, `clientId`, `clientSecret`,
|
|
102
|
-
* `
|
|
103
|
-
* `
|
|
102
|
+
* `tokenEndpointAuthMethod`, `clientPrivateKey`, `clientEncryptionKey`, `checks`, `codeChallengeMethod`,
|
|
103
|
+
* `idTokenSignedResponseAlg`, `tokenRefreshSkew`…) takes the names of `conformance/options.md` and mirrors the client
|
|
104
|
+
* as registered in Authio: an option left out is off. The BFF's own settings are those of `Authio.AspNetCore.Bff`
|
|
105
|
+
* (`Authio:Bff`).
|
|
104
106
|
*/
|
|
105
107
|
interface AuthioBffOptions extends Partial<AuthioClientOptions> {
|
|
106
108
|
/**
|
|
107
109
|
* An Authio client to use instead of creating one from the connection and credential options above. The BFF still
|
|
108
|
-
* reads `tokenRefreshSkew`, `
|
|
110
|
+
* reads `tokenRefreshSkew`, `checks`, `codeChallengeMethod`, `responseMode`, `redirectUri` and
|
|
111
|
+
* `postLogoutRedirectUri` from these options; tokens are validated and decrypted by the client.
|
|
109
112
|
*/
|
|
110
113
|
client?: AuthioClient;
|
|
111
114
|
/**
|
|
@@ -118,7 +121,10 @@ interface AuthioBffOptions extends Partial<AuthioClientOptions> {
|
|
|
118
121
|
store?: AuthioBffSessionStore;
|
|
119
122
|
/** The route prefix of the BFF endpoints. Defaults to `/bff`. */
|
|
120
123
|
endpointPrefix?: string;
|
|
121
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* The scopes asked for at sign-in, sent as configured: without `openid` there is no ID token, and the session is
|
|
126
|
+
* built from the access token (and UserInfo). Defaults to `openid profile email offline_access`.
|
|
127
|
+
*/
|
|
122
128
|
scopes?: readonly string[];
|
|
123
129
|
/** Adds the `roles` scope, so the session carries the `*_resource_access` claims flattened on `/bff/user`. */
|
|
124
130
|
requestRoles?: boolean;
|
|
@@ -174,6 +180,12 @@ interface AuthioBffOptions extends Partial<AuthioClientOptions> {
|
|
|
174
180
|
* (`sub`) a logout token names. Defaults to `true`; register that URL as the client's back-channel logout URI.
|
|
175
181
|
*/
|
|
176
182
|
backChannelLogout?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Serves `GET {endpointPrefix}/frontchannel-logout`, which Authio loads in an iframe when a session ends: it ends the
|
|
185
|
+
* sessions of the `sid` it names (with `iss` checked), or the browser's own session when it names none. Defaults to
|
|
186
|
+
* `false`; register that URL as the client's front-channel logout URI, with the session required.
|
|
187
|
+
*/
|
|
188
|
+
frontChannelLogout?: boolean;
|
|
177
189
|
/** Called with errors the BFF recovers from: a failed revocation, introspection or refresh, a refused logout token. */
|
|
178
190
|
onError?: (error: unknown) => void;
|
|
179
191
|
}
|
|
@@ -289,8 +301,9 @@ interface AuthioBff extends RequestHandler {
|
|
|
289
301
|
}
|
|
290
302
|
/**
|
|
291
303
|
* Creates the Authio BFF. Mount it with `app.use(bff)`: it serves `{endpointPrefix}/login`, `/logout`, `/user`,
|
|
292
|
-
* `/silent-login`, `/silent-login-callback`, `/backchannel-logout`,
|
|
293
|
-
* sign-out callback (`signedOutCallbackPath`), and reads the session of every other
|
|
304
|
+
* `/silent-login`, `/silent-login-callback`, `/backchannel-logout`, `/frontchannel-logout` (when turned on), the sign-in
|
|
305
|
+
* callback (`callbackPath`) and the sign-out callback (`signedOutCallbackPath`), and reads the session of every other
|
|
306
|
+
* request that carries the cookie.
|
|
294
307
|
*
|
|
295
308
|
* @throws AuthioConfigurationError naming every invalid option.
|
|
296
309
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -99,13 +99,16 @@ type AuthioBffSessionValidation = "none" | "introspection";
|
|
|
99
99
|
type AuthioBffCookieSameSite = "lax" | "strict";
|
|
100
100
|
/**
|
|
101
101
|
* Settings of the Authio BFF. The client itself (`issuer` or `authority` + `realm`, `clientId`, `clientSecret`,
|
|
102
|
-
* `
|
|
103
|
-
* `
|
|
102
|
+
* `tokenEndpointAuthMethod`, `clientPrivateKey`, `clientEncryptionKey`, `checks`, `codeChallengeMethod`,
|
|
103
|
+
* `idTokenSignedResponseAlg`, `tokenRefreshSkew`…) takes the names of `conformance/options.md` and mirrors the client
|
|
104
|
+
* as registered in Authio: an option left out is off. The BFF's own settings are those of `Authio.AspNetCore.Bff`
|
|
105
|
+
* (`Authio:Bff`).
|
|
104
106
|
*/
|
|
105
107
|
interface AuthioBffOptions extends Partial<AuthioClientOptions> {
|
|
106
108
|
/**
|
|
107
109
|
* An Authio client to use instead of creating one from the connection and credential options above. The BFF still
|
|
108
|
-
* reads `tokenRefreshSkew`, `
|
|
110
|
+
* reads `tokenRefreshSkew`, `checks`, `codeChallengeMethod`, `responseMode`, `redirectUri` and
|
|
111
|
+
* `postLogoutRedirectUri` from these options; tokens are validated and decrypted by the client.
|
|
109
112
|
*/
|
|
110
113
|
client?: AuthioClient;
|
|
111
114
|
/**
|
|
@@ -118,7 +121,10 @@ interface AuthioBffOptions extends Partial<AuthioClientOptions> {
|
|
|
118
121
|
store?: AuthioBffSessionStore;
|
|
119
122
|
/** The route prefix of the BFF endpoints. Defaults to `/bff`. */
|
|
120
123
|
endpointPrefix?: string;
|
|
121
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* The scopes asked for at sign-in, sent as configured: without `openid` there is no ID token, and the session is
|
|
126
|
+
* built from the access token (and UserInfo). Defaults to `openid profile email offline_access`.
|
|
127
|
+
*/
|
|
122
128
|
scopes?: readonly string[];
|
|
123
129
|
/** Adds the `roles` scope, so the session carries the `*_resource_access` claims flattened on `/bff/user`. */
|
|
124
130
|
requestRoles?: boolean;
|
|
@@ -174,6 +180,12 @@ interface AuthioBffOptions extends Partial<AuthioClientOptions> {
|
|
|
174
180
|
* (`sub`) a logout token names. Defaults to `true`; register that URL as the client's back-channel logout URI.
|
|
175
181
|
*/
|
|
176
182
|
backChannelLogout?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Serves `GET {endpointPrefix}/frontchannel-logout`, which Authio loads in an iframe when a session ends: it ends the
|
|
185
|
+
* sessions of the `sid` it names (with `iss` checked), or the browser's own session when it names none. Defaults to
|
|
186
|
+
* `false`; register that URL as the client's front-channel logout URI, with the session required.
|
|
187
|
+
*/
|
|
188
|
+
frontChannelLogout?: boolean;
|
|
177
189
|
/** Called with errors the BFF recovers from: a failed revocation, introspection or refresh, a refused logout token. */
|
|
178
190
|
onError?: (error: unknown) => void;
|
|
179
191
|
}
|
|
@@ -289,8 +301,9 @@ interface AuthioBff extends RequestHandler {
|
|
|
289
301
|
}
|
|
290
302
|
/**
|
|
291
303
|
* Creates the Authio BFF. Mount it with `app.use(bff)`: it serves `{endpointPrefix}/login`, `/logout`, `/user`,
|
|
292
|
-
* `/silent-login`, `/silent-login-callback`, `/backchannel-logout`,
|
|
293
|
-
* sign-out callback (`signedOutCallbackPath`), and reads the session of every other
|
|
304
|
+
* `/silent-login`, `/silent-login-callback`, `/backchannel-logout`, `/frontchannel-logout` (when turned on), the sign-in
|
|
305
|
+
* callback (`callbackPath`) and the sign-out callback (`signedOutCallbackPath`), and reads the session of every other
|
|
306
|
+
* request that carries the cookie.
|
|
294
307
|
*
|
|
295
308
|
* @throws AuthioConfigurationError naming every invalid option.
|
|
296
309
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes, createDecipheriv, createCipheriv, timingSafeEqual, createHash, hkdfSync, createHmac } from 'crypto';
|
|
2
|
-
import { AuthioClaimTypes, AuthioResourceAccessClaims, systemClock, createAuthioClient,
|
|
2
|
+
import { AuthioClaimTypes, AuthioResourceAccessClaims, systemClock, createAuthioClient, AuthioPrompts, AuthioProtocolError, AuthioTokenTypes, AuthioError, AuthioConfigurationError, AuthioDefaults, resourceAccessSourceOf, parseResourceAccess, AuthioOAuthErrors } from '@hyper.authio/client';
|
|
3
3
|
import { Readable } from 'stream';
|
|
4
4
|
import { pipeline } from 'stream/promises';
|
|
5
5
|
|
|
@@ -30,16 +30,11 @@ function toSessionClaims(idToken, fallbackSubject) {
|
|
|
30
30
|
function mergeUserInfo(claims, userInfo) {
|
|
31
31
|
for (const [type, value] of Object.entries(userInfo)) if (claims[type] === void 0 && value !== void 0 && value !== null) claims[type] = value;
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
const payload = await readPayload(accessToken, decrypt);
|
|
33
|
+
function replaceAccessTokenClaims(claims, payload) {
|
|
35
34
|
if (!payload) return;
|
|
36
35
|
for (const type of Object.keys(claims)) if (accessTokenClaimTypes.has(type)) delete claims[type];
|
|
37
36
|
for (const [type, value] of Object.entries(payload)) if (accessTokenClaimTypes.has(type) && value !== void 0 && value !== null) claims[type] = value;
|
|
38
37
|
}
|
|
39
|
-
async function readAccessTokenSubject(accessToken, decrypt) {
|
|
40
|
-
const subject = (await readPayload(accessToken, decrypt))?.sub;
|
|
41
|
-
return typeof subject === "string" && subject ? subject : void 0;
|
|
42
|
-
}
|
|
43
38
|
function toAuthioBffClaims(claims, sessionExpiresIn, logoutUrl) {
|
|
44
39
|
const entries = [];
|
|
45
40
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -60,18 +55,6 @@ function toAuthioBffClaims(claims, sessionExpiresIn, logoutUrl) {
|
|
|
60
55
|
entries.push({ type: AuthioBffClaimTypes.LogoutUrl, value: logoutUrl });
|
|
61
56
|
return entries;
|
|
62
57
|
}
|
|
63
|
-
async function readPayload(token, decrypt) {
|
|
64
|
-
if (!token) return void 0;
|
|
65
|
-
try {
|
|
66
|
-
const signed = isAuthioTokenEncrypted(token) ? await decrypt(token) : token;
|
|
67
|
-
const parts = signed.split(".");
|
|
68
|
-
if (parts.length !== 3 || !parts[1]) return void 0;
|
|
69
|
-
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
70
|
-
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : void 0;
|
|
71
|
-
} catch {
|
|
72
|
-
return void 0;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
58
|
function toText(value) {
|
|
76
59
|
return typeof value === "string" ? value : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
77
60
|
}
|
|
@@ -200,7 +183,7 @@ function resolveBffSettings(options) {
|
|
|
200
183
|
const settings = {
|
|
201
184
|
cookieSecrets,
|
|
202
185
|
endpointPrefix: `/${(options.endpointPrefix ?? AuthioBffDefaults.EndpointPrefix).replace(/^\/+|\/+$/g, "")}`,
|
|
203
|
-
scopes:
|
|
186
|
+
scopes: distinct([...options.scopes ?? AuthioBffDefaults.Scopes, ...options.requestRoles ? ["roles"] : []]),
|
|
204
187
|
getClaimsFromUserInfoEndpoint: options.getClaimsFromUserInfoEndpoint ?? false,
|
|
205
188
|
organization: nonEmpty(options.organization),
|
|
206
189
|
resource: nonEmpty(options.resource),
|
|
@@ -224,6 +207,7 @@ function resolveBffSettings(options) {
|
|
|
224
207
|
antiForgeryHeaderValue: options.antiForgeryHeaderValue ?? AuthioBffDefaults.AntiForgeryHeaderValue,
|
|
225
208
|
sessionKeyPrefix: options.sessionKeyPrefix ?? AuthioBffDefaults.SessionKeyPrefix,
|
|
226
209
|
backChannelLogout: options.backChannelLogout ?? true,
|
|
210
|
+
frontChannelLogout: options.frontChannelLogout ?? false,
|
|
227
211
|
tokenRefreshSkewMs: (options.tokenRefreshSkew ?? 60) * 1e3,
|
|
228
212
|
redirectUri: nonEmpty(options.redirectUri),
|
|
229
213
|
postLogoutRedirectUri: nonEmpty(options.postLogoutRedirectUri),
|
|
@@ -252,6 +236,8 @@ function resolveBffSettings(options) {
|
|
|
252
236
|
if (settings.maxAge !== void 0 && (!Number.isInteger(settings.maxAge) || settings.maxAge < 0))
|
|
253
237
|
problems.push("maxAge must be a non-negative whole number of seconds.");
|
|
254
238
|
if (!settings.sessionKeyPrefix.trim()) problems.push("sessionKeyPrefix is required.");
|
|
239
|
+
if (options.responseMode !== void 0 && options.responseMode !== "query")
|
|
240
|
+
problems.push("responseMode must be query: the BFF reads Authio's answer at callbackPath (web_message needs a page of the SPA's own).");
|
|
255
241
|
if (!Number.isFinite(settings.tokenRefreshSkewMs) || settings.tokenRefreshSkewMs < 0) problems.push("tokenRefreshSkew must be a non-negative number of seconds.");
|
|
256
242
|
if (problems.length > 0) throw new AuthioConfigurationError(`The Authio BFF options are not valid:
|
|
257
243
|
- ${problems.join("\n- ")}`);
|
|
@@ -261,9 +247,8 @@ function isLocalUrl(url) {
|
|
|
261
247
|
if (typeof url !== "string" || !url || /[\u0000-\u001f\u007f-\u009f]/.test(url)) return false;
|
|
262
248
|
return url[0] === "/" && (url.length === 1 || url[1] !== "/" && url[1] !== "\\");
|
|
263
249
|
}
|
|
264
|
-
function
|
|
265
|
-
|
|
266
|
-
return Object.freeze(distinct.includes("openid") ? distinct : ["openid", ...distinct]);
|
|
250
|
+
function distinct(scopes) {
|
|
251
|
+
return Object.freeze([...new Set(scopes.map((scope) => scope.trim()).filter(Boolean))]);
|
|
267
252
|
}
|
|
268
253
|
function nonEmpty(value) {
|
|
269
254
|
return value?.trim() ? value : void 0;
|
|
@@ -532,12 +517,9 @@ function authioBff(options) {
|
|
|
532
517
|
const protector = createAuthioBffProtector(settings.cookieSecrets);
|
|
533
518
|
const locks = new AuthioBffSessionLocks();
|
|
534
519
|
const exchanges = /* @__PURE__ */ new Map();
|
|
535
|
-
const decrypt = createAuthioTokenDecrypter(
|
|
536
|
-
options.clientEncryptionKey ?? (options.clientEncryptionKeyPath ? encryptionKeyFromFile(options.clientEncryptionKeyPath) : void 0)
|
|
537
|
-
);
|
|
538
520
|
const prefix = settings.endpointPrefix;
|
|
539
521
|
const loginLifetimeMs = AuthioBffDefaults.LoginLifetime * 1e3;
|
|
540
|
-
const loginCookieName = (state) => `${settings.cookieName}.login.${createHash("sha256").update(state).digest("base64url").slice(0, 16)}`;
|
|
522
|
+
const loginCookieName = (state) => state ? `${settings.cookieName}.login.${createHash("sha256").update(state).digest("base64url").slice(0, 16)}` : `${settings.cookieName}.login`;
|
|
541
523
|
const toInfo = (session) => Object.freeze({
|
|
542
524
|
subject: textOf(session.data.claims.sub),
|
|
543
525
|
sessionId: textOf(session.data.claims.sid),
|
|
@@ -581,6 +563,26 @@ function authioBff(options) {
|
|
|
581
563
|
return;
|
|
582
564
|
}
|
|
583
565
|
};
|
|
566
|
+
const readAccessToken = async (accessToken) => {
|
|
567
|
+
if (!accessToken) return void 0;
|
|
568
|
+
try {
|
|
569
|
+
return await client.readAccessTokenClaims(accessToken);
|
|
570
|
+
} catch (error) {
|
|
571
|
+
report(error);
|
|
572
|
+
return void 0;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
const refusesIdToken = async (idToken, accessToken, subject) => {
|
|
576
|
+
try {
|
|
577
|
+
const claims = await client.validateIdToken(idToken, { accessToken });
|
|
578
|
+
if (typeof subject === "string" && claims.sub !== subject)
|
|
579
|
+
throw new AuthioError(`The ID token of the refresh names '${claims.sub}', not the session's person.`);
|
|
580
|
+
return false;
|
|
581
|
+
} catch (error) {
|
|
582
|
+
report(error);
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
};
|
|
584
586
|
const isActive = async (data) => {
|
|
585
587
|
const token = data.refreshToken ?? data.accessToken;
|
|
586
588
|
if (!token) return void 0;
|
|
@@ -641,12 +643,16 @@ function authioBff(options) {
|
|
|
641
643
|
try {
|
|
642
644
|
const issuedAt = clock.now();
|
|
643
645
|
const answer = await client.tokens.refresh({ refreshToken: data.refreshToken });
|
|
646
|
+
if (answer.id_token && await refusesIdToken(answer.id_token, answer.access_token, data.claims.sub)) {
|
|
647
|
+
await endSessionByKey(request, response, session.key);
|
|
648
|
+
return void 0;
|
|
649
|
+
}
|
|
644
650
|
data.accessToken = answer.access_token;
|
|
645
651
|
data.accessTokenExpiresAt = issuedAt + lifetimeOf(answer.expires_in);
|
|
646
652
|
data.tokenType = answer.token_type ?? data.tokenType;
|
|
647
653
|
if (answer.refresh_token) data.refreshToken = answer.refresh_token;
|
|
648
654
|
if (answer.id_token) data.idToken = answer.id_token;
|
|
649
|
-
|
|
655
|
+
replaceAccessTokenClaims(data.claims, await readAccessToken(answer.access_token));
|
|
650
656
|
if (!await store.get(session.key)) {
|
|
651
657
|
await endSessionByKey(request, response, session.key);
|
|
652
658
|
return void 0;
|
|
@@ -714,6 +720,9 @@ function authioBff(options) {
|
|
|
714
720
|
const startLogin = async (request, response, parameters) => {
|
|
715
721
|
const { url, transaction } = await client.createAuthorizationUrl({
|
|
716
722
|
redirectUri: settings.redirectUri ?? requestBase(request) + settings.callbackPath,
|
|
723
|
+
checks: options.checks,
|
|
724
|
+
codeChallengeMethod: options.codeChallengeMethod,
|
|
725
|
+
responseMode: options.responseMode,
|
|
717
726
|
scopes: settings.scopes,
|
|
718
727
|
resource: parameters.resource ?? settings.resource,
|
|
719
728
|
organization: parameters.organization ?? settings.organization,
|
|
@@ -752,22 +761,25 @@ function authioBff(options) {
|
|
|
752
761
|
const silentLogin = (request, response) => startLogin(request, response, { returnUrl: `${request.baseUrl ?? ""}${prefix}/silent-login-callback`, prompt: "none" });
|
|
753
762
|
const callback = async (request, response) => {
|
|
754
763
|
const query = readQuery(request);
|
|
755
|
-
const state = query.get("state")
|
|
756
|
-
const
|
|
757
|
-
const
|
|
764
|
+
const state = query.get("state") || void 0;
|
|
765
|
+
const cookies = readCookies(request);
|
|
766
|
+
const cookieName = [loginCookieName(state), loginCookieName(void 0)].find((name) => cookies.has(name));
|
|
767
|
+
const sealed = cookieName ? cookies.get(cookieName) : void 0;
|
|
758
768
|
const pending = sealed ? protector.open("login", sealed) : void 0;
|
|
759
769
|
if (cookieName && sealed) setCookie(response, cookieName, "", "lax", 0);
|
|
760
770
|
response.set("Cache-Control", "no-store");
|
|
761
771
|
const fail = (target2, error) => response.redirect(302, withQueryParameter(target2, "error", error));
|
|
762
|
-
if (!pending || pending.transaction?.state !== state || pending.expiresAt <= clock.now())
|
|
772
|
+
if (!pending || pending.transaction?.state !== void 0 && pending.transaction.state !== state || pending.expiresAt <= clock.now())
|
|
773
|
+
return fail(settings.defaultReturnUrl, "login_failed");
|
|
763
774
|
const target = isLocalUrl(pending.returnUrl) ? pending.returnUrl : settings.defaultReturnUrl;
|
|
764
775
|
if (settings.requireIssuerParameter && !query.has("iss") && !query.has("error")) return fail(target, "login_failed");
|
|
765
776
|
let data;
|
|
766
777
|
try {
|
|
767
778
|
const { tokens, idToken } = await client.handleAuthorizationCallback(query, pending.transaction);
|
|
768
779
|
const now = clock.now();
|
|
769
|
-
const
|
|
770
|
-
|
|
780
|
+
const accessClaims = await readAccessToken(tokens.access_token);
|
|
781
|
+
const claims = toSessionClaims(idToken, textOf(accessClaims?.sub));
|
|
782
|
+
replaceAccessTokenClaims(claims, accessClaims);
|
|
771
783
|
if (settings.getClaimsFromUserInfoEndpoint) mergeUserInfo(claims, await client.userInfo(tokens.access_token));
|
|
772
784
|
data = {
|
|
773
785
|
publicId: randomBytes(16).toString("hex"),
|
|
@@ -869,6 +881,28 @@ function authioBff(options) {
|
|
|
869
881
|
else if (logout2.subject) await store.deleteBySubject(logout2.subject);
|
|
870
882
|
response.status(200).end();
|
|
871
883
|
};
|
|
884
|
+
const frontChannelLogout = async (request, response) => {
|
|
885
|
+
response.set("Cache-Control", "no-store");
|
|
886
|
+
if (!settings.frontChannelLogout) return void response.status(404).end();
|
|
887
|
+
let sessionId;
|
|
888
|
+
try {
|
|
889
|
+
sessionId = client.validateFrontChannelLogout(readQuery(request)).sessionId;
|
|
890
|
+
} catch (error) {
|
|
891
|
+
report(error);
|
|
892
|
+
if (!(error instanceof AuthioError)) throw error;
|
|
893
|
+
return sendError(response, 400, "invalid_request", "The front-channel logout request is not valid.");
|
|
894
|
+
}
|
|
895
|
+
const cookie = readCookies(request).get(settings.cookieName);
|
|
896
|
+
const id = cookie ? protector.unsign(cookie) : void 0;
|
|
897
|
+
const key = id ? settings.sessionKeyPrefix + id : void 0;
|
|
898
|
+
if (sessionId) await store.deleteBySessionId(sessionId);
|
|
899
|
+
else if (key) {
|
|
900
|
+
await store.delete(key);
|
|
901
|
+
dropExchanges(key);
|
|
902
|
+
}
|
|
903
|
+
if (key && !await store.get(key)) clearCookie(response);
|
|
904
|
+
response.status(200).type("text/html; charset=utf-8").send('<!doctype html><html><head><meta charset="utf-8"><title>Authio</title></head><body></body></html>');
|
|
905
|
+
};
|
|
872
906
|
const routes = /* @__PURE__ */ new Map([
|
|
873
907
|
[settings.callbackPath, { methods: ["GET"], run: callback }],
|
|
874
908
|
[settings.signedOutCallbackPath, { methods: ["GET"], run: signedOutCallback }],
|
|
@@ -877,7 +911,8 @@ function authioBff(options) {
|
|
|
877
911
|
[`${prefix}/user`, { methods: ["GET"], run: user }],
|
|
878
912
|
[`${prefix}/silent-login`, { methods: ["GET"], run: silentLogin }],
|
|
879
913
|
[`${prefix}/silent-login-callback`, { methods: ["GET"], run: silentLoginCallback }],
|
|
880
|
-
[`${prefix}/backchannel-logout`, { methods: ["POST"], run: backChannelLogout }]
|
|
914
|
+
[`${prefix}/backchannel-logout`, { methods: ["POST"], run: backChannelLogout }],
|
|
915
|
+
[`${prefix}/frontchannel-logout`, { methods: ["GET"], run: frontChannelLogout }]
|
|
881
916
|
]);
|
|
882
917
|
const middleware = (request, response, next) => {
|
|
883
918
|
const route = routes.get(request.path.replace(/(.)\/+$/, "$1"));
|