@o3co/create-auth-provider 0.16.0 → 0.17.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.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 -26
- package/templates/standalone/Dockerfile +6 -2
- package/templates/standalone/README.ja.md +293 -162
- package/templates/standalone/README.md +533 -199
- package/templates/standalone/config/application.conf +196 -341
- 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 +399 -0
- package/templates/standalone/docker-compose.mailpit.yml +36 -0
- package/templates/standalone/docker-compose.production.yml +58 -30
- 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__/access-token-lifetime-alias.test.mts +19 -20
- package/templates/standalone/src/__tests__/adapters.test.mts +358 -0
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +216 -141
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +48 -33
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +77 -58
- package/templates/standalone/src/__tests__/audit-sink.test.mts +78 -76
- package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
- package/templates/standalone/src/__tests__/core-federations.test.mts +226 -0
- package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +9 -14
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +656 -269
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +425 -84
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +91 -70
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +7 -5
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +22 -19
- package/templates/standalone/src/__tests__/google-session.fixture.mts +53 -42
- package/templates/standalone/src/__tests__/host-routes.test.mts +4 -11
- package/templates/standalone/src/__tests__/library-references.fixture.mts +183 -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 +328 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +4 -9
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +780 -0
- package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +52 -46
- package/templates/standalone/src/__tests__/own-modules.test.mts +1457 -0
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +7 -11
- 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 +112 -81
- package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
- package/templates/standalone/src/__tests__/replica-safety.test.mts +153 -142
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +4 -7
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +275 -95
- package/templates/standalone/src/__tests__/sections.test.mts +480 -0
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +67 -55
- package/templates/standalone/src/__tests__/session-requirements.test.mts +299 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +163 -96
- package/templates/standalone/src/__tests__/smoke.test.mts +179 -252
- package/templates/standalone/src/__tests__/store-credential.test.mts +91 -50
- package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +108 -95
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +438 -0
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +390 -0
- package/templates/standalone/src/adapters.mts +149 -0
- package/templates/standalone/src/app.mts +64 -79
- package/templates/standalone/src/buildModules.mts +212 -251
- package/templates/standalone/src/configPath.mts +422 -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 +262 -0
- package/templates/standalone/src/modules.mts +458 -476
- package/templates/standalone/src/rootRenames.mts +79 -0
- package/templates/standalone/src/routes.mts +8 -14
- package/templates/standalone/src/sections.mts +366 -0
- package/templates/standalone/src/shutdown.mts +45 -123
- package/templates/standalone/tests/index.test.js +2 -2
- 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
|
@@ -16,20 +16,17 @@
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* The all-modules composition of `all-modules-composition.test.mts` on more
|
|
19
|
-
* than one replica: every shared store on Redis, `deployment.mode = "multi"`,
|
|
19
|
+
* than one replica: every shared store on Redis, `core.deployment.mode = "multi"`,
|
|
20
20
|
* every module the template can turn on switched on together.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* routes, the same discovery document as on one replica.
|
|
22
|
+
* With every module on, the all-Redis composition boots with nothing declaring
|
|
23
|
+
* replica-unsafe state, and each store switched back to memory is refused at
|
|
24
|
+
* boot, naming the module. The Redis adapters change nothing a client can
|
|
25
|
+
* see: the same routes, the same discovery document as on one replica.
|
|
27
26
|
*
|
|
28
27
|
* ioredis, node-redis and connect-redis are stand-ins, as in
|
|
29
|
-
* `replica-safety.test.mts`:
|
|
30
|
-
*
|
|
31
|
-
* Redis server to run against — `packages/redis` runs its adapters against
|
|
32
|
-
* one.
|
|
28
|
+
* `replica-safety.test.mts`: nothing here issues a command. `packages/redis`
|
|
29
|
+
* runs its adapters against a real server.
|
|
33
30
|
*/
|
|
34
31
|
|
|
35
32
|
import { type Module, replicaUnsafeReason } from "@o3co/auth-provider-core";
|
|
@@ -110,7 +107,7 @@ const MULTI = { env: MULTI_ENV, shippedRefreshTokenFamilyStore: true } as const;
|
|
|
110
107
|
|
|
111
108
|
/** What `buildModules` lists with every switch on and every shared store on Redis. */
|
|
112
109
|
const ALL_ON_REDIS_MODULES = [
|
|
113
|
-
"
|
|
110
|
+
"session-store",
|
|
114
111
|
"federation-grant-background",
|
|
115
112
|
"federation-grants",
|
|
116
113
|
"oauth",
|
|
@@ -118,18 +115,21 @@ const ALL_ON_REDIS_MODULES = [
|
|
|
118
115
|
"oauth-authorization",
|
|
119
116
|
"jwks",
|
|
120
117
|
"session",
|
|
121
|
-
"federation
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"federation
|
|
125
|
-
"
|
|
118
|
+
"federation-google",
|
|
119
|
+
"google-federation-config",
|
|
120
|
+
"oidc-federation-config",
|
|
121
|
+
"federation-oidc-oidc",
|
|
122
|
+
"logging",
|
|
123
|
+
"http",
|
|
124
|
+
"key-store",
|
|
126
125
|
"test:repositories",
|
|
127
|
-
"
|
|
128
|
-
"
|
|
126
|
+
"audit-sink",
|
|
127
|
+
"standard-smtp-mail-sender",
|
|
128
|
+
"redis-clients",
|
|
129
129
|
"redis-federation-token-store",
|
|
130
130
|
"redis-federation-grant-store",
|
|
131
131
|
"redis-federation-grant-intent-store",
|
|
132
|
-
"
|
|
132
|
+
"redis-session-stores",
|
|
133
133
|
"redis-rate-limiter",
|
|
134
134
|
"redis-code-repository",
|
|
135
135
|
"redis-access-token-denylist",
|
|
@@ -141,7 +141,7 @@ const ALL_ON_REDIS_MODULES = [
|
|
|
141
141
|
"subject-revocation-service",
|
|
142
142
|
];
|
|
143
143
|
|
|
144
|
-
describe('every module on, every shared store on Redis, deployment.mode = "multi"', () => {
|
|
144
|
+
describe('every module on, every shared store on Redis, core.deployment.mode = "multi"', () => {
|
|
145
145
|
it("lists every module, none declaring replica-unsafe state, and boots", async () => {
|
|
146
146
|
const modules = composedModules(resolveConfig(MULTI_ENV), MULTI);
|
|
147
147
|
expect(modules.map((m) => m.name)).toEqual(ALL_ON_REDIS_MODULES);
|
|
@@ -166,21 +166,36 @@ describe('every module on, every shared store on Redis, deployment.mode = "multi
|
|
|
166
166
|
expect(discovery.body).toEqual(single.discovery);
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
+
it("boots as multi on CORE_DEPLOYMENT_MODE=multi alone", async () => {
|
|
170
|
+
expect(MULTI_ENV).toMatchObject({ CORE_DEPLOYMENT_MODE: "multi" });
|
|
171
|
+
expect(MULTI_ENV).not.toHaveProperty("DEPLOYMENT_MODE");
|
|
172
|
+
current = await compose(MULTI);
|
|
173
|
+
expect(current.handle.components.deploymentMode).toBe("multi");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("boots as multi on DEPLOYMENT_MODE=multi beside CORE_DEPLOYMENT_MODE=multi, as the umbrella sets them", async () => {
|
|
177
|
+
current = await compose({
|
|
178
|
+
...MULTI,
|
|
179
|
+
env: { ...MULTI_ENV, DEPLOYMENT_MODE: "multi", CORE_DEPLOYMENT_MODE: "multi" },
|
|
180
|
+
});
|
|
181
|
+
expect(current.handle.components.deploymentMode).toBe("multi");
|
|
182
|
+
});
|
|
183
|
+
|
|
169
184
|
/**
|
|
170
185
|
* Each store's switch back to memory, alone, with every module on. The
|
|
171
186
|
* boot names the module that declared the state it would fork.
|
|
172
187
|
*/
|
|
173
188
|
const MEMORY_SWITCHES: ReadonlyArray<readonly [variable: string, module: string]> = [
|
|
174
|
-
["
|
|
175
|
-
["
|
|
176
|
-
["
|
|
177
|
-
["
|
|
178
|
-
["
|
|
179
|
-
["
|
|
180
|
-
["
|
|
181
|
-
["
|
|
182
|
-
["
|
|
183
|
-
["
|
|
189
|
+
["SESSION_STORE_STORAGE_TYPE", "session-store"],
|
|
190
|
+
["ADAPTERS_USER_SESSION_STORES", "standalone-in-memory-session-stores"],
|
|
191
|
+
["ADAPTERS_CODE_REPOSITORY", "standalone-in-memory-code-repository"],
|
|
192
|
+
["ADAPTERS_FEDERATION_TOKEN_STORE", "standalone-in-memory-federation-token-store"],
|
|
193
|
+
["ADAPTERS_RATE_LIMITER", "core-rate-limiter-memory"],
|
|
194
|
+
["ADAPTERS_ACCESS_TOKEN_DENYLIST", "core-access-token-denylist-memory"],
|
|
195
|
+
["ADAPTERS_REPLAY_SEEN_SET", "core-replay-seen-set-memory"],
|
|
196
|
+
["ADAPTERS_CONSENT_STORE", "core-consent-store-memory"],
|
|
197
|
+
["ADAPTERS_FEDERATION_GRANT_STORE", "core-federation-grant-store-memory"],
|
|
198
|
+
["ADAPTERS_FEDERATION_GRANT_INTENT_STORE", "core-federation-grant-intent-store-memory"],
|
|
184
199
|
];
|
|
185
200
|
|
|
186
201
|
it.each(MEMORY_SWITCHES)("%s=memory is refused at boot, naming %s", async (variable, module) => {
|
|
@@ -212,8 +227,8 @@ describe("the shared Redis socket can back every store the Redis package ships",
|
|
|
212
227
|
* the others to the manifest with the modules that read them (the device
|
|
213
228
|
* grant's code store, WebAuthn's challenge store), and each requires its
|
|
214
229
|
* client from this module. One it does not provide is a boot refused with
|
|
215
|
-
* `missing-required-component
|
|
216
|
-
*
|
|
230
|
+
* `missing-required-component`, which `modules.mts` guards against for
|
|
231
|
+
* the device-code store by name.
|
|
217
232
|
*/
|
|
218
233
|
const storeModules: Module[] = Object.entries(redisPackage).flatMap(([name, value]) => {
|
|
219
234
|
if (name.endsWith("ModuleFor") && typeof value === "function") {
|
|
@@ -226,7 +241,7 @@ describe("the shared Redis socket can back every store the Redis package ships",
|
|
|
226
241
|
});
|
|
227
242
|
|
|
228
243
|
it.each(storeModules.map((m) => [m.name, m] as const))(
|
|
229
|
-
"%s: its client slot is provided by
|
|
244
|
+
"%s: its client slot is provided by redis-clients",
|
|
230
245
|
(_name, module) => {
|
|
231
246
|
const provided = Object.keys(standaloneRedisClientsModule.provides ?? {});
|
|
232
247
|
const clients = (module.requires ?? []).filter((slot) => String(slot).endsWith("Client"));
|
|
@@ -16,34 +16,29 @@
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Every module this template can turn on, booted together on one replica
|
|
19
|
-
* through the real composition
|
|
20
|
-
* `createApp`, mounted as `app.mts` mounts it
|
|
21
|
-
* exist only when the modules meet
|
|
22
|
-
* truth about every grant and endpoint,
|
|
23
|
-
* same app, each module's body rules
|
|
24
|
-
* order, and a store outage answered
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* made invalid by a neighbour's contribution, a disabled grant still
|
|
29
|
-
* advertised, one module's prefix parser setting another's body limit, a
|
|
30
|
-
* content-type rule that held in one mount order only. See
|
|
31
|
-
* `all-modules-composition.fixture.mts` for what is real and what is
|
|
32
|
-
* substituted, and `all-modules-composition.multi.test.mts` for the same
|
|
33
|
-
* composition on Redis under `deployment.mode = "multi"`.
|
|
19
|
+
* through the real composition (the shipped HOCON, `buildModules`, core's
|
|
20
|
+
* `createApp`, mounted as `app.mts` mounts it), and held to the contracts that
|
|
21
|
+
* exist only when the modules meet, which no package's own suite can see: one
|
|
22
|
+
* discovery document that tells the truth about every grant and endpoint,
|
|
23
|
+
* every module's route answering in the same app, each module's body rules
|
|
24
|
+
* surviving its neighbours in either mount order, and a store outage answered
|
|
25
|
+
* 503 and logged once. The fixture file says what is real and what is
|
|
26
|
+
* substituted; `all-modules-composition.multi.test.mts` boots the same
|
|
27
|
+
* composition on Redis under `core.deployment.mode = "multi"`.
|
|
34
28
|
*
|
|
35
29
|
* `knownDefect` marks a contract the composition breaks today; its comment
|
|
36
30
|
* names the defect. In the monorepo it is `it.fails`: the fix that mends the
|
|
37
31
|
* defect turns the case red, and turns it into a plain `it`. An outage case
|
|
38
|
-
* pins only the part of the
|
|
32
|
+
* pins only the part of the outage rule that is broken, and asserts the rest
|
|
39
33
|
* (`describeOutages` in the fixture).
|
|
40
34
|
*/
|
|
41
35
|
|
|
42
36
|
import { readdirSync, readFileSync } from "node:fs";
|
|
43
|
-
import
|
|
37
|
+
import { federationsOf } from "@o3co/auth-provider-core";
|
|
44
38
|
import type express from "express";
|
|
45
39
|
import request from "supertest";
|
|
46
40
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
41
|
+
import type { Switches } from "#/configPath.mjs";
|
|
47
42
|
import {
|
|
48
43
|
ALICE,
|
|
49
44
|
AS_LISTED,
|
|
@@ -115,16 +110,22 @@ const TEMPLATE_PACKAGES: Readonly<Record<string, string>> = {
|
|
|
115
110
|
"@o3co/auth-provider-federation-google": "googleFederationModule",
|
|
116
111
|
"@o3co/auth-provider-federation-grants": "federationGrantsModules",
|
|
117
112
|
"@o3co/auth-provider-federation-oidc": "oidcFederationModule, one per `oidc` section",
|
|
118
|
-
"@o3co/auth-provider-foundation":
|
|
113
|
+
"@o3co/auth-provider-foundation":
|
|
114
|
+
"the HTTP user repository adapter; foundationMfaFactorStoreModule under MFA_MODE with adapters.mfaFactorStore = store (tools/composition boots it)",
|
|
115
|
+
"@o3co/auth-provider-mfa":
|
|
116
|
+
"mfaModules (mfaTotpFactorModule, mfaRecoveryCodeFactorModule, mfaEmailFactorModule, mfaModule) and mfaResetModule, under MFA_MODE (mfa-switch.test.mts)",
|
|
119
117
|
"@o3co/auth-provider-oauth":
|
|
120
118
|
"oauthModule, oauthSessionModule, oauthAuthorizationModule, subjectRevocationServiceModule",
|
|
121
119
|
"@o3co/auth-provider-redis": "the Redis stores (all-modules-composition.multi.test.mts)",
|
|
122
120
|
"@o3co/auth-provider-session": "sessionModule, sessionStoreModuleFor",
|
|
121
|
+
"@o3co/auth-provider-standard":
|
|
122
|
+
"standardSmtpMailSenderModule; standardDevelopmentMailSenderModule in development",
|
|
123
123
|
};
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
126
|
* Workspace packages the template does not depend on, so nothing here can
|
|
127
|
-
* boot them. Each
|
|
127
|
+
* boot them. Each but the test kit, which has no module, is a module a
|
|
128
|
+
* deployment adds to this manifest by hand —
|
|
128
129
|
* `modules.mts` provides the Redis clients their stores need for that reason
|
|
129
130
|
* — and `tools/composition` in the monorepo boots each beside every module
|
|
130
131
|
* here, through this file's fixture.
|
|
@@ -136,7 +137,9 @@ const NOT_IN_TEMPLATE: Readonly<Record<string, string>> = {
|
|
|
136
137
|
"@o3co/auth-provider-federation-github": "githubFederationModule",
|
|
137
138
|
"@o3co/auth-provider-mtls": "mtlsModule",
|
|
138
139
|
"@o3co/auth-provider-oauth-token-exchange": "tokenExchangeModule",
|
|
139
|
-
"@o3co/auth-provider-
|
|
140
|
+
"@o3co/auth-provider-test-kit": "no module: contract suites and fakes, for tests",
|
|
141
|
+
"@o3co/auth-provider-webauthn":
|
|
142
|
+
"webauthnModule, webauthnSessionSubjectModule, webauthnMfaFactorModule",
|
|
140
143
|
};
|
|
141
144
|
|
|
142
145
|
describe("what the all-modules composition covers", () => {
|
|
@@ -173,7 +176,7 @@ describe("what the all-modules composition covers", () => {
|
|
|
173
176
|
|
|
174
177
|
/** What `buildModules` lists with every switch on, one replica, memory stores. */
|
|
175
178
|
const ALL_ON_MODULES = [
|
|
176
|
-
"
|
|
179
|
+
"session-store",
|
|
177
180
|
"federation-grant-background",
|
|
178
181
|
"federation-grants",
|
|
179
182
|
"oauth",
|
|
@@ -181,19 +184,22 @@ const ALL_ON_MODULES = [
|
|
|
181
184
|
"oauth-authorization",
|
|
182
185
|
"jwks",
|
|
183
186
|
"session",
|
|
184
|
-
"federation
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"federation
|
|
188
|
-
"
|
|
187
|
+
"federation-google",
|
|
188
|
+
"google-federation-config",
|
|
189
|
+
"oidc-federation-config",
|
|
190
|
+
"federation-oidc-oidc",
|
|
191
|
+
"logging",
|
|
192
|
+
"http",
|
|
193
|
+
"key-store",
|
|
189
194
|
"test:repositories",
|
|
190
|
-
"
|
|
191
|
-
"
|
|
195
|
+
"audit-sink",
|
|
196
|
+
"standard-smtp-mail-sender",
|
|
197
|
+
"standalone-in-memory-federation-token-store",
|
|
192
198
|
"core-federation-grant-store-memory",
|
|
193
199
|
"core-federation-grant-intent-store-memory",
|
|
194
|
-
"standalone
|
|
200
|
+
"standalone-in-memory-session-stores",
|
|
195
201
|
"core-rate-limiter-memory",
|
|
196
|
-
"standalone
|
|
202
|
+
"standalone-in-memory-code-repository",
|
|
197
203
|
"core-access-token-denylist-memory",
|
|
198
204
|
"core-replay-seen-set-memory",
|
|
199
205
|
"core-consent-store-memory",
|
|
@@ -244,10 +250,10 @@ describe("every module the template can turn on boots together", () => {
|
|
|
244
250
|
});
|
|
245
251
|
|
|
246
252
|
it("hands the deployment's logger to every module that answers a request", async () => {
|
|
247
|
-
// `app.mts` fills the `logger` slot so that `
|
|
253
|
+
// `app.mts` fills the `logger` slot so that `LOGGING_LEVEL` and the JSON
|
|
248
254
|
// envelope reach every module; boot hands a module only the slots its
|
|
249
255
|
// manifest names, so a route or grant module without `logger` writes
|
|
250
|
-
// its outage lines to nobody
|
|
256
|
+
// its outage lines to nobody.
|
|
251
257
|
const { modules } = await boot();
|
|
252
258
|
const answering = modules.filter(
|
|
253
259
|
(m) => contributionNames(m, "routes").length + contributionNames(m, "grants").length > 0,
|
|
@@ -325,10 +331,10 @@ describe("discovery", () => {
|
|
|
325
331
|
* is gone from the document and from the app, and nothing else moves.
|
|
326
332
|
*/
|
|
327
333
|
const GRANT_SWITCHES: ReadonlyArray<readonly [grant: string, variable: string]> = [
|
|
328
|
-
["session", "
|
|
329
|
-
["authorization_code", "
|
|
330
|
-
["refresh_token", "
|
|
331
|
-
["client_credentials", "
|
|
334
|
+
["session", "OAUTH_SESSION_ENABLED"],
|
|
335
|
+
["authorization_code", "OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED"],
|
|
336
|
+
["refresh_token", "OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED"],
|
|
337
|
+
["client_credentials", "OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED"],
|
|
332
338
|
];
|
|
333
339
|
|
|
334
340
|
it.each(GRANT_SWITCHES)(
|
|
@@ -336,7 +342,7 @@ describe("discovery", () => {
|
|
|
336
342
|
async (grant, variable) => {
|
|
337
343
|
const { app } = await boot({ env: { ...SINGLE_ENV, [variable]: "false" } });
|
|
338
344
|
const doc = (await request(app).get(DISCOVERY_PATHS[0])).body;
|
|
339
|
-
expectValidMetadata(doc);
|
|
345
|
+
expectValidMetadata(doc, { authorizationCode: grant !== "authorization_code" });
|
|
340
346
|
expect([...doc.grant_types_supported].sort()).toEqual(
|
|
341
347
|
ENABLED_GRANTS.filter((g) => g !== grant),
|
|
342
348
|
);
|
|
@@ -347,11 +353,17 @@ describe("discovery", () => {
|
|
|
347
353
|
.send({ grant_type: grant });
|
|
348
354
|
expect(res.status).toBe(400);
|
|
349
355
|
expect(res.body.error).toBe("unsupported_grant_type");
|
|
356
|
+
if (grant === "authorization_code") {
|
|
357
|
+
// No grant redeems a code, so none is issued: none is served.
|
|
358
|
+
expect((await request(app).get("/oauth/authorize")).status).toBe(404);
|
|
359
|
+
}
|
|
350
360
|
},
|
|
351
361
|
);
|
|
352
362
|
|
|
353
363
|
it("Client ID Metadata Documents off: the flag is gone and nothing else moves", async () => {
|
|
354
|
-
const { app } = await boot({
|
|
364
|
+
const { app } = await boot({
|
|
365
|
+
env: { ...SINGLE_ENV, OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ENABLED: "false" },
|
|
366
|
+
});
|
|
355
367
|
const doc = (await request(app).get(DISCOVERY_PATHS[0])).body;
|
|
356
368
|
expectValidMetadata(doc);
|
|
357
369
|
expect(doc.client_id_metadata_document_supported).toBeUndefined();
|
|
@@ -367,7 +379,7 @@ describe("discovery", () => {
|
|
|
367
379
|
await on.handle.dispose();
|
|
368
380
|
current = undefined;
|
|
369
381
|
|
|
370
|
-
const { app } = await boot({ env: { ...SINGLE_ENV,
|
|
382
|
+
const { app } = await boot({ env: { ...SINGLE_ENV, ADAPTERS_CONSENT_STORE: "none" } });
|
|
371
383
|
const doc = (await request(app).get(DISCOVERY_PATHS[0])).body;
|
|
372
384
|
expectValidMetadata(doc);
|
|
373
385
|
expect(doc.client_id_metadata_document_supported).toBeUndefined();
|
|
@@ -375,27 +387,30 @@ describe("discovery", () => {
|
|
|
375
387
|
});
|
|
376
388
|
|
|
377
389
|
/** The grant connections dropped: they name the OIDC federation. */
|
|
378
|
-
const withoutConnections = (config:
|
|
379
|
-
({
|
|
390
|
+
const withoutConnections = (config: Switches): Switches =>
|
|
391
|
+
({
|
|
392
|
+
...config,
|
|
393
|
+
"federation-grants": { ...config["federation-grants"], connections: {} },
|
|
394
|
+
}) as Switches;
|
|
380
395
|
|
|
381
396
|
const FEATURE_SWITCHES: ReadonlyArray<
|
|
382
397
|
readonly [
|
|
383
398
|
feature: string,
|
|
384
399
|
variable: string,
|
|
385
400
|
path: string,
|
|
386
|
-
config: ((config:
|
|
401
|
+
config: ((config: Switches) => Switches) | undefined,
|
|
387
402
|
]
|
|
388
403
|
> = [
|
|
389
404
|
["federation grants", "FEDERATION_GRANTS_ENABLED", "/oauth/federation-grants", undefined],
|
|
390
405
|
[
|
|
391
406
|
"the Google federation",
|
|
392
|
-
"
|
|
407
|
+
"CORE_FEDERATIONS_GOOGLE_ENABLED",
|
|
393
408
|
"/session/oauth/federation/google",
|
|
394
409
|
undefined,
|
|
395
410
|
],
|
|
396
411
|
[
|
|
397
412
|
"the OIDC federation",
|
|
398
|
-
"
|
|
413
|
+
"CORE_FEDERATIONS_OIDC_ENABLED",
|
|
399
414
|
"/session/oauth/federation/oidc",
|
|
400
415
|
withoutConnections,
|
|
401
416
|
],
|
|
@@ -421,7 +436,7 @@ describe("discovery", () => {
|
|
|
421
436
|
|
|
422
437
|
it("the OIDC federation off while a grant connection names it is refused at boot, naming both", async () => {
|
|
423
438
|
await expect(
|
|
424
|
-
compose({ env: { ...SINGLE_ENV,
|
|
439
|
+
compose({ env: { ...SINGLE_ENV, CORE_FEDERATIONS_OIDC_ENABLED: "false" } }),
|
|
425
440
|
).rejects.toSatisfy((err: unknown) => {
|
|
426
441
|
const e = err as { name?: string; cause?: { message?: string } };
|
|
427
442
|
return (
|
|
@@ -554,7 +569,7 @@ describe("every module's primary route answers in the one app", () => {
|
|
|
554
569
|
|
|
555
570
|
// KNOWN DEFECT (the template's federation config, with the session
|
|
556
571
|
// package's redirect policy): a federation enabled the way the template
|
|
557
|
-
// README documents — its `
|
|
572
|
+
// README documents — its `CORE_FEDERATIONS_OIDC_*` variables — boots, and every
|
|
558
573
|
// login through it then ends at the callback in `500 misconfiguration`
|
|
559
574
|
// "client URL not configured", after the user has signed in upstream.
|
|
560
575
|
// `clientUrl` has no environment form (`application.conf` ships it
|
|
@@ -564,10 +579,13 @@ describe("every module's primary route answers in the one app", () => {
|
|
|
564
579
|
knownDefect(
|
|
565
580
|
"a federation enabled from the documented variables alone either refuses to boot or completes a login",
|
|
566
581
|
async () => {
|
|
567
|
-
const withoutLanding = (config:
|
|
568
|
-
const federations = config
|
|
582
|
+
const withoutLanding = (config: Switches): Switches => {
|
|
583
|
+
const federations = federationsOf(config) as Record<string, Record<string, unknown>>;
|
|
569
584
|
const { clientUrl: _dropped, ...oidc } = federations.oidc ?? {};
|
|
570
|
-
return {
|
|
585
|
+
return {
|
|
586
|
+
...config,
|
|
587
|
+
core: { ...config.core, federations: { ...federations, oidc } },
|
|
588
|
+
} as unknown as Switches;
|
|
571
589
|
};
|
|
572
590
|
let composed: Composition;
|
|
573
591
|
try {
|
|
@@ -746,9 +764,10 @@ describe.each([AS_LISTED, REVERSED] satisfies ModuleOrder[])(
|
|
|
746
764
|
);
|
|
747
765
|
|
|
748
766
|
describe("a request body the OAuth endpoints do not parse", () => {
|
|
749
|
-
// KNOWN DEFECT (packages/oauth, `
|
|
750
|
-
//
|
|
751
|
-
//
|
|
767
|
+
// KNOWN DEFECT (packages/oauth, `routes/token.mts`,
|
|
768
|
+
// `routes/introspectCaller.mts` and `routes.mts`): the token and
|
|
769
|
+
// introspection handlers read `req.body` without checking that a parser
|
|
770
|
+
// ran. A body that is neither a form nor JSON — or none at all —
|
|
752
771
|
// leaves it undefined, the handler throws, and the terminal handler answers
|
|
753
772
|
// `500 server_error` and logs `unhandled_request_error` for the client's
|
|
754
773
|
// own mistake. `/oauth/revoke` checks, and answers 400.
|
|
@@ -918,7 +937,7 @@ const OUTAGES: readonly OutageCase[] = [
|
|
|
918
937
|
.send({ grant_type: "session", scope: "openid" });
|
|
919
938
|
},
|
|
920
939
|
answer: { status: 503, error: "temporarily_unavailable" },
|
|
921
|
-
event: "
|
|
940
|
+
event: "session_admission_unavailable",
|
|
922
941
|
},
|
|
923
942
|
{
|
|
924
943
|
module: "oauth (consent step)",
|
|
@@ -933,7 +952,7 @@ const OUTAGES: readonly OutageCase[] = [
|
|
|
933
952
|
event: "authorize_consent_store_unavailable",
|
|
934
953
|
defects: {
|
|
935
954
|
"store-field":
|
|
936
|
-
"packages/oauth `routes/
|
|
955
|
+
"packages/oauth `routes/authorizeConsent.mts`: `authorize_consent_store_unavailable` carries `clientId` and `err` but no `store` / `step` / `site` field",
|
|
937
956
|
},
|
|
938
957
|
},
|
|
939
958
|
{
|
|
@@ -949,7 +968,7 @@ const OUTAGES: readonly OutageCase[] = [
|
|
|
949
968
|
event: "authorize_pending_consent_store_unavailable",
|
|
950
969
|
defects: {
|
|
951
970
|
"store-field":
|
|
952
|
-
"packages/oauth `routes/
|
|
971
|
+
"packages/oauth `routes/authorizeConsent.mts`: `authorize_pending_consent_store_unavailable` carries `clientId` and `err` but no `store` / `step` / `site` field",
|
|
953
972
|
},
|
|
954
973
|
},
|
|
955
974
|
{
|
|
@@ -1006,7 +1025,7 @@ const OUTAGES: readonly OutageCase[] = [
|
|
|
1006
1025
|
outage.down = true;
|
|
1007
1026
|
return (await login(app)).res;
|
|
1008
1027
|
},
|
|
1009
|
-
// Intended
|
|
1028
|
+
// Intended: the login is not denied for a best-effort index write,
|
|
1010
1029
|
// and the missed write is logged at error because a token minted from
|
|
1011
1030
|
// this session keeps introspecting active after a credential change —
|
|
1012
1031
|
// the runbook pages on it.
|
|
@@ -1034,7 +1053,7 @@ const OUTAGES: readonly OutageCase[] = [
|
|
|
1034
1053
|
{
|
|
1035
1054
|
module: "core (rate-limit guard)",
|
|
1036
1055
|
slot: "rateLimiter",
|
|
1037
|
-
surface: "/oauth/token (
|
|
1056
|
+
surface: "/oauth/token (redis-rate-limiter.failMode = closed)",
|
|
1038
1057
|
run: async (app, outage) => {
|
|
1039
1058
|
outage.down = true;
|
|
1040
1059
|
return tokenRequest(app, M2M).send({ grant_type: "client_credentials", scope: "api.read" });
|
|
@@ -1094,7 +1113,7 @@ const OUTAGES: readonly OutageCase[] = [
|
|
|
1094
1113
|
];
|
|
1095
1114
|
|
|
1096
1115
|
describeOutages(
|
|
1097
|
-
"a store or repository outage
|
|
1116
|
+
"a store or repository outage gets the answer its case pins, and is logged once, at error",
|
|
1098
1117
|
OUTAGES,
|
|
1099
1118
|
(outage) => compose({ outage }),
|
|
1100
1119
|
);
|