@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/README.md
CHANGED
|
@@ -25,12 +25,16 @@ and refresh tokens outlive the page. With a BFF:
|
|
|
25
25
|
|
|
26
26
|
## Register the client at Authio
|
|
27
27
|
|
|
28
|
-
- **Type**: confidential, with a secret (`client_secret_basic` by default, or `
|
|
28
|
+
- **Type**: confidential, with a secret (`client_secret_basic` by default, or `tokenEndpointAuthMethod:
|
|
29
29
|
"client_secret_post"`), or with `private_key_jwt` (`clientPrivateKey` or `clientPrivateKeyPath`, and
|
|
30
30
|
`clientKeyId`).
|
|
31
|
+
- **PKCE**: required with S256 is the recommended setting, mirrored by `checks: ["pkce", "state", "nonce"]` and
|
|
32
|
+
`codeChallengeMethod: "S256"`. The BFF checks only what `checks` lists: left out, it checks nothing.
|
|
31
33
|
- **Redirect URI**: `https://<your-app>/signin-oidc` (`callbackPath`).
|
|
32
34
|
- **Post-logout redirect URI**: `https://<your-app>/signout-callback-oidc` (`signedOutCallbackPath`).
|
|
33
35
|
- **Back-channel logout URI**: `https://<your-app>/bff/backchannel-logout`.
|
|
36
|
+
- **Front-channel logout URI** (optional, with `frontChannelLogout: true`): `https://<your-app>/bff/frontchannel-logout`,
|
|
37
|
+
with the session required so Authio sends `iss` and `sid`.
|
|
34
38
|
- **Scopes**: `openid profile email offline_access` (`offline_access` gives the refresh token), plus `roles` if you set
|
|
35
39
|
`requestRoles`, plus your API scopes.
|
|
36
40
|
- **Resources** (optional): the API resource identifiers you ask for with `resource` or use with remote APIs; allow
|
|
@@ -49,12 +53,14 @@ const bff = authioBff({
|
|
|
49
53
|
realm: "acme",
|
|
50
54
|
clientId: "acme-web",
|
|
51
55
|
clientSecret: () => process.env.AUTHIO_CLIENT_SECRET!, // read on every call: a rotated secret needs no restart
|
|
56
|
+
checks: ["pkce", "state", "nonce"], // the client requires PKCE with S256: mirror it, nothing is checked by default
|
|
57
|
+
codeChallengeMethod: "S256",
|
|
52
58
|
cookieSecret: process.env.BFF_COOKIE_SECRET!, // 32+ characters; an array rotates them
|
|
53
59
|
requestRoles: true,
|
|
54
60
|
resource: "https://orders.example.com", // the session's token is addressed to the orders API
|
|
55
61
|
});
|
|
56
62
|
|
|
57
|
-
app.use(bff); // /bff/login, /bff/logout, /bff/user, /bff/silent-login, /bff/backchannel-logout, /signin-oidc, /signout-callback-oidc
|
|
63
|
+
app.use(bff); // /bff/login, /bff/logout, /bff/user, /bff/silent-login, /bff/backchannel-logout, /bff/frontchannel-logout, /signin-oidc, /signout-callback-oidc
|
|
58
64
|
|
|
59
65
|
// Remote APIs: mount them before any body parser, so the body is streamed through.
|
|
60
66
|
app.use("/api/orders", bff.remoteApi("https://orders.example.com"));
|
|
@@ -99,11 +105,12 @@ if (response.status === 401) {
|
|
|
99
105
|
| Endpoint | Behaviour |
|
|
100
106
|
| --- | --- |
|
|
101
107
|
| `GET /bff/login` | Starts the sign-in. `returnUrl` (local only, else `400`), `organization`, `prompt` (`none`, `login`, `select_account`, `consent`), `login_hint`, `ui_locales`, `max_age`, `resource` (must be `resource` or one of `allowedResources`, else `400 invalid_target`). A failure comes back as `returnUrl?error=<code>` (`access_denied`, `login_required`, `login_failed`…). |
|
|
102
|
-
| `GET /signin-oidc` | The callback:
|
|
108
|
+
| `GET /signin-oidc` | The callback: finds the sealed sign-in cookie (by `state` with the `state` check), checks the RFC 9207 `iss` and the configured `checks`, redeems the code (with the PKCE verifier with `pkce`), validates the ID token (`nonce` with `nonce`, `at_hash`), reads the access token's claims once its signature verifies, creates the session and replaces any previous one. |
|
|
103
109
|
| `GET /bff/user` | With `X-CSRF: 1`: `[{ "type", "value" }]` — the session's claims (strings, one entry per array element), one `role` per Authio role and one `permission` (`resource:method`) per grant from the `*_resource_access` claims, `bff:session_expires_in` (seconds) and `bff:logout_url`. `401` without the header (`antiforgery_required`) or without a session (`unauthorized`), never a redirect. |
|
|
104
110
|
| `GET\|POST /bff/logout` | With the session's `sid` (from `bff:logout_url`) or `X-CSRF: 1`, optionally `returnUrl`: revokes the refresh token, deletes the session, clears the cookie and redirects to Authio's end-session endpoint with `id_token_hint`, `post_logout_redirect_uri` and a sealed `state`; `/signout-callback-oidc` then sends the browser to `returnUrl`. Without `sid` or the header, a signed-in person cannot be logged out by another site (`400`). |
|
|
105
111
|
| `GET /bff/silent-login` | A `prompt=none` sign-in for a hidden iframe; `/bff/silent-login-callback` posts `{ source: "authio-bff-silent-login", isLoggedIn, error }` to the parent window (same origin only). |
|
|
106
112
|
| `POST /bff/backchannel-logout` | Authio's back-channel logout: validates the `logout_token` (`validateLogoutToken`) and deletes every session of that Authio session (`sid`), or of the person (`sub`) when the token names no `sid`. `400 invalid_request` for a missing or invalid token. |
|
|
113
|
+
| `GET /bff/frontchannel-logout` | With `frontChannelLogout: true`, Authio's front-channel logout iframe: checks `iss` (`validateFrontChannelLogout`) and deletes every session of the `sid` it names, or the browser's own session when it names none; answers an empty page. `400 invalid_request` for another issuer or a `sid` without `iss`; `404` when turned off. |
|
|
107
114
|
|
|
108
115
|
## Calling APIs
|
|
109
116
|
|
|
@@ -142,7 +149,11 @@ refresh token, which stays addressed to the BFF.
|
|
|
142
149
|
- **Refresh**: the access token is refreshed `tokenRefreshSkew` (60 s) before it expires, under a lock per session, so
|
|
143
150
|
concurrent requests share one refresh (Authio rotates refresh tokens and treats a late reuse as a replay). The rotated
|
|
144
151
|
refresh token is stored before the lock is released. A refused refresh (`invalid_grant`) ends the session; a network
|
|
145
|
-
error keeps the current token while it lasts.
|
|
152
|
+
error keeps the current token while it lasts. An ID token the refresh returns is validated (signature, `iss`, `aud`,
|
|
153
|
+
`exp`, `at_hash`, the same `sub`) before it is stored; one that does not validate ends the session.
|
|
154
|
+
- **Claims**: the session's claims are the validated ID token's, plus the claims only the access token carries
|
|
155
|
+
(`*_resource_access`, `org_id`, `org_alias`, `account_id`) once the access token's signature, `iss` and `exp` verify
|
|
156
|
+
(`client.readAccessTokenClaims`), plus UserInfo's with `getClaimsFromUserInfoEndpoint`.
|
|
146
157
|
- **Session check** (`sessionValidation: "introspection"`, the default): every `sessionValidationInterval` (300 s) the
|
|
147
158
|
refresh token (or the access token when there is none) is introspected; an inactive answer ends the session. Needs a
|
|
148
159
|
confidential client.
|
|
@@ -194,16 +205,31 @@ sessions) or rely on Authio's short refresh-token reuse leeway.
|
|
|
194
205
|
## Options
|
|
195
206
|
|
|
196
207
|
The client takes the names of [`conformance/options.md`](../../conformance/options.md) (`issuer` or `authority` +
|
|
197
|
-
`realm`, `clientId`, `clientSecret`, `
|
|
198
|
-
`clientEncryptionKey`, `
|
|
199
|
-
|
|
208
|
+
`realm`, `clientId`, `clientSecret`, `tokenEndpointAuthMethod`, `clientPrivateKey`, `clientKeyId`,
|
|
209
|
+
`clientEncryptionKey`, `encryptionAlgorithms`, `encryptionMethods`, `idTokenSignedResponseAlg`, `tokenRefreshSkew`,
|
|
210
|
+
`requireHttps`, `clock`…), or an existing `client`. They mirror the client as it is registered in Authio, and an
|
|
211
|
+
option left out is off:
|
|
212
|
+
|
|
213
|
+
| In the console | Options |
|
|
214
|
+
| --- | --- |
|
|
215
|
+
| PKCE required, S256 | `checks: ["pkce", "state", "nonce"]`, `codeChallengeMethod: "S256"` |
|
|
216
|
+
| PKCE required, plain only | `checks: ["pkce", "state", "nonce"]`, `codeChallengeMethod: "plain"` |
|
|
217
|
+
| PKCE off | `checks: ["state", "nonce"]` |
|
|
218
|
+
| `client_secret_post` | `clientSecret`, `tokenEndpointAuthMethod: "client_secret_post"` |
|
|
219
|
+
| ID token signing algorithm HS256 | `clientSecret`, `idTokenSignedResponseAlg: "HS256"` |
|
|
220
|
+
| Access token signing algorithm HS256 | `clientSecret`, `accessTokenSignedResponseAlg: "HS256"` |
|
|
221
|
+
| Front-channel logout URI with the session required | `frontChannelLogout: true` |
|
|
222
|
+
|
|
223
|
+
`checks`, `codeChallengeMethod` and `responseMode` (only `query`: the BFF reads the callback itself) are read even with
|
|
224
|
+
an existing `client`. The BFF's own options are those of `Authio.AspNetCore.Bff` (`Authio:Bff`), in camelCase, with
|
|
225
|
+
durations in seconds:
|
|
200
226
|
|
|
201
227
|
| Option | Default | |
|
|
202
228
|
| --- | --- | --- |
|
|
203
229
|
| `cookieSecret` | — (required) | 32+ characters; an array rotates: the first protects, every one is accepted. |
|
|
204
230
|
| `store` | in memory | `AuthioBffSessionStore`. |
|
|
205
231
|
| `endpointPrefix` | `/bff` | Where the BFF endpoints are served. |
|
|
206
|
-
| `scopes` | `openid profile email offline_access` | `openid` is
|
|
232
|
+
| `scopes` | `openid profile email offline_access` | Sent as configured; `openid` is not added. Without it there is no ID token, and the session is built from the verified access token. |
|
|
207
233
|
| `requestRoles` | `false` | Adds `roles`, so the session carries `*_resource_access`. |
|
|
208
234
|
| `getClaimsFromUserInfoEndpoint` | `false` | Adds UserInfo's claims to the session. |
|
|
209
235
|
| `organization`, `resource`, `prompt`, `uiLocales`, `maxAge` | — | Sent on every sign-in unless the login request names one. |
|
|
@@ -220,7 +246,8 @@ are those of `Authio.AspNetCore.Bff` (`Authio:Bff`), in camelCase, with duration
|
|
|
220
246
|
| `antiForgeryHeaderName`, `antiForgeryHeaderValue` | `X-CSRF`, `1` | |
|
|
221
247
|
| `sessionKeyPrefix` | `authio-bff:` | The prefix of the store keys. |
|
|
222
248
|
| `backChannelLogout` | `true` | Serves `/bff/backchannel-logout`. |
|
|
223
|
-
| `
|
|
249
|
+
| `frontChannelLogout` | `false` | Serves `/bff/frontchannel-logout`. |
|
|
250
|
+
| `onError` | — | Errors the BFF recovers from (failed revocation, introspection or refresh, refused logout token or ID token, an access token that does not verify, failed sign-in). |
|
|
224
251
|
|
|
225
252
|
Every invalid option is named at once, in one `AuthioConfigurationError`, when `authioBff()` runs.
|
|
226
253
|
|
package/dist/index.cjs
CHANGED
|
@@ -32,16 +32,11 @@ function toSessionClaims(idToken, fallbackSubject) {
|
|
|
32
32
|
function mergeUserInfo(claims, userInfo) {
|
|
33
33
|
for (const [type, value] of Object.entries(userInfo)) if (claims[type] === void 0 && value !== void 0 && value !== null) claims[type] = value;
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
const payload = await readPayload(accessToken, decrypt);
|
|
35
|
+
function replaceAccessTokenClaims(claims, payload) {
|
|
37
36
|
if (!payload) return;
|
|
38
37
|
for (const type of Object.keys(claims)) if (accessTokenClaimTypes.has(type)) delete claims[type];
|
|
39
38
|
for (const [type, value] of Object.entries(payload)) if (accessTokenClaimTypes.has(type) && value !== void 0 && value !== null) claims[type] = value;
|
|
40
39
|
}
|
|
41
|
-
async function readAccessTokenSubject(accessToken, decrypt) {
|
|
42
|
-
const subject = (await readPayload(accessToken, decrypt))?.sub;
|
|
43
|
-
return typeof subject === "string" && subject ? subject : void 0;
|
|
44
|
-
}
|
|
45
40
|
function toAuthioBffClaims(claims, sessionExpiresIn, logoutUrl) {
|
|
46
41
|
const entries = [];
|
|
47
42
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -62,18 +57,6 @@ function toAuthioBffClaims(claims, sessionExpiresIn, logoutUrl) {
|
|
|
62
57
|
entries.push({ type: AuthioBffClaimTypes.LogoutUrl, value: logoutUrl });
|
|
63
58
|
return entries;
|
|
64
59
|
}
|
|
65
|
-
async function readPayload(token, decrypt) {
|
|
66
|
-
if (!token) return void 0;
|
|
67
|
-
try {
|
|
68
|
-
const signed = client.isAuthioTokenEncrypted(token) ? await decrypt(token) : token;
|
|
69
|
-
const parts = signed.split(".");
|
|
70
|
-
if (parts.length !== 3 || !parts[1]) return void 0;
|
|
71
|
-
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
72
|
-
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : void 0;
|
|
73
|
-
} catch {
|
|
74
|
-
return void 0;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
60
|
function toText(value) {
|
|
78
61
|
return typeof value === "string" ? value : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
79
62
|
}
|
|
@@ -202,7 +185,7 @@ function resolveBffSettings(options) {
|
|
|
202
185
|
const settings = {
|
|
203
186
|
cookieSecrets,
|
|
204
187
|
endpointPrefix: `/${(options.endpointPrefix ?? AuthioBffDefaults.EndpointPrefix).replace(/^\/+|\/+$/g, "")}`,
|
|
205
|
-
scopes:
|
|
188
|
+
scopes: distinct([...options.scopes ?? AuthioBffDefaults.Scopes, ...options.requestRoles ? ["roles"] : []]),
|
|
206
189
|
getClaimsFromUserInfoEndpoint: options.getClaimsFromUserInfoEndpoint ?? false,
|
|
207
190
|
organization: nonEmpty(options.organization),
|
|
208
191
|
resource: nonEmpty(options.resource),
|
|
@@ -226,6 +209,7 @@ function resolveBffSettings(options) {
|
|
|
226
209
|
antiForgeryHeaderValue: options.antiForgeryHeaderValue ?? AuthioBffDefaults.AntiForgeryHeaderValue,
|
|
227
210
|
sessionKeyPrefix: options.sessionKeyPrefix ?? AuthioBffDefaults.SessionKeyPrefix,
|
|
228
211
|
backChannelLogout: options.backChannelLogout ?? true,
|
|
212
|
+
frontChannelLogout: options.frontChannelLogout ?? false,
|
|
229
213
|
tokenRefreshSkewMs: (options.tokenRefreshSkew ?? 60) * 1e3,
|
|
230
214
|
redirectUri: nonEmpty(options.redirectUri),
|
|
231
215
|
postLogoutRedirectUri: nonEmpty(options.postLogoutRedirectUri),
|
|
@@ -254,6 +238,8 @@ function resolveBffSettings(options) {
|
|
|
254
238
|
if (settings.maxAge !== void 0 && (!Number.isInteger(settings.maxAge) || settings.maxAge < 0))
|
|
255
239
|
problems.push("maxAge must be a non-negative whole number of seconds.");
|
|
256
240
|
if (!settings.sessionKeyPrefix.trim()) problems.push("sessionKeyPrefix is required.");
|
|
241
|
+
if (options.responseMode !== void 0 && options.responseMode !== "query")
|
|
242
|
+
problems.push("responseMode must be query: the BFF reads Authio's answer at callbackPath (web_message needs a page of the SPA's own).");
|
|
257
243
|
if (!Number.isFinite(settings.tokenRefreshSkewMs) || settings.tokenRefreshSkewMs < 0) problems.push("tokenRefreshSkew must be a non-negative number of seconds.");
|
|
258
244
|
if (problems.length > 0) throw new client.AuthioConfigurationError(`The Authio BFF options are not valid:
|
|
259
245
|
- ${problems.join("\n- ")}`);
|
|
@@ -263,9 +249,8 @@ function isLocalUrl(url) {
|
|
|
263
249
|
if (typeof url !== "string" || !url || /[\u0000-\u001f\u007f-\u009f]/.test(url)) return false;
|
|
264
250
|
return url[0] === "/" && (url.length === 1 || url[1] !== "/" && url[1] !== "\\");
|
|
265
251
|
}
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
return Object.freeze(distinct.includes("openid") ? distinct : ["openid", ...distinct]);
|
|
252
|
+
function distinct(scopes) {
|
|
253
|
+
return Object.freeze([...new Set(scopes.map((scope) => scope.trim()).filter(Boolean))]);
|
|
269
254
|
}
|
|
270
255
|
function nonEmpty(value) {
|
|
271
256
|
return value?.trim() ? value : void 0;
|
|
@@ -534,12 +519,9 @@ function authioBff(options) {
|
|
|
534
519
|
const protector = createAuthioBffProtector(settings.cookieSecrets);
|
|
535
520
|
const locks = new AuthioBffSessionLocks();
|
|
536
521
|
const exchanges = /* @__PURE__ */ new Map();
|
|
537
|
-
const decrypt = client.createAuthioTokenDecrypter(
|
|
538
|
-
options.clientEncryptionKey ?? (options.clientEncryptionKeyPath ? client.encryptionKeyFromFile(options.clientEncryptionKeyPath) : void 0)
|
|
539
|
-
);
|
|
540
522
|
const prefix = settings.endpointPrefix;
|
|
541
523
|
const loginLifetimeMs = AuthioBffDefaults.LoginLifetime * 1e3;
|
|
542
|
-
const loginCookieName = (state) => `${settings.cookieName}.login.${crypto.createHash("sha256").update(state).digest("base64url").slice(0, 16)}`;
|
|
524
|
+
const loginCookieName = (state) => state ? `${settings.cookieName}.login.${crypto.createHash("sha256").update(state).digest("base64url").slice(0, 16)}` : `${settings.cookieName}.login`;
|
|
543
525
|
const toInfo = (session) => Object.freeze({
|
|
544
526
|
subject: textOf(session.data.claims.sub),
|
|
545
527
|
sessionId: textOf(session.data.claims.sid),
|
|
@@ -583,6 +565,26 @@ function authioBff(options) {
|
|
|
583
565
|
return;
|
|
584
566
|
}
|
|
585
567
|
};
|
|
568
|
+
const readAccessToken = async (accessToken) => {
|
|
569
|
+
if (!accessToken) return void 0;
|
|
570
|
+
try {
|
|
571
|
+
return await client$1.readAccessTokenClaims(accessToken);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
report(error);
|
|
574
|
+
return void 0;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
const refusesIdToken = async (idToken, accessToken, subject) => {
|
|
578
|
+
try {
|
|
579
|
+
const claims = await client$1.validateIdToken(idToken, { accessToken });
|
|
580
|
+
if (typeof subject === "string" && claims.sub !== subject)
|
|
581
|
+
throw new client.AuthioError(`The ID token of the refresh names '${claims.sub}', not the session's person.`);
|
|
582
|
+
return false;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
report(error);
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
586
588
|
const isActive = async (data) => {
|
|
587
589
|
const token = data.refreshToken ?? data.accessToken;
|
|
588
590
|
if (!token) return void 0;
|
|
@@ -643,12 +645,16 @@ function authioBff(options) {
|
|
|
643
645
|
try {
|
|
644
646
|
const issuedAt = clock.now();
|
|
645
647
|
const answer = await client$1.tokens.refresh({ refreshToken: data.refreshToken });
|
|
648
|
+
if (answer.id_token && await refusesIdToken(answer.id_token, answer.access_token, data.claims.sub)) {
|
|
649
|
+
await endSessionByKey(request, response, session.key);
|
|
650
|
+
return void 0;
|
|
651
|
+
}
|
|
646
652
|
data.accessToken = answer.access_token;
|
|
647
653
|
data.accessTokenExpiresAt = issuedAt + lifetimeOf(answer.expires_in);
|
|
648
654
|
data.tokenType = answer.token_type ?? data.tokenType;
|
|
649
655
|
if (answer.refresh_token) data.refreshToken = answer.refresh_token;
|
|
650
656
|
if (answer.id_token) data.idToken = answer.id_token;
|
|
651
|
-
|
|
657
|
+
replaceAccessTokenClaims(data.claims, await readAccessToken(answer.access_token));
|
|
652
658
|
if (!await store.get(session.key)) {
|
|
653
659
|
await endSessionByKey(request, response, session.key);
|
|
654
660
|
return void 0;
|
|
@@ -716,6 +722,9 @@ function authioBff(options) {
|
|
|
716
722
|
const startLogin = async (request, response, parameters) => {
|
|
717
723
|
const { url, transaction } = await client$1.createAuthorizationUrl({
|
|
718
724
|
redirectUri: settings.redirectUri ?? requestBase(request) + settings.callbackPath,
|
|
725
|
+
checks: options.checks,
|
|
726
|
+
codeChallengeMethod: options.codeChallengeMethod,
|
|
727
|
+
responseMode: options.responseMode,
|
|
719
728
|
scopes: settings.scopes,
|
|
720
729
|
resource: parameters.resource ?? settings.resource,
|
|
721
730
|
organization: parameters.organization ?? settings.organization,
|
|
@@ -754,22 +763,25 @@ function authioBff(options) {
|
|
|
754
763
|
const silentLogin = (request, response) => startLogin(request, response, { returnUrl: `${request.baseUrl ?? ""}${prefix}/silent-login-callback`, prompt: "none" });
|
|
755
764
|
const callback = async (request, response) => {
|
|
756
765
|
const query = readQuery(request);
|
|
757
|
-
const state = query.get("state")
|
|
758
|
-
const
|
|
759
|
-
const
|
|
766
|
+
const state = query.get("state") || void 0;
|
|
767
|
+
const cookies = readCookies(request);
|
|
768
|
+
const cookieName = [loginCookieName(state), loginCookieName(void 0)].find((name) => cookies.has(name));
|
|
769
|
+
const sealed = cookieName ? cookies.get(cookieName) : void 0;
|
|
760
770
|
const pending = sealed ? protector.open("login", sealed) : void 0;
|
|
761
771
|
if (cookieName && sealed) setCookie(response, cookieName, "", "lax", 0);
|
|
762
772
|
response.set("Cache-Control", "no-store");
|
|
763
773
|
const fail = (target2, error) => response.redirect(302, withQueryParameter(target2, "error", error));
|
|
764
|
-
if (!pending || pending.transaction?.state !== state || pending.expiresAt <= clock.now())
|
|
774
|
+
if (!pending || pending.transaction?.state !== void 0 && pending.transaction.state !== state || pending.expiresAt <= clock.now())
|
|
775
|
+
return fail(settings.defaultReturnUrl, "login_failed");
|
|
765
776
|
const target = isLocalUrl(pending.returnUrl) ? pending.returnUrl : settings.defaultReturnUrl;
|
|
766
777
|
if (settings.requireIssuerParameter && !query.has("iss") && !query.has("error")) return fail(target, "login_failed");
|
|
767
778
|
let data;
|
|
768
779
|
try {
|
|
769
780
|
const { tokens, idToken } = await client$1.handleAuthorizationCallback(query, pending.transaction);
|
|
770
781
|
const now = clock.now();
|
|
771
|
-
const
|
|
772
|
-
|
|
782
|
+
const accessClaims = await readAccessToken(tokens.access_token);
|
|
783
|
+
const claims = toSessionClaims(idToken, textOf(accessClaims?.sub));
|
|
784
|
+
replaceAccessTokenClaims(claims, accessClaims);
|
|
773
785
|
if (settings.getClaimsFromUserInfoEndpoint) mergeUserInfo(claims, await client$1.userInfo(tokens.access_token));
|
|
774
786
|
data = {
|
|
775
787
|
publicId: crypto.randomBytes(16).toString("hex"),
|
|
@@ -871,6 +883,28 @@ function authioBff(options) {
|
|
|
871
883
|
else if (logout2.subject) await store.deleteBySubject(logout2.subject);
|
|
872
884
|
response.status(200).end();
|
|
873
885
|
};
|
|
886
|
+
const frontChannelLogout = async (request, response) => {
|
|
887
|
+
response.set("Cache-Control", "no-store");
|
|
888
|
+
if (!settings.frontChannelLogout) return void response.status(404).end();
|
|
889
|
+
let sessionId;
|
|
890
|
+
try {
|
|
891
|
+
sessionId = client$1.validateFrontChannelLogout(readQuery(request)).sessionId;
|
|
892
|
+
} catch (error) {
|
|
893
|
+
report(error);
|
|
894
|
+
if (!(error instanceof client.AuthioError)) throw error;
|
|
895
|
+
return sendError(response, 400, "invalid_request", "The front-channel logout request is not valid.");
|
|
896
|
+
}
|
|
897
|
+
const cookie = readCookies(request).get(settings.cookieName);
|
|
898
|
+
const id = cookie ? protector.unsign(cookie) : void 0;
|
|
899
|
+
const key = id ? settings.sessionKeyPrefix + id : void 0;
|
|
900
|
+
if (sessionId) await store.deleteBySessionId(sessionId);
|
|
901
|
+
else if (key) {
|
|
902
|
+
await store.delete(key);
|
|
903
|
+
dropExchanges(key);
|
|
904
|
+
}
|
|
905
|
+
if (key && !await store.get(key)) clearCookie(response);
|
|
906
|
+
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>');
|
|
907
|
+
};
|
|
874
908
|
const routes = /* @__PURE__ */ new Map([
|
|
875
909
|
[settings.callbackPath, { methods: ["GET"], run: callback }],
|
|
876
910
|
[settings.signedOutCallbackPath, { methods: ["GET"], run: signedOutCallback }],
|
|
@@ -879,7 +913,8 @@ function authioBff(options) {
|
|
|
879
913
|
[`${prefix}/user`, { methods: ["GET"], run: user }],
|
|
880
914
|
[`${prefix}/silent-login`, { methods: ["GET"], run: silentLogin }],
|
|
881
915
|
[`${prefix}/silent-login-callback`, { methods: ["GET"], run: silentLoginCallback }],
|
|
882
|
-
[`${prefix}/backchannel-logout`, { methods: ["POST"], run: backChannelLogout }]
|
|
916
|
+
[`${prefix}/backchannel-logout`, { methods: ["POST"], run: backChannelLogout }],
|
|
917
|
+
[`${prefix}/frontchannel-logout`, { methods: ["GET"], run: frontChannelLogout }]
|
|
883
918
|
]);
|
|
884
919
|
const middleware = (request, response, next) => {
|
|
885
920
|
const route = routes.get(request.path.replace(/(.)\/+$/, "$1"));
|