@o3co/create-auth-provider 0.16.0 → 0.17.0-rc.2
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.ja.md +66 -21
- package/README.md +76 -31
- package/dist/index.d.mts +33 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +118 -59
- package/dist/internal/template-filter.d.mts +7 -10
- package/dist/internal/template-filter.d.mts.map +1 -1
- package/dist/internal/template-filter.mjs +7 -10
- package/package.json +6 -4
- package/templates/standalone/.dockerignore +15 -2
- package/templates/standalone/.env.example +59 -29
- package/templates/standalone/Dockerfile +6 -2
- package/templates/standalone/README.ja.md +324 -169
- package/templates/standalone/README.md +618 -208
- package/templates/standalone/config/application.conf +211 -352
- package/templates/standalone/config/clients.yaml +2 -0
- package/templates/standalone/config/clients.yaml.example +4 -0
- package/templates/standalone/config/development.conf +13 -4
- package/templates/standalone/config/mailpit.conf +8 -0
- package/templates/standalone/config/production.conf +5 -6
- package/templates/standalone/config/reference.conf +418 -0
- package/templates/standalone/docker-compose.mailpit.yml +36 -0
- package/templates/standalone/docker-compose.production.yml +63 -32
- package/templates/standalone/docker-compose.yml +13 -3
- package/templates/standalone/gitignore +1 -0
- package/templates/standalone/package.json +9 -6
- package/templates/standalone/src/__tests__/adapters.test.mts +419 -0
- package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +87 -0
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +287 -198
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +71 -35
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +148 -94
- package/templates/standalone/src/__tests__/audit-sink.test.mts +84 -81
- package/templates/standalone/src/__tests__/config-notices.test.mts +154 -0
- package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
- package/templates/standalone/src/__tests__/core-federations.test.mts +249 -0
- package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +10 -15
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +970 -288
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +421 -86
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +87 -72
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +16 -8
- package/templates/standalone/src/__tests__/federation-types.test.mts +239 -0
- package/templates/standalone/src/__tests__/google-session.fixture.mts +97 -93
- package/templates/standalone/src/__tests__/host-routes.test.mts +4 -11
- package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +1 -1
- package/templates/standalone/src/__tests__/library-references.fixture.mts +188 -0
- package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +216 -0
- package/templates/standalone/src/__tests__/listen.test.mts +6 -13
- package/templates/standalone/src/__tests__/logger-projection.test.mts +1 -1
- package/templates/standalone/src/__tests__/mail-sender.test.mts +134 -0
- package/templates/standalone/src/__tests__/mailpit-compose.test.mts +326 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +4 -9
- package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +122 -0
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +825 -0
- package/templates/standalone/src/__tests__/own-modules.test.mts +1486 -0
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +10 -14
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +1 -5
- package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +116 -81
- package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
- package/templates/standalone/src/__tests__/replica-safety.test.mts +265 -146
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +4 -7
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +278 -103
- package/templates/standalone/src/__tests__/sections.test.mts +480 -0
- package/templates/standalone/src/__tests__/session-lifecycle.test.mts +215 -0
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +69 -61
- package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +130 -0
- package/templates/standalone/src/__tests__/session-requirements.test.mts +382 -0
- package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +270 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +163 -96
- package/templates/standalone/src/__tests__/smoke.test.mts +176 -258
- package/templates/standalone/src/__tests__/store-credential.test.mts +88 -52
- package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +131 -102
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +604 -0
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +382 -0
- package/templates/standalone/src/adapters.mts +162 -0
- package/templates/standalone/src/app.mts +65 -79
- package/templates/standalone/src/bootRefusal.mts +117 -0
- package/templates/standalone/src/buildModules.mts +220 -309
- package/templates/standalone/src/configPath.mts +449 -29
- package/templates/standalone/src/listen.mts +12 -18
- package/templates/standalone/src/logger.mts +31 -60
- package/templates/standalone/src/metrics.mts +24 -47
- package/templates/standalone/src/mfaSwitch.mts +285 -0
- package/templates/standalone/src/modules.mts +423 -629
- package/templates/standalone/src/rootRenames.mts +96 -0
- package/templates/standalone/src/routes.mts +8 -14
- package/templates/standalone/src/sections.mts +367 -0
- package/templates/standalone/src/shutdown.mts +45 -123
- package/templates/standalone/tests/index.test.js +6 -5
- package/templates/standalone/tests/package-lock.json +1615 -2223
- package/templates/standalone/tests/package.json +2 -2
- package/templates/standalone/tests/vitest.config.mjs +26 -0
- package/templates/standalone/vitest.config.mts +8 -7
- package/templates/standalone/vitest.supertest-loopback.mts +97 -69
- package/templates/versions.json +18 -15
- package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +0 -135
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +0 -199
- package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +0 -138
|
@@ -18,22 +18,16 @@
|
|
|
18
18
|
* `POST /session/logout` must invalidate the token the `session` grant minted
|
|
19
19
|
* from the same browser session.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* may not import each other. The standalone template is the composition root
|
|
26
|
-
* where both are mounted on one app, so it is where the contract between them
|
|
27
|
-
* is testable.
|
|
21
|
+
* Neither package can assert this alone: the logout endpoint is in
|
|
22
|
+
* `@o3co/auth-provider-session`, introspection and userinfo are in
|
|
23
|
+
* `@o3co/auth-provider-oauth`, and the two are siblings that may not import
|
|
24
|
+
* each other. This template is the composition root that mounts both.
|
|
28
25
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* access token minted from the session kept introspecting `active: true` and
|
|
35
|
-
* kept answering at `/userinfo` for its full lifetime after the user logged
|
|
36
|
-
* out. Only `/oauth/logout` with an `id_token_hint` was ever closed by #506.
|
|
26
|
+
* The `session` grant stamps `sid` on its access token, and `/oauth/introspect`
|
|
27
|
+
* and `/oauth/userinfo` both check that the `UserSession` it names is alive.
|
|
28
|
+
* In the BFF / `auth.proxy` injection topology, whose logout IS this endpoint,
|
|
29
|
+
* a logout that destroyed only the cookie would leave the token live at both
|
|
30
|
+
* for its full lifetime.
|
|
37
31
|
*/
|
|
38
32
|
|
|
39
33
|
import { generateKeyPairSync } from "node:crypto";
|
|
@@ -48,10 +42,13 @@ import {
|
|
|
48
42
|
memoryRefreshTokenFamilyStoreModule,
|
|
49
43
|
registerBuiltinKeyStores,
|
|
50
44
|
} from "@o3co/auth-provider-core";
|
|
45
|
+
import { coreConfigForTests } from "@o3co/auth-provider-core/testing";
|
|
51
46
|
import express from "express";
|
|
52
47
|
import request from "supertest";
|
|
53
48
|
import { afterEach, describe, expect, it } from "vitest";
|
|
54
49
|
import { buildModules } from "../buildModules.mjs";
|
|
50
|
+
import type { Switches } from "../configPath.mjs";
|
|
51
|
+
import { capturedRenames, inProcessAdapters } from "./library-references.fixture.mjs";
|
|
55
52
|
|
|
56
53
|
const keyPair = generateKeyPairSync("ed25519", {
|
|
57
54
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
@@ -64,36 +61,48 @@ const BASIC = `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("ba
|
|
|
64
61
|
const USERNAME = "alice";
|
|
65
62
|
const PASSWORD = "correct-horse-battery-staple";
|
|
66
63
|
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
/** The `http` module's section, as the hand-built configuration below carries it. */
|
|
65
|
+
const HTTP = {
|
|
66
|
+
port: 0,
|
|
67
|
+
trustProxy: false,
|
|
68
|
+
readinessTimeoutMs: 1000,
|
|
69
|
+
cors: { allowedOrigins: [] as string[] },
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The template modules' sections sit beside core's, so the configuration is wider than `AppConfig`. */
|
|
73
|
+
const config: AppConfig & Record<string, unknown> = {
|
|
74
|
+
// What a resolution under an environment that sets none captures of
|
|
75
|
+
// core's renamed variables.
|
|
76
|
+
...{
|
|
77
|
+
"renamed-variables": capturedRenames({}),
|
|
78
|
+
},
|
|
79
|
+
http: HTTP,
|
|
69
80
|
logging: { level: "silent" },
|
|
81
|
+
"key-store": {
|
|
82
|
+
provider: "local",
|
|
83
|
+
local: {
|
|
84
|
+
algorithm: "EdDSA",
|
|
85
|
+
kid: "v0",
|
|
86
|
+
privateKey: keyPair.privateKey,
|
|
87
|
+
publicKey: keyPair.publicKey,
|
|
88
|
+
previousKeys: [],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
// The session requirements this composition expects, as the shipped
|
|
92
|
+
// `application.conf` does (ADR 2026-09-28-session-admission): none.
|
|
93
|
+
...coreConfigForTests({ federations: { google: { enabled: false, type: "google" } } }),
|
|
70
94
|
oauth: {
|
|
71
95
|
jwt: {
|
|
72
96
|
issuer: "https://auth.test",
|
|
73
|
-
signingKey: {
|
|
74
|
-
provider: "local",
|
|
75
|
-
local: {
|
|
76
|
-
algorithm: "EdDSA",
|
|
77
|
-
kid: "v0",
|
|
78
|
-
privateKey: keyPair.privateKey,
|
|
79
|
-
publicKey: keyPair.publicKey,
|
|
80
|
-
previousKeys: [],
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
accessToken: { expiresIn: 3600 },
|
|
85
|
-
refreshToken: {
|
|
86
|
-
expiresIn: 86400,
|
|
87
|
-
unknownFamilyPolicy: "reject" as const,
|
|
88
|
-
legacyRtPolicy: "reject" as const,
|
|
89
97
|
},
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
grants: { session: { enabled: true } },
|
|
98
|
+
accessToken: { defaultExpiresIn: 3600 },
|
|
99
|
+
refreshToken: { expiresIn: 86400 },
|
|
93
100
|
oidcMode: "oidc-required",
|
|
94
|
-
code: { adapter: "memory" as const },
|
|
95
101
|
},
|
|
96
|
-
|
|
102
|
+
// The grant this whole test is about: it mints straight from an
|
|
103
|
+
// authenticated browser session, which is the BFF topology.
|
|
104
|
+
"oauth-session": { enabled: true },
|
|
105
|
+
"session-store": {
|
|
97
106
|
secret: "test-session-secret.at-least-32-bytes.ok",
|
|
98
107
|
name: "auth.sid",
|
|
99
108
|
maxAge: 3600000,
|
|
@@ -102,20 +111,21 @@ const config: AppConfig = {
|
|
|
102
111
|
domain: null,
|
|
103
112
|
storage: { type: "memory", redis: { url: "redis://localhost:6379" } },
|
|
104
113
|
},
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
federations: { google: { enabled: false } },
|
|
110
|
-
repositories: {
|
|
111
|
-
client: { type: "yaml", path: "./config/clients.yaml" },
|
|
112
|
-
user: { type: "yaml", path: "./config/users.yaml", timeout: 5000 },
|
|
113
|
-
code: { type: "memory", defaultExpiresIn: 600 },
|
|
114
|
+
session: {
|
|
115
|
+
loginPage: { url: "/login" },
|
|
116
|
+
rateLimit: { login: { windowMs: 60000, limit: 100 } },
|
|
114
117
|
},
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
rateLimit: { failMode: "open" },
|
|
119
|
+
"standalone-in-memory-code-repository": { defaultExpiresIn: 600 },
|
|
117
120
|
} as unknown as AppConfig;
|
|
118
121
|
|
|
122
|
+
/** What phase one hands `buildModules`: the configuration, and every store in process. */
|
|
123
|
+
const switches = {
|
|
124
|
+
...config,
|
|
125
|
+
adapters: inProcessAdapters(),
|
|
126
|
+
mfaMode: "off",
|
|
127
|
+
} as unknown as Switches;
|
|
128
|
+
|
|
119
129
|
const testRepositoriesModule = defineModule({
|
|
120
130
|
name: "test:repositories",
|
|
121
131
|
provides: {
|
|
@@ -154,7 +164,7 @@ const testKeyStoreModule = defineModule({
|
|
|
154
164
|
registerBuiltinKeyStores(factory);
|
|
155
165
|
return factory.create({
|
|
156
166
|
type: "local",
|
|
157
|
-
...((c as
|
|
167
|
+
...((c as { "key-store"?: { local?: object } })["key-store"]?.local ?? {}),
|
|
158
168
|
});
|
|
159
169
|
},
|
|
160
170
|
},
|
|
@@ -170,7 +180,7 @@ describe("POST /session/logout invalidates the session grant's access token", ()
|
|
|
170
180
|
|
|
171
181
|
async function buildApp() {
|
|
172
182
|
const handle = await createApp({
|
|
173
|
-
modules: buildModules(
|
|
183
|
+
modules: buildModules(switches, {
|
|
174
184
|
keyStoreModule: testKeyStoreModule,
|
|
175
185
|
repositoriesModule: testRepositoriesModule,
|
|
176
186
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
@@ -183,7 +193,7 @@ describe("POST /session/logout invalidates the session grant's access token", ()
|
|
|
183
193
|
app.use(
|
|
184
194
|
createReadinessRouter(express, {
|
|
185
195
|
probes: handle.readinessProbes,
|
|
186
|
-
timeoutMs:
|
|
196
|
+
timeoutMs: HTTP.readinessTimeoutMs,
|
|
187
197
|
}),
|
|
188
198
|
);
|
|
189
199
|
app.use(handle.router);
|
|
@@ -212,12 +222,10 @@ describe("POST /session/logout invalidates the session grant's access token", ()
|
|
|
212
222
|
expect(loginRes.status).toBe(200);
|
|
213
223
|
|
|
214
224
|
const loginCookies = loginRes.headers["set-cookie"] as unknown as string[];
|
|
215
|
-
// The
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
// token the header has to echo; the pre-login token no longer matches.
|
|
220
|
-
const csrfCookiePrefix = `${config.session.name}.csrf=`;
|
|
225
|
+
// The mechanism is a signed double-submit, so the reissued cookie's
|
|
226
|
+
// value IS the token the header has to echo; the pre-login token no
|
|
227
|
+
// longer matches.
|
|
228
|
+
const csrfCookiePrefix = `${(config["session-store"] as { name: string }).name}.csrf=`;
|
|
221
229
|
const reissued = loginCookies
|
|
222
230
|
.find((c) => c.startsWith(csrfCookiePrefix))
|
|
223
231
|
?.slice(csrfCookiePrefix.length)
|
|
@@ -268,9 +276,9 @@ describe("POST /session/logout invalidates the session grant's access token", ()
|
|
|
268
276
|
expect(logoutRes.status).toBe(200);
|
|
269
277
|
expect(logoutRes.body).toMatchObject({ message: "Logged out successfully" });
|
|
270
278
|
|
|
271
|
-
// After it, the same token no longer authorizes anything
|
|
272
|
-
//
|
|
273
|
-
//
|
|
279
|
+
// After it, the same token no longer authorizes anything: the
|
|
280
|
+
// `UserSession` record the liveness checks resolve does not outlive
|
|
281
|
+
// the logout.
|
|
274
282
|
const afterIntrospect = await request(app)
|
|
275
283
|
.post("/oauth/introspect")
|
|
276
284
|
.set("Authorization", BASIC)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 1o1 Co. Ltd.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The template with core's session lifecycle module: `POST /session/logout`
|
|
19
|
+
* closes the session through it, so the browser's own logout revokes the
|
|
20
|
+
* session's refresh-token families and tells its relying parties, as
|
|
21
|
+
* `/oauth/logout` does.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
type RefreshTokenFamilyRevocation,
|
|
26
|
+
readVersionedSessionLifecycle,
|
|
27
|
+
type SessionLifecycle,
|
|
28
|
+
type SessionLifecycleStore,
|
|
29
|
+
type UserSessionStore,
|
|
30
|
+
} from "@o3co/auth-provider-core";
|
|
31
|
+
import request from "supertest";
|
|
32
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
33
|
+
import { authorize, codeFrom, compose, login, redeem } from "./all-modules-composition.fixture.mjs";
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.restoreAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** A JWT's claims, unverified. */
|
|
40
|
+
const claimsOf = (token: string): Record<string, unknown> =>
|
|
41
|
+
JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
42
|
+
|
|
43
|
+
/** The CSRF token a login reissued, read off its double-submit cookie. */
|
|
44
|
+
const csrfTokenOf = (cookies: readonly string[]): string => {
|
|
45
|
+
const cookie = cookies.find((c) => /^[^=]*\.csrf=/.test(c));
|
|
46
|
+
if (cookie === undefined) throw new Error("the login reissued no CSRF cookie");
|
|
47
|
+
return decodeURIComponent(cookie.slice(cookie.indexOf("=") + 1).split(";")[0] ?? "");
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
describe("POST /session/logout with the session lifecycle module", () => {
|
|
51
|
+
it("revokes the session's families, tells its relying parties and closes its record", async () => {
|
|
52
|
+
const { app, handle } = await compose({
|
|
53
|
+
extraClients: {
|
|
54
|
+
"rp-bc": {
|
|
55
|
+
tokenEndpointAuthMethod: "client_secret_basic",
|
|
56
|
+
clientSecret: "rp-bc-secret-long-enough",
|
|
57
|
+
allowedRedirectUris: ["https://rp-bc.test/cb"],
|
|
58
|
+
allowedScopes: ["openid"],
|
|
59
|
+
allowedGrantTypes: ["authorization_code"],
|
|
60
|
+
backchannelLogoutUri: "https://rp-bc.test/logout",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
const components = handle.components as Record<string, unknown>;
|
|
66
|
+
const lifecycle = components.sessionLifecycle as SessionLifecycle;
|
|
67
|
+
const sessions = components.userSessionStore as UserSessionStore;
|
|
68
|
+
const families = components.refreshTokenFamilyRevocation as RefreshTokenFamilyRevocation;
|
|
69
|
+
const { cookies } = await login(app);
|
|
70
|
+
const exchanged = await redeem(app, codeFrom(await authorize(app, cookies)));
|
|
71
|
+
expect(exchanged.status).toBe(200);
|
|
72
|
+
const claims = claimsOf(exchanged.body.refresh_token as string);
|
|
73
|
+
const sid = String(claims.sid);
|
|
74
|
+
const familyId = String(claims.family_id);
|
|
75
|
+
expect(
|
|
76
|
+
await lifecycle.join(sid, {
|
|
77
|
+
rp: {
|
|
78
|
+
clientId: "rp-bc",
|
|
79
|
+
backchannelLogoutUri: "https://rp-bc.test/logout",
|
|
80
|
+
backchannelLogoutSessionRequired: true,
|
|
81
|
+
frontchannelLogoutUri: undefined,
|
|
82
|
+
frontchannelLogoutSessionRequired: undefined,
|
|
83
|
+
registeredAt: new Date(),
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
).toEqual({ outcome: "joined" });
|
|
87
|
+
const posted = vi
|
|
88
|
+
.spyOn(globalThis, "fetch")
|
|
89
|
+
.mockImplementation(async () => new Response(null, { status: 200 }));
|
|
90
|
+
|
|
91
|
+
const csrfToken = csrfTokenOf(cookies);
|
|
92
|
+
const res = await request(app)
|
|
93
|
+
.post("/session/logout")
|
|
94
|
+
.set("Cookie", cookies)
|
|
95
|
+
.set("x-csrf-token", csrfToken);
|
|
96
|
+
|
|
97
|
+
expect(res.status).toBe(200);
|
|
98
|
+
expect(await families.isFamilyRevoked(familyId)).toBe(true);
|
|
99
|
+
expect(posted.mock.calls.map(([url]) => String(url))).toEqual(["https://rp-bc.test/logout"]);
|
|
100
|
+
expect(await sessions.get(sid)).toBeNull();
|
|
101
|
+
const store = components.sessionLifecycleStore as SessionLifecycleStore;
|
|
102
|
+
expect(readVersionedSessionLifecycle(await store.read(sid))?.value.state).toBe("closed");
|
|
103
|
+
} finally {
|
|
104
|
+
await handle.dispose();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("answers 503 when the close cannot commit, and the cookie still admits the session for a retry", async () => {
|
|
109
|
+
const { app, handle } = await compose();
|
|
110
|
+
try {
|
|
111
|
+
const { cookies } = await login(app);
|
|
112
|
+
const components = handle.components as Record<string, unknown>;
|
|
113
|
+
const store = components.sessionLifecycleStore as SessionLifecycleStore;
|
|
114
|
+
vi.spyOn(store, "beginClose").mockRejectedValue(new Error("lifecycle store down"));
|
|
115
|
+
|
|
116
|
+
const res = await request(app)
|
|
117
|
+
.post("/session/logout")
|
|
118
|
+
.set("Cookie", cookies)
|
|
119
|
+
.set("x-csrf-token", csrfTokenOf(cookies));
|
|
120
|
+
|
|
121
|
+
expect(res.status).toBe(503);
|
|
122
|
+
expect(res.body.error).toBe("temporarily_unavailable");
|
|
123
|
+
// The session is still live and its cookie still admits it.
|
|
124
|
+
const authorized = await authorize(app, cookies);
|
|
125
|
+
expect(codeFrom(authorized)).toEqual(expect.any(String));
|
|
126
|
+
} finally {
|
|
127
|
+
await handle.dispose();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|