@o3co/create-auth-provider 0.17.0-rc.2 → 0.17.0-rc.4
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 +3 -1
- package/README.md +5 -2
- package/package.json +3 -3
- package/templates/standalone/.env.example +7 -2
- package/templates/standalone/README.ja.md +17 -11
- package/templates/standalone/README.md +47 -21
- package/templates/standalone/config/application.conf +1 -0
- package/templates/standalone/config/clients.yaml.example +8 -0
- package/templates/standalone/config/development.conf +2 -2
- package/templates/standalone/config/mailpit.conf +10 -5
- package/templates/standalone/config/reference.conf +9 -3
- package/templates/standalone/docker-compose.mailpit.yml +3 -0
- package/templates/standalone/package.json +6 -6
- package/templates/standalone/src/__tests__/adapters.test.mts +12 -6
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +13 -5
- package/templates/standalone/src/__tests__/config-notices.test.mts +4 -1
- package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +48 -0
- package/templates/standalone/src/__tests__/core-federations.test.mts +9 -6
- package/templates/standalone/src/__tests__/core-sections.test.mts +7 -5
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +91 -87
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +4 -0
- package/templates/standalone/src/__tests__/logout-session-cookie.test.mts +113 -0
- package/templates/standalone/src/__tests__/mailpit-compose.test.mts +81 -14
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +8 -4
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +79 -21
- package/templates/standalone/src/__tests__/own-modules.test.mts +38 -12
- package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +214 -0
- package/templates/standalone/src/__tests__/replica-safety.test.mts +4 -0
- package/templates/standalone/src/__tests__/repositories-logger.test.mts +119 -0
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +10 -3
- package/templates/standalone/src/__tests__/sections.test.mts +24 -0
- package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +4 -0
- package/templates/standalone/src/__tests__/startup-log-lines.test.mts +148 -0
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +8 -4
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +2 -1
- package/templates/standalone/src/adapters.mts +2 -2
- package/templates/standalone/src/mfaSwitch.mts +6 -7
- package/templates/standalone/src/modules.mts +21 -9
- package/templates/standalone/src/rootRenames.mts +12 -13
- package/templates/standalone/src/sections.mts +12 -2
- package/templates/standalone/tests/package-lock.json +95 -95
- package/templates/standalone/tests/package.json +1 -1
- package/templates/versions.json +18 -18
|
@@ -78,6 +78,10 @@ vi.mock("ioredis", () => {
|
|
|
78
78
|
quit: async () => "OK",
|
|
79
79
|
disconnect: () => undefined,
|
|
80
80
|
ping: async () => "PONG",
|
|
81
|
+
// The Redis stores' eviction gate reads the policy from INFO memory: a
|
|
82
|
+
// default server's.
|
|
83
|
+
info: async (section: string) =>
|
|
84
|
+
section === "memory" ? "# Memory\r\nmaxmemory_policy:noeviction\r\n" : null,
|
|
81
85
|
};
|
|
82
86
|
const makeMockRedis = (): object =>
|
|
83
87
|
new Proxy(
|
|
@@ -182,12 +186,16 @@ describe('every module on, every shared store on Redis, core.deployment.mode = "
|
|
|
182
186
|
expect(current.handle.components.deploymentMode).toBe("multi");
|
|
183
187
|
});
|
|
184
188
|
|
|
185
|
-
it("
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
it("refuses DEPLOYMENT_MODE=multi beside CORE_DEPLOYMENT_MODE=multi: the old name must be unset", async () => {
|
|
190
|
+
await expect(
|
|
191
|
+
compose({
|
|
192
|
+
...MULTI,
|
|
193
|
+
env: { ...MULTI_ENV, DEPLOYMENT_MODE: "multi", CORE_DEPLOYMENT_MODE: "multi" },
|
|
194
|
+
}),
|
|
195
|
+
).rejects.toMatchObject({
|
|
196
|
+
reason: "environment-variable-renamed",
|
|
197
|
+
details: { renamed: [{ from: "DEPLOYMENT_MODE", state: "different" }] },
|
|
189
198
|
});
|
|
190
|
-
expect(current.handle.components.deploymentMode).toBe("multi");
|
|
191
199
|
});
|
|
192
200
|
|
|
193
201
|
/**
|
|
@@ -149,6 +149,9 @@ describe("configDefaultsFor", () => {
|
|
|
149
149
|
|
|
150
150
|
it("takes nothing from the process's environment", () => {
|
|
151
151
|
vi.stubEnv("REDIS_CLIENTS_URL", "redis://operator.test:6379");
|
|
152
|
-
expect(configDefaultsFor(modules)[NOT_LOADED]).toEqual({
|
|
152
|
+
expect(configDefaultsFor(modules)[NOT_LOADED]).toEqual({
|
|
153
|
+
url: "redis://localhost:6379",
|
|
154
|
+
assumeNoEviction: false,
|
|
155
|
+
});
|
|
153
156
|
});
|
|
154
157
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
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 MFA switch, `mfaMode`, as this project's own configuration files write
|
|
19
|
+
* it with `MFA_MODE` unset: `{environment}.conf` over `application.conf` over
|
|
20
|
+
* the template's `config/reference.conf`. The template ships it `required`;
|
|
21
|
+
* a project `create-auth-provider --no-mfa` scaffolds writes it `off` at the
|
|
22
|
+
* end of its `application.conf`. A suite whose subject depends on the switch
|
|
23
|
+
* asserts what MFA on (`required`, as the template ships it) or off (as
|
|
24
|
+
* `--no-mfa` writes it) implies, so the suite that ships with a scaffold holds
|
|
25
|
+
* in either project. A project whose files write another mode, such as
|
|
26
|
+
* `optional`, is not what these suites claim to cover.
|
|
27
|
+
*
|
|
28
|
+
* Read with the HOCON parser alone, not through phase one (`readSwitches`),
|
|
29
|
+
* so that a suite about phase one can state what phase one should answer.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
import { type Config, empty, parseFile } from "@o3co/ts.hocon";
|
|
35
|
+
import { type MfaSwitch, mfaSwitchSchema } from "#/sections.mjs";
|
|
36
|
+
|
|
37
|
+
const configDir = fileURLToPath(new URL("../../config", import.meta.url));
|
|
38
|
+
|
|
39
|
+
/** `mfaMode` as the files selected by `environment` write it, with no variable set. */
|
|
40
|
+
export function configuredMfaMode(environment: string): MfaSwitch {
|
|
41
|
+
const resolved = [`${environment}.conf`, "application.conf", "reference.conf"]
|
|
42
|
+
.reduce<Config>(
|
|
43
|
+
(layered, file) => layered.withFallback(parseFile(join(configDir, file), { env: {} })),
|
|
44
|
+
empty(),
|
|
45
|
+
)
|
|
46
|
+
.toObject() as Record<string, unknown>;
|
|
47
|
+
return mfaSwitchSchema.parse(resolved.mfaMode);
|
|
48
|
+
}
|
|
@@ -22,9 +22,8 @@
|
|
|
22
22
|
* bundles are listed whatever the map says (`federation-types.test.mts`
|
|
23
23
|
* boots them). A variable the
|
|
24
24
|
* template bound before, `FEDERATIONS_<NAME>_<KEY>`, set alone or beside its
|
|
25
|
-
* new name at
|
|
26
|
-
*
|
|
27
|
-
* are accepted. The map written at the top level refuses boot, naming its
|
|
25
|
+
* new name, at any value, is refused before any module is chosen, naming the
|
|
26
|
+
* new variable and path and never a value. The map written at the top level refuses boot, naming its
|
|
28
27
|
* paths under `core.federations`.
|
|
29
28
|
*/
|
|
30
29
|
|
|
@@ -206,9 +205,13 @@ describe("a variable the template bound before, FEDERATIONS_<NAME>_<KEY>", () =>
|
|
|
206
205
|
);
|
|
207
206
|
|
|
208
207
|
it.each(RENAMED)(
|
|
209
|
-
"core.federations.%s.%s: %s beside its new name at the same value is
|
|
210
|
-
(
|
|
211
|
-
|
|
208
|
+
"core.federations.%s.%s: %s beside its new name at the same value is refused all the same",
|
|
209
|
+
(_name, _key, old, variable, value) => {
|
|
210
|
+
const err = refusal({ [old]: value, [variable]: value });
|
|
211
|
+
expect(err).toMatchObject({
|
|
212
|
+
reason: "environment-variable-renamed",
|
|
213
|
+
details: { renamed: [{ from: old, state: "different" }] },
|
|
214
|
+
});
|
|
212
215
|
},
|
|
213
216
|
);
|
|
214
217
|
});
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* environment, phase one's switches, then the layers over every loaded
|
|
21
21
|
* package's `reference.conf` handed to boot. A path they moved from, written
|
|
22
22
|
* in the operator's own layer, refuses boot naming the new one;
|
|
23
|
-
* `DEPLOYMENT_MODE`, renamed `CORE_DEPLOYMENT_MODE`, refuses boot
|
|
24
|
-
*
|
|
23
|
+
* `DEPLOYMENT_MODE`, renamed `CORE_DEPLOYMENT_MODE`, refuses boot while it is
|
|
24
|
+
* set, whatever the new name holds.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { BootError } from "@o3co/auth-provider-core";
|
|
@@ -176,10 +176,12 @@ describe("DEPLOYMENT_MODE, renamed CORE_DEPLOYMENT_MODE, through the template's
|
|
|
176
176
|
expect(err.message).not.toContain("new-mode-c81a");
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
it("set beside the new name at the same value:
|
|
180
|
-
const
|
|
179
|
+
it("set beside the new name at the same value: refused all the same", async () => {
|
|
180
|
+
const err = await refused({ DEPLOYMENT_MODE: "single", CORE_DEPLOYMENT_MODE: "single" });
|
|
181
181
|
|
|
182
|
-
expect(
|
|
182
|
+
expect(err.details).toMatchObject({
|
|
183
|
+
renamed: [{ from: "DEPLOYMENT_MODE", to: "CORE_DEPLOYMENT_MODE", state: "different" }],
|
|
184
|
+
});
|
|
183
185
|
});
|
|
184
186
|
|
|
185
187
|
it("unset, with the new name set: boots with its mode", async () => {
|
|
@@ -44,7 +44,12 @@ import {
|
|
|
44
44
|
resolveLayers,
|
|
45
45
|
type Switches,
|
|
46
46
|
} from "../configPath.mjs";
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
httpModule,
|
|
49
|
+
keyStoreModule,
|
|
50
|
+
standaloneRedisClientsModule,
|
|
51
|
+
templateReference,
|
|
52
|
+
} from "../modules.mjs";
|
|
48
53
|
|
|
49
54
|
/**
|
|
50
55
|
* Boots the shipped config with EVERY documented override supplied the way an
|
|
@@ -134,7 +139,6 @@ const DOCUMENTED_ENV: Readonly<Record<string, string>> = {
|
|
|
134
139
|
OAUTH_SESSION_ENABLED: "false",
|
|
135
140
|
OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED: "true",
|
|
136
141
|
OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED: "true",
|
|
137
|
-
OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY: "reject",
|
|
138
142
|
OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED: "true",
|
|
139
143
|
OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED: "false",
|
|
140
144
|
|
|
@@ -195,6 +199,7 @@ const DOCUMENTED_ENV: Readonly<Record<string, string>> = {
|
|
|
195
199
|
REDIS_REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT: "3",
|
|
196
200
|
REDIS_CLIENTS_URL: "redis://redis:6379",
|
|
197
201
|
REDIS_CLIENTS_PASSWORD: "rt-family-password",
|
|
202
|
+
REDIS_CLIENTS_ASSUME_NO_EVICTION: "true",
|
|
198
203
|
// The federation token store's Redis branch.
|
|
199
204
|
REDIS_FEDERATION_TOKEN_STORE_KEY_PREFIX: "ft:",
|
|
200
205
|
REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_MODE: "required",
|
|
@@ -311,139 +316,141 @@ const DELIBERATELY_UNSET: Readonly<Record<string, string>> = {
|
|
|
311
316
|
OAUTH_AUTHORIZE_ALLOW_UNMARKED_CLIENTS:
|
|
312
317
|
"the variable of a removed key, and only captured — any value fails boot",
|
|
313
318
|
OAUTH_ACCESS_TOKEN_EXPIRES_IN:
|
|
314
|
-
"renamed OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN, and only captured — set
|
|
319
|
+
"renamed OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
315
320
|
DEPLOYMENT_MODE:
|
|
316
|
-
"renamed CORE_DEPLOYMENT_MODE, and only captured — set
|
|
321
|
+
"renamed CORE_DEPLOYMENT_MODE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
317
322
|
MEMORY_RATE_LIMITER_MAX_BUCKETS:
|
|
318
|
-
"renamed CORE_RATE_LIMITER_MEMORY_MAX_BUCKETS, and only captured — set
|
|
323
|
+
"renamed CORE_RATE_LIMITER_MEMORY_MAX_BUCKETS, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
319
324
|
RATE_LIMIT_FAIL_MODE:
|
|
320
|
-
"renamed REDIS_RATE_LIMITER_FAIL_MODE, and only captured — set
|
|
325
|
+
"renamed REDIS_RATE_LIMITER_FAIL_MODE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
321
326
|
REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX:
|
|
322
|
-
"renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX, and only captured — set
|
|
327
|
+
"renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
323
328
|
REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT:
|
|
324
|
-
"renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT, and only captured — set
|
|
329
|
+
"renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
325
330
|
FEDERATION_GRANTS_ENCRYPTION_MODE:
|
|
326
|
-
"renamed REDIS_FEDERATION_GRANT_STORE_ENCRYPTION_MODE, and only captured — set
|
|
331
|
+
"renamed REDIS_FEDERATION_GRANT_STORE_ENCRYPTION_MODE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
327
332
|
OAUTH_TOKEN_BINDING_DISPATCH_POLICY:
|
|
328
|
-
"renamed CORE_TOKEN_BINDING_DISPATCH_POLICY, and only captured — set
|
|
333
|
+
"renamed CORE_TOKEN_BINDING_DISPATCH_POLICY, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
329
334
|
OAUTH_TOKEN_BINDING_BIND_CONFIDENTIAL_CLIENT_REFRESH_TOKENS:
|
|
330
|
-
"renamed CORE_TOKEN_BINDING_BIND_CONFIDENTIAL_CLIENT_REFRESH_TOKENS, and only captured — set
|
|
335
|
+
"renamed CORE_TOKEN_BINDING_BIND_CONFIDENTIAL_CLIENT_REFRESH_TOKENS, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
331
336
|
OAUTH_GRANTS_SESSION_ENABLED:
|
|
332
|
-
"renamed OAUTH_SESSION_ENABLED, and only captured — set
|
|
337
|
+
"renamed OAUTH_SESSION_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
333
338
|
OAUTH_GRANTS_AUTHORIZATION_CODE_ENABLED:
|
|
334
|
-
"renamed OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED, and only captured — set
|
|
339
|
+
"renamed OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
335
340
|
OAUTH_GRANTS_REFRESH_TOKEN_ENABLED:
|
|
336
|
-
"renamed OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED, and only captured — set
|
|
341
|
+
"renamed OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
337
342
|
OAUTH_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY:
|
|
338
|
-
"
|
|
343
|
+
"the variable of a removed key, and only captured — any value fails boot",
|
|
344
|
+
OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY:
|
|
345
|
+
"the variable of a removed key, and only captured — any value fails boot",
|
|
339
346
|
OAUTH_GRANTS_CLIENT_CREDENTIALS_ENABLED:
|
|
340
|
-
"renamed OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED, and only captured — set
|
|
347
|
+
"renamed OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
341
348
|
OAUTH_GRANTS_JWT_BEARER_ENABLED:
|
|
342
|
-
"renamed OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED, and only captured — set
|
|
349
|
+
"renamed OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
343
350
|
ENDPOINTS_CONSENT_URL:
|
|
344
|
-
"renamed OAUTH_CONSENT_PAGE_URL, and only captured — set
|
|
351
|
+
"renamed OAUTH_CONSENT_PAGE_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
345
352
|
OAUTH_CIMD_ENABLED:
|
|
346
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ENABLED, and only captured — set
|
|
353
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
347
354
|
OAUTH_CIMD_ALLOWED_SCOPES:
|
|
348
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_SCOPES, and only captured — set
|
|
355
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_SCOPES, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
349
356
|
OAUTH_CIMD_ALLOWED_AUDIENCES:
|
|
350
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_AUDIENCES, and only captured — set
|
|
357
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_AUDIENCES, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
351
358
|
OAUTH_CIMD_ALLOWED_HOSTS:
|
|
352
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_HOSTS, and only captured — set
|
|
359
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_HOSTS, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
353
360
|
OAUTH_CIMD_DENIED_HOSTS:
|
|
354
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_DENIED_HOSTS, and only captured — set
|
|
361
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_DENIED_HOSTS, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
355
362
|
OAUTH_CIMD_MAX_BYTES:
|
|
356
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_BYTES, and only captured — set
|
|
363
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_BYTES, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
357
364
|
OAUTH_CIMD_TIMEOUT_MS:
|
|
358
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_TIMEOUT_MS, and only captured — set
|
|
365
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_TIMEOUT_MS, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
359
366
|
OAUTH_CIMD_CACHE_MAX_AGE_MS:
|
|
360
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_CACHE_MAX_AGE_MS, and only captured — set
|
|
367
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_CACHE_MAX_AGE_MS, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
361
368
|
OAUTH_CIMD_MAX_CACHE_ENTRIES:
|
|
362
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CACHE_ENTRIES, and only captured — set
|
|
369
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CACHE_ENTRIES, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
363
370
|
OAUTH_CIMD_STALE_IF_ERROR_MS:
|
|
364
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_STALE_IF_ERROR_MS, and only captured — set
|
|
371
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_STALE_IF_ERROR_MS, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
365
372
|
OAUTH_CIMD_NEGATIVE_CACHE_MS:
|
|
366
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_NEGATIVE_CACHE_MS, and only captured — set
|
|
373
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_NEGATIVE_CACHE_MS, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
367
374
|
OAUTH_CIMD_MAX_CONCURRENT_FETCHES:
|
|
368
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CONCURRENT_FETCHES, and only captured — set
|
|
375
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CONCURRENT_FETCHES, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
369
376
|
OAUTH_GRANTS_AUTHORIZATION_CODE_PKCE_REQUIRE_S256:
|
|
370
377
|
"the authorization-code grant's pkce block was removed, and this is only captured — set at all, it fails boot",
|
|
371
378
|
ENDPOINTS_LOGIN_URL:
|
|
372
|
-
"renamed SESSION_LOGIN_PAGE_URL, and only captured — set
|
|
379
|
+
"renamed SESSION_LOGIN_PAGE_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
373
380
|
SESSION_SECRET:
|
|
374
|
-
"renamed SESSION_STORE_SECRET, and only captured — set
|
|
381
|
+
"renamed SESSION_STORE_SECRET, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
375
382
|
SESSION_NAME:
|
|
376
|
-
"renamed SESSION_STORE_NAME, and only captured — set
|
|
383
|
+
"renamed SESSION_STORE_NAME, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
377
384
|
SESSION_MAX_AGE:
|
|
378
|
-
"renamed SESSION_STORE_MAX_AGE, and only captured — set
|
|
385
|
+
"renamed SESSION_STORE_MAX_AGE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
379
386
|
SESSION_SECURE:
|
|
380
|
-
"renamed SESSION_STORE_SECURE, and only captured — set
|
|
387
|
+
"renamed SESSION_STORE_SECURE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
381
388
|
SESSION_SAME_SITE:
|
|
382
|
-
"renamed SESSION_STORE_SAME_SITE, and only captured — set
|
|
389
|
+
"renamed SESSION_STORE_SAME_SITE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
383
390
|
SESSION_DOMAIN:
|
|
384
|
-
"renamed SESSION_STORE_DOMAIN, and only captured — set
|
|
391
|
+
"renamed SESSION_STORE_DOMAIN, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
385
392
|
SESSION_STORAGE_TYPE:
|
|
386
|
-
"renamed SESSION_STORE_STORAGE_TYPE, and only captured — set
|
|
393
|
+
"renamed SESSION_STORE_STORAGE_TYPE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
387
394
|
SESSION_STORAGE_REDIS_URL:
|
|
388
|
-
"renamed SESSION_STORE_STORAGE_REDIS_URL, and only captured — set
|
|
395
|
+
"renamed SESSION_STORE_STORAGE_REDIS_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
389
396
|
SESSION_STORAGE_REDIS_PASSWORD:
|
|
390
|
-
"renamed SESSION_STORE_STORAGE_REDIS_PASSWORD, and only captured — set
|
|
397
|
+
"renamed SESSION_STORE_STORAGE_REDIS_PASSWORD, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
391
398
|
LOG_LEVEL:
|
|
392
|
-
"renamed LOGGING_LEVEL, and only captured — set
|
|
399
|
+
"renamed LOGGING_LEVEL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
393
400
|
CORS_ALLOWED_ORIGINS:
|
|
394
|
-
"renamed HTTP_CORS_ALLOWED_ORIGINS, and only captured — set
|
|
401
|
+
"renamed HTTP_CORS_ALLOWED_ORIGINS, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
395
402
|
OAUTH_JWT_SIGNING_KEY_PROVIDER:
|
|
396
|
-
"renamed KEY_STORE_PROVIDER, and only captured — set
|
|
403
|
+
"renamed KEY_STORE_PROVIDER, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
397
404
|
OAUTH_JWT_ALGORITHM:
|
|
398
|
-
"renamed KEY_STORE_LOCAL_ALGORITHM, and only captured — set
|
|
405
|
+
"renamed KEY_STORE_LOCAL_ALGORITHM, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
399
406
|
OAUTH_JWT_KID:
|
|
400
|
-
"renamed KEY_STORE_LOCAL_KID, and only captured — set
|
|
407
|
+
"renamed KEY_STORE_LOCAL_KID, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
401
408
|
OAUTH_JWT_SECRET:
|
|
402
|
-
"renamed KEY_STORE_LOCAL_SECRET, and only captured — set
|
|
409
|
+
"renamed KEY_STORE_LOCAL_SECRET, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
403
410
|
OAUTH_JWT_PRIVATE_KEY_PATH:
|
|
404
|
-
"renamed KEY_STORE_LOCAL_PRIVATE_KEY_PATH, and only captured — set
|
|
411
|
+
"renamed KEY_STORE_LOCAL_PRIVATE_KEY_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
405
412
|
OAUTH_JWT_PUBLIC_KEY_PATH:
|
|
406
|
-
"renamed KEY_STORE_LOCAL_PUBLIC_KEY_PATH, and only captured — set
|
|
413
|
+
"renamed KEY_STORE_LOCAL_PUBLIC_KEY_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
407
414
|
OAUTH_JWT_PRIVATE_KEY:
|
|
408
|
-
"renamed KEY_STORE_LOCAL_PRIVATE_KEY, and only captured — set
|
|
415
|
+
"renamed KEY_STORE_LOCAL_PRIVATE_KEY, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
409
416
|
OAUTH_JWT_PUBLIC_KEY:
|
|
410
|
-
"renamed KEY_STORE_LOCAL_PUBLIC_KEY, and only captured — set
|
|
417
|
+
"renamed KEY_STORE_LOCAL_PUBLIC_KEY, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
411
418
|
REFRESH_TOKEN_FAMILY_STORE_REDIS_URL:
|
|
412
|
-
"renamed REDIS_CLIENTS_URL, and only captured — set
|
|
419
|
+
"renamed REDIS_CLIENTS_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
413
420
|
REFRESH_TOKEN_FAMILY_STORE_REDIS_PASSWORD:
|
|
414
|
-
"renamed REDIS_CLIENTS_PASSWORD, and only captured — set
|
|
421
|
+
"renamed REDIS_CLIENTS_PASSWORD, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
415
422
|
CLIENT_PATH:
|
|
416
|
-
"renamed REPOSITORIES_CLIENT_YAML_PATH, and only captured — set
|
|
423
|
+
"renamed REPOSITORIES_CLIENT_YAML_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
417
424
|
CLIENT_USER_PATH:
|
|
418
|
-
"renamed REPOSITORIES_USER_YAML_PATH, and only captured — set
|
|
425
|
+
"renamed REPOSITORIES_USER_YAML_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
419
426
|
CLIENT_USER_AUTHENTICATE_URL:
|
|
420
|
-
"renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_URL, and only captured — set
|
|
427
|
+
"renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
421
428
|
CLIENT_USER_AUTHENTICATE_BY_TOKEN_URL:
|
|
422
|
-
"renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_BY_TOKEN_URL, and only captured — set
|
|
429
|
+
"renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_BY_TOKEN_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
423
430
|
CLIENT_USER_LINK_FEDERATED_IDENTITY_URL:
|
|
424
|
-
"renamed REPOSITORIES_USER_HTTP_LINK_FEDERATED_IDENTITY_URL, and only captured — set
|
|
431
|
+
"renamed REPOSITORIES_USER_HTTP_LINK_FEDERATED_IDENTITY_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
425
432
|
CLIENT_USER_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL:
|
|
426
|
-
"renamed REPOSITORIES_USER_HTTP_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL, and only captured — set
|
|
433
|
+
"renamed REPOSITORIES_USER_HTTP_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
427
434
|
CLIENT_USER_BEARER_TOKEN:
|
|
428
|
-
"renamed REPOSITORIES_USER_HTTP_BEARER_TOKEN, and only captured — set
|
|
435
|
+
"renamed REPOSITORIES_USER_HTTP_BEARER_TOKEN, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
429
436
|
CLIENT_USER_TIMEOUT:
|
|
430
|
-
"renamed REPOSITORIES_USER_HTTP_TIMEOUT, and only captured — set
|
|
437
|
+
"renamed REPOSITORIES_USER_HTTP_TIMEOUT, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
431
438
|
CLIENT_USER_MAX_RESPONSE_BYTES:
|
|
432
|
-
"renamed REPOSITORIES_USER_HTTP_MAX_RESPONSE_BYTES, and only captured — set
|
|
439
|
+
"renamed REPOSITORIES_USER_HTTP_MAX_RESPONSE_BYTES, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
433
440
|
CLIENT_CODE_DEFAULT_EXPIRES_IN:
|
|
434
|
-
"renamed REDIS_CODE_REPOSITORY_DEFAULT_EXPIRES_IN (the Redis code repository) and STANDALONE_IN_MEMORY_CODE_REPOSITORY_DEFAULT_EXPIRES_IN (the in-process one), and only captured — set
|
|
441
|
+
"renamed REDIS_CODE_REPOSITORY_DEFAULT_EXPIRES_IN (the Redis code repository) and STANDALONE_IN_MEMORY_CODE_REPOSITORY_DEFAULT_EXPIRES_IN (the in-process one), and only captured — set at any value, beside its new name or not, it fails boot",
|
|
435
442
|
CLIENT_CODE_KEY_PREFIX:
|
|
436
|
-
"renamed REDIS_CODE_REPOSITORY_KEY_PREFIX, and only captured — set
|
|
443
|
+
"renamed REDIS_CODE_REPOSITORY_KEY_PREFIX, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
437
444
|
CLIENT_CODE_ENDPOINT_URI:
|
|
438
445
|
"removed: the Redis code repository uses the shared redis-clients connection; only captured — set at all, it fails boot",
|
|
439
446
|
CLIENT_CODE_PASSWORD:
|
|
440
447
|
"removed: the Redis code repository uses the shared redis-clients connection; only captured — set at all, it fails boot",
|
|
441
448
|
ENDPOINTS_MFA_URL:
|
|
442
|
-
"renamed MFA_PAGE_URL, and only captured — set
|
|
449
|
+
"renamed MFA_PAGE_URL, and only captured — set at any value, beside its new name or not, it fails boot where MFA is installed",
|
|
443
450
|
MFA_TOTP_ENABLED:
|
|
444
|
-
"renamed MFA_TOTP_FACTOR_ENABLED, and only captured — set
|
|
451
|
+
"renamed MFA_TOTP_FACTOR_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot where MFA is installed",
|
|
445
452
|
MFA_TOTP_ISSUER:
|
|
446
|
-
"renamed MFA_TOTP_FACTOR_ISSUER, and only captured — set
|
|
453
|
+
"renamed MFA_TOTP_FACTOR_ISSUER, and only captured — set at any value, beside its new name or not, it fails boot where MFA is installed",
|
|
447
454
|
};
|
|
448
455
|
|
|
449
456
|
/**
|
|
@@ -480,11 +487,11 @@ function commentedFederationBindings(): string {
|
|
|
480
487
|
}
|
|
481
488
|
|
|
482
489
|
/**
|
|
483
|
-
* The provider environment `o3co/auth`'s `tests/docker-compose.yml`
|
|
484
|
-
* transcribed: each renamed variable under its
|
|
485
|
-
*
|
|
486
|
-
* so a parse failure here is a red umbrella
|
|
487
|
-
* first. `SESSION_STORE_SECURE=false` is the one it cannot run without: the
|
|
490
|
+
* The provider environment `o3co/auth`'s `tests/docker-compose.yml` is to
|
|
491
|
+
* set, transcribed: each renamed variable under its new name alone, since an
|
|
492
|
+
* old name set refuses boot beside its new one too. The umbrella E2E boots
|
|
493
|
+
* the shipped template with this, so a parse failure here is a red umbrella
|
|
494
|
+
* build that this repository can see first. `SESSION_STORE_SECURE=false` is the one it cannot run without: the
|
|
488
495
|
* suite speaks plain HTTP.
|
|
489
496
|
*
|
|
490
497
|
* The two `FEDERATION_TOKEN_STORE` lines are required: the federation token
|
|
@@ -493,37 +500,23 @@ function commentedFederationBindings(): string {
|
|
|
493
500
|
* Redis store is selected with its encryption key.
|
|
494
501
|
*/
|
|
495
502
|
const UMBRELLA_E2E_ENV: Readonly<Record<string, string>> = {
|
|
496
|
-
OAUTH_JWT_ALGORITHM: "HS256",
|
|
497
503
|
KEY_STORE_LOCAL_ALGORITHM: "HS256",
|
|
498
|
-
OAUTH_JWT_SECRET: "e2e-shared-hs256-secret.at-least-32-bytes.ok",
|
|
499
504
|
KEY_STORE_LOCAL_SECRET: "e2e-shared-hs256-secret.at-least-32-bytes.ok",
|
|
500
505
|
OAUTH_JWT_ISSUER: "https://auth.e2e.test",
|
|
501
|
-
SESSION_SECRET: "lO0QH09fuKSGuViZ9myJbH3jsgai99A2GpC3RYRuy6Y=",
|
|
502
506
|
SESSION_STORE_SECRET: "lO0QH09fuKSGuViZ9myJbH3jsgai99A2GpC3RYRuy6Y=",
|
|
503
|
-
SESSION_SECURE: "false",
|
|
504
507
|
SESSION_STORE_SECURE: "false",
|
|
505
|
-
SESSION_NAME: "auth.session",
|
|
506
508
|
SESSION_STORE_NAME: "auth.session",
|
|
507
|
-
DEPLOYMENT_MODE: "multi",
|
|
508
509
|
CORE_DEPLOYMENT_MODE: "multi",
|
|
509
|
-
REFRESH_TOKEN_FAMILY_STORE_REDIS_URL: "redis://redis:6379",
|
|
510
510
|
REDIS_CLIENTS_URL: "redis://redis:6379",
|
|
511
|
-
SESSION_STORAGE_REDIS_URL: "redis://redis:6379",
|
|
512
511
|
SESSION_STORE_STORAGE_REDIS_URL: "redis://redis:6379",
|
|
513
|
-
USER_SESSION_STORES_ADAPTER: "redis",
|
|
514
512
|
ADAPTERS_USER_SESSION_STORES: "redis",
|
|
515
|
-
RATE_LIMITER_ADAPTER: "redis",
|
|
516
513
|
ADAPTERS_RATE_LIMITER: "redis",
|
|
517
|
-
OAUTH_CODE_ADAPTER: "redis",
|
|
518
514
|
ADAPTERS_CODE_REPOSITORY: "redis",
|
|
519
|
-
FEDERATION_TOKEN_STORE_TYPE: "redis",
|
|
520
515
|
ADAPTERS_FEDERATION_TOKEN_STORE: "redis",
|
|
521
516
|
REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY: "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=",
|
|
522
|
-
CLIENT_USER_TYPE: "yaml",
|
|
523
517
|
ADAPTERS_USER_REPOSITORY: "yaml",
|
|
524
518
|
OAUTH_RESOURCE_INDICATOR_ENABLED: "true",
|
|
525
519
|
OAUTH_REQUIRE_EMAIL_VERIFIED: "true",
|
|
526
|
-
OAUTH_GRANTS_SESSION_ENABLED: "true",
|
|
527
520
|
OAUTH_SESSION_ENABLED: "true",
|
|
528
521
|
MFA_MODE: "off",
|
|
529
522
|
};
|
|
@@ -782,6 +775,11 @@ describe("the shipped config boots with every documented override supplied as a
|
|
|
782
775
|
// A leftover string here would be read as "on" by a truthiness check
|
|
783
776
|
// and as "off" by `=== true`, for a feature whose whole default is off.
|
|
784
777
|
expect(federationGrantsSection(config).enabled).toBe(true);
|
|
778
|
+
expect(
|
|
779
|
+
standaloneRedisClientsModule.section?.schema.parse(
|
|
780
|
+
(config as Record<string, unknown>)["redis-clients"],
|
|
781
|
+
),
|
|
782
|
+
).toMatchObject({ assumeNoEviction: true });
|
|
785
783
|
});
|
|
786
784
|
|
|
787
785
|
it("turns every non-boolean override into its declared type", async () => {
|
|
@@ -1245,12 +1243,18 @@ describe("the shipped config boots with every documented override supplied as a
|
|
|
1245
1243
|
).rejects.toMatchObject({ reason: "environment-variable-renamed" });
|
|
1246
1244
|
});
|
|
1247
1245
|
|
|
1248
|
-
it("
|
|
1249
|
-
const
|
|
1246
|
+
it("refuses OAUTH_ACCESS_TOKEN_EXPIRES_IN beside its new name set to the same value", async () => {
|
|
1247
|
+
const booting = bootParsed({
|
|
1250
1248
|
...DOCUMENTED_ENV,
|
|
1251
1249
|
OAUTH_ACCESS_TOKEN_EXPIRES_IN:
|
|
1252
1250
|
DOCUMENTED_ENV.OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN as string,
|
|
1253
1251
|
});
|
|
1252
|
+
await expect(booting).rejects.toMatchObject({ reason: "environment-variable-renamed" });
|
|
1253
|
+
await expect(booting).rejects.toThrow(/unset OAUTH_ACCESS_TOKEN_EXPIRES_IN/);
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
it("boots with OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN alone, reading it", async () => {
|
|
1257
|
+
const config = await bootParsed(DOCUMENTED_ENV);
|
|
1254
1258
|
expect(oauthSection(config).accessToken).toEqual({
|
|
1255
1259
|
defaultExpiresIn: 900,
|
|
1256
1260
|
maxExpiresIn: 7200,
|
|
@@ -85,6 +85,10 @@ vi.mock("ioredis", () => {
|
|
|
85
85
|
quit: async () => "OK",
|
|
86
86
|
disconnect: () => undefined,
|
|
87
87
|
ping: async () => "PONG",
|
|
88
|
+
// The Redis stores' eviction gate reads the policy from INFO memory: a
|
|
89
|
+
// default server's.
|
|
90
|
+
info: async (section: string) =>
|
|
91
|
+
section === "memory" ? "# Memory\r\nmaxmemory_policy:noeviction\r\n" : null,
|
|
88
92
|
};
|
|
89
93
|
const makeMockRedis = (): object =>
|
|
90
94
|
new Proxy(
|
|
@@ -0,0 +1,113 @@
|
|
|
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's two logout endpoints and the session cookie: the session
|
|
19
|
+
* store module expires the cookie of a session destroyed during the request,
|
|
20
|
+
* whichever route destroyed it, so `POST /oauth/logout` — whose package knows
|
|
21
|
+
* no cookie name — and `POST /session/logout` both answer with the cookie
|
|
22
|
+
* expired, with the attributes the login set it with.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { SessionCookiePolicy } from "@o3co/auth-provider-core";
|
|
26
|
+
import request from "supertest";
|
|
27
|
+
import { describe, expect, it } from "vitest";
|
|
28
|
+
import { authorize, codeFrom, compose, login, redeem } from "./all-modules-composition.fixture.mjs";
|
|
29
|
+
|
|
30
|
+
/** The `Set-Cookie` lines among `lines` for the cookie `name`. */
|
|
31
|
+
const linesNamed = (lines: readonly string[], name: string): string[] =>
|
|
32
|
+
lines.filter((line) => line.startsWith(`${name}=`));
|
|
33
|
+
|
|
34
|
+
/** A response's `Set-Cookie` lines. */
|
|
35
|
+
const setCookies = (res: request.Response): string[] =>
|
|
36
|
+
([] as string[]).concat(res.headers["set-cookie"] ?? []);
|
|
37
|
+
|
|
38
|
+
/** A `Set-Cookie` line's attributes, lower-cased, with its value and expiry left out. */
|
|
39
|
+
const attributesOf = (line: string): string[] =>
|
|
40
|
+
line
|
|
41
|
+
.split(";")
|
|
42
|
+
.slice(1)
|
|
43
|
+
.map((part) => part.trim().toLowerCase())
|
|
44
|
+
.filter((part) => !part.startsWith("expires=") && !part.startsWith("max-age="))
|
|
45
|
+
.sort();
|
|
46
|
+
|
|
47
|
+
/** The CSRF token a login reissued, read off its double-submit cookie. */
|
|
48
|
+
const csrfTokenOf = (cookies: readonly string[]): string => {
|
|
49
|
+
const cookie = cookies.find((c) => /^[^=]*\.csrf=/.test(c));
|
|
50
|
+
if (cookie === undefined) throw new Error("the login reissued no CSRF cookie");
|
|
51
|
+
return decodeURIComponent(cookie.slice(cookie.indexOf("=") + 1).split(";")[0] ?? "");
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The one `Set-Cookie` line of `res` for the session cookie, checked to
|
|
56
|
+
* expire it with the attributes of `setLine`, the line the login set it with.
|
|
57
|
+
*/
|
|
58
|
+
function expectExpired(res: request.Response, name: string, setLine: string): void {
|
|
59
|
+
const lines = linesNamed(setCookies(res), name);
|
|
60
|
+
expect(lines).toHaveLength(1);
|
|
61
|
+
const [line] = lines as [string];
|
|
62
|
+
expect(line.startsWith(`${name}=;`)).toBe(true);
|
|
63
|
+
expect(line).toContain("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
|
64
|
+
expect(attributesOf(line)).toEqual(attributesOf(setLine));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("the session cookie at the template's logout endpoints", () => {
|
|
68
|
+
it("POST /oauth/logout expires the session cookie, with the attributes the login set it with", async () => {
|
|
69
|
+
const { app, handle } = await compose();
|
|
70
|
+
try {
|
|
71
|
+
const policy = handle.components.sessionCookiePolicy as SessionCookiePolicy;
|
|
72
|
+
const { res: signedIn, cookies } = await login(app);
|
|
73
|
+
expect(signedIn.status).toBe(200);
|
|
74
|
+
const [setLine] = linesNamed(cookies, policy.name);
|
|
75
|
+
expect(setLine).toBeDefined();
|
|
76
|
+
const tokens = await redeem(app, codeFrom(await authorize(app, cookies)));
|
|
77
|
+
expect(tokens.status).toBe(200);
|
|
78
|
+
|
|
79
|
+
const res = await request(app)
|
|
80
|
+
.post("/oauth/logout")
|
|
81
|
+
.set("Cookie", cookies)
|
|
82
|
+
.type("form")
|
|
83
|
+
.send({ id_token_hint: tokens.body.id_token as string });
|
|
84
|
+
|
|
85
|
+
expect(res.status).toBe(200);
|
|
86
|
+
expect(res.body).toEqual({ logged_out: true });
|
|
87
|
+
expectExpired(res, policy.name, setLine as string);
|
|
88
|
+
} finally {
|
|
89
|
+
await handle.dispose();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("POST /session/logout expires the session cookie, with the attributes the login set it with", async () => {
|
|
94
|
+
const { app, handle } = await compose();
|
|
95
|
+
try {
|
|
96
|
+
const policy = handle.components.sessionCookiePolicy as SessionCookiePolicy;
|
|
97
|
+
const { res: signedIn, cookies } = await login(app);
|
|
98
|
+
expect(signedIn.status).toBe(200);
|
|
99
|
+
const [setLine] = linesNamed(cookies, policy.name);
|
|
100
|
+
expect(setLine).toBeDefined();
|
|
101
|
+
|
|
102
|
+
const res = await request(app)
|
|
103
|
+
.post("/session/logout")
|
|
104
|
+
.set("Cookie", cookies)
|
|
105
|
+
.set("x-csrf-token", csrfTokenOf(cookies));
|
|
106
|
+
|
|
107
|
+
expect(res.status).toBe(200);
|
|
108
|
+
expectExpired(res, policy.name, setLine as string);
|
|
109
|
+
} finally {
|
|
110
|
+
await handle.dispose();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|