@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
|
@@ -14,12 +14,9 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
import {
|
|
17
|
-
type AppConfig,
|
|
18
|
-
consoleLogger,
|
|
19
17
|
defaultRefreshTokenFamilyRevocationModule,
|
|
20
18
|
defaultRefreshTokenFamilyRotationModule,
|
|
21
19
|
jwksModule,
|
|
22
|
-
type Logger,
|
|
23
20
|
type Module,
|
|
24
21
|
memoryAccessTokenDenylistModule,
|
|
25
22
|
memoryConsentStoreModule,
|
|
@@ -27,18 +24,20 @@ import {
|
|
|
27
24
|
memoryFederationGrantStoreModule,
|
|
28
25
|
memoryRateLimiterModule,
|
|
29
26
|
memoryReplaySeenSetModule,
|
|
27
|
+
sessionLifecycleModule,
|
|
30
28
|
} from "@o3co/auth-provider-core";
|
|
31
|
-
import {
|
|
29
|
+
import { googleFederationTypeModule } from "@o3co/auth-provider-federation-google";
|
|
32
30
|
import { federationGrantsModules } from "@o3co/auth-provider-federation-grants";
|
|
33
|
-
import {
|
|
31
|
+
import { oidcFederationTypeModule } from "@o3co/auth-provider-federation-oidc";
|
|
34
32
|
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
oauthAuthorizationGrantsModule,
|
|
34
|
+
oauthEndpointsModule,
|
|
35
|
+
oauthSessionGrantModule,
|
|
38
36
|
subjectRevocationServiceModule,
|
|
39
37
|
} from "@o3co/auth-provider-oauth";
|
|
40
38
|
import {
|
|
41
39
|
redisAccessTokenDenylistModule,
|
|
40
|
+
redisAttemptCounterModule,
|
|
42
41
|
redisCodeRepositoryModule,
|
|
43
42
|
redisConsentStoreModule,
|
|
44
43
|
redisFederationGrantIntentStoreModule,
|
|
@@ -49,399 +48,311 @@ import {
|
|
|
49
48
|
redisReplaySeenSetModule,
|
|
50
49
|
redisSessionStoresModule,
|
|
51
50
|
} from "@o3co/auth-provider-redis";
|
|
51
|
+
import { sessionModule, sessionStoreModule } from "@o3co/auth-provider-session";
|
|
52
52
|
import {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} from "
|
|
53
|
+
standardDevelopmentMailSenderModule,
|
|
54
|
+
standardSmtpMailSenderModule,
|
|
55
|
+
} from "@o3co/auth-provider-standard";
|
|
56
|
+
import type { Switches } from "./configPath.mjs";
|
|
57
|
+
import { mfaModulesFor } from "./mfaSwitch.mjs";
|
|
57
58
|
import {
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
auditSinkModuleFor,
|
|
60
|
+
httpModule,
|
|
60
61
|
inMemoryCodeRepositoryModule,
|
|
61
62
|
inMemoryFederationTokenStoreModule,
|
|
62
63
|
inMemorySessionStoresModule,
|
|
63
64
|
keyStoreModule,
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
loggingModule,
|
|
66
|
+
repositoriesModuleFor,
|
|
66
67
|
standaloneRedisClientsModule,
|
|
67
68
|
} from "./modules.mjs";
|
|
68
69
|
|
|
69
70
|
/**
|
|
70
|
-
* Overrides for the composition. All but `environment` and
|
|
71
|
-
* test-only: they let the smoke test substitute
|
|
72
|
-
* the file-system-backed modules, and production
|
|
73
|
-
* — the defaults match the standalone scaffold.
|
|
71
|
+
* Overrides for the composition. All but `environment` and
|
|
72
|
+
* `mailSenderModules` are test-only: they let the smoke test substitute
|
|
73
|
+
* in-memory implementations of the file-system-backed modules, and production
|
|
74
|
+
* callers should not pass them — the defaults match the standalone scaffold.
|
|
74
75
|
*/
|
|
75
76
|
export interface BuildModulesOverrides {
|
|
76
77
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* the
|
|
82
|
-
*
|
|
78
|
+
* The name this deployment selected its configuration by (`CONFIG_ENV ||
|
|
79
|
+
* NODE_ENV`, computed once in `app.mts`), so the Redis federation-token
|
|
80
|
+
* store's `allow-plaintext` guard reads the environment the config came
|
|
81
|
+
* from, not `NODE_ENV` alone. Omitted, the guard falls back to `NODE_ENV`
|
|
82
|
+
* (and the `deploymentMode` slot core fills from `core.deployment.mode`, which it
|
|
83
|
+
* reads either way). Unless `mailSenderModules` is given, it also chooses
|
|
84
|
+
* the mail sender: `development` installs the one that logs each code, any
|
|
85
|
+
* other name the SMTP one, and none is installed when it is omitted. The
|
|
86
|
+
* MFA module reads it too, for the development sample key's refusal.
|
|
83
87
|
*/
|
|
84
88
|
readonly environment?: string;
|
|
85
|
-
/**
|
|
86
|
-
* Where the composition's own notices go — a deprecated config key, one
|
|
87
|
-
* `config_key_deprecated` line (warn) each. `app.mts` passes the logger it
|
|
88
|
-
* hands every module; omitted, `consoleLogger`.
|
|
89
|
-
*/
|
|
90
|
-
readonly logger?: Logger;
|
|
91
89
|
readonly keyStoreModule?: Module;
|
|
92
90
|
readonly repositoriesModule?: Module;
|
|
93
|
-
readonly storesModule?: Module;
|
|
94
91
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* Smoke tests / unit tests that don't want to open an ioredis connection
|
|
101
|
-
* pass `[memoryRefreshTokenFamilyStoreModule]` here. The override REPLACES
|
|
102
|
-
* the entire group — when the override is provided, `standaloneRedisClientsModule`
|
|
103
|
-
* is dropped from the manifest unless the override list includes it.
|
|
92
|
+
* Replaces the refresh-token family store modules (default
|
|
93
|
+
* `[redisRefreshTokenFamilyStoreModule]`, on the shared ioredis socket).
|
|
94
|
+
* Tests that must not open an ioredis connection pass
|
|
95
|
+
* `[memoryRefreshTokenFamilyStoreModule]`.
|
|
104
96
|
*/
|
|
105
97
|
readonly refreshTokenFamilyModules?: readonly Module[];
|
|
106
98
|
/**
|
|
107
|
-
*
|
|
108
|
-
* a sink they can assert on
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* The override REPLACES the default — it does not add a second provider,
|
|
112
|
-
* which would be a boot-time slot collision. There is intentionally no way
|
|
113
|
-
* to pass "no audit sink": #304's sink policy is that the trail is always
|
|
114
|
-
* wired, and a composition that genuinely wants events discarded says so by
|
|
115
|
-
* providing a sink that discards them.
|
|
99
|
+
* Replaces the module filling the `auditSink` slot (the default writes the
|
|
100
|
+
* trail to stdout); tests substitute a sink they can assert on. It replaces
|
|
101
|
+
* rather than adds, since a second provider would be a boot-time slot
|
|
102
|
+
* collision. There is no way to pass "no audit sink": pass one that discards.
|
|
116
103
|
*/
|
|
117
104
|
readonly auditSinkModule?: Module;
|
|
105
|
+
/**
|
|
106
|
+
* The deployment's own modules behind core's `mailSender` slot. Given, they
|
|
107
|
+
* are installed under every environment name in place of the bundled
|
|
108
|
+
* sender, which is never installed beside them (a second provider would be
|
|
109
|
+
* a boot-time slot collision); an empty list installs no sender, and a
|
|
110
|
+
* module that needs one (the MFA email factor, or the MFA module with
|
|
111
|
+
* `requireEmailProof = "always"`) then refuses the boot.
|
|
112
|
+
*/
|
|
113
|
+
readonly mailSenderModules?: readonly Module[];
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
117
|
+
* Compose the standalone module list from phase one's `config`: the
|
|
118
|
+
* composition root's `adapters`, which choose the adapter behind each slot,
|
|
119
|
+
* its MFA switch, and whether federation grants are installed. Kept out of
|
|
120
|
+
* `app.mts` so a smoke test can check the manifest (that disabling federation
|
|
121
|
+
* grants removes their modules, say) without an HTTP server. The rules for
|
|
122
|
+
* editing the list (order, one module per store slot, the federation types it
|
|
123
|
+
* bundles) are in the template README, "Module Composition Order".
|
|
126
124
|
*/
|
|
127
|
-
|
|
125
|
+
export function buildModules(config: Switches, overrides: BuildModulesOverrides = {}): Module[] {
|
|
126
|
+
const adapters = config.adapters;
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
* `federations.<name>` of type "oidc": `oidcFederationModule(name)` per
|
|
144
|
-
* instance plus the one `oidcFederationConfigModule` that reads every
|
|
145
|
-
* instance's config into the slot they share. A section's `type` decides
|
|
146
|
-
* which implementation owns the name, so the two gates never both select
|
|
147
|
-
* the same section.
|
|
148
|
-
*/
|
|
149
|
-
export function buildModules(config: AppConfig, overrides: BuildModulesOverrides = {}): Module[] {
|
|
150
|
-
// A section's `type` names the implementation: `federations.google` is the
|
|
151
|
-
// built-in Google federation only when its type is `google` (the default
|
|
152
|
-
// for that name). With `type = "oidc"` it is a generic OIDC instance named
|
|
153
|
-
// "google" (#524), and composing both would contribute the same
|
|
154
|
-
// federation and redirect-policy keys twice.
|
|
155
|
-
const googleEnabled =
|
|
156
|
-
extractFederationSection(config.federations ?? {}, "google")?.type === "google";
|
|
157
|
-
const oidcFederations = oidcFederationNames(config.federations ?? {});
|
|
158
|
-
const logger = overrides.logger ?? consoleLogger;
|
|
128
|
+
// Federation grants: a user's standing consent that a client may obtain
|
|
129
|
+
// upstream tokens with no session behind the call. Off by default, and off
|
|
130
|
+
// installs nothing: no store, no socket, no boot requirement. On, the routes
|
|
131
|
+
// and the background registry the shutdown drains come as a pair; each store
|
|
132
|
+
// follows its own selection (grants in Redis with acquisition in memory is
|
|
133
|
+
// a supported single-replica shape); and the subject-revocation service is
|
|
134
|
+
// installed, whose `federationGrantStore` edge makes "revoke everything this
|
|
135
|
+
// subject holds" include the grants. Enabling it also states the deployment
|
|
136
|
+
// has a consent page, a callback per connection and a user repository
|
|
137
|
+
// covering each connection's registration; the routes module refuses at
|
|
138
|
+
// boot what is missing, naming it. Phase one decides it (`readSwitches`): a
|
|
139
|
+
// value boot would refuse installs the feature, so that boot refuses it
|
|
140
|
+
// rather than reading the feature as off.
|
|
141
|
+
const federationGrantsEnabled = config["federation-grants"]?.enabled === true;
|
|
159
142
|
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// the
|
|
163
|
-
//
|
|
164
|
-
// `
|
|
165
|
-
|
|
166
|
-
// Redis-backed store module), the shared `standaloneRedisClientsModule`
|
|
167
|
-
// is added once and provides every per-purpose ComponentMap slot from a
|
|
168
|
-
// single ioredis socket per replica. Memory-only deployments skip it.
|
|
169
|
-
const rateLimiterAdapter = config.rateLimiter?.adapter ?? "memory";
|
|
170
|
-
const userSessionStoresAdapter = config.userSessionStores?.adapter ?? "memory";
|
|
171
|
-
// #277: the RFC 7009 access-token denylist. Unlike the switches above there
|
|
172
|
-
// is no "no denylist" branch — `oauthModule` reads the `accessTokenDenylist`
|
|
173
|
-
// slot because it mounts `/oauth/revoke`, and core's boot validator refuses
|
|
174
|
-
// a composition that reads the slot with nothing filling it, since the
|
|
175
|
-
// endpoint would answer 200 while the token kept working. The switch here is
|
|
176
|
-
// only over WHICH denylist.
|
|
177
|
-
const accessTokenDenylistAdapter = config.accessTokenDenylist?.adapter ?? "memory";
|
|
178
|
-
// #484: the jti single-use record behind private_key_jwt client auth.
|
|
179
|
-
const replaySeenSetAdapter = config.replaySeenSet?.adapter ?? "memory";
|
|
180
|
-
// #527: the consent store behind /authorize for clients that are not
|
|
181
|
-
// first-party. `"none"` (the default) wires nothing — such clients are
|
|
182
|
-
// refused, as before — so a first-party-only deployment pays nothing and
|
|
183
|
-
// a multi-replica one is not handed a memory store it cannot run.
|
|
184
|
-
// `"redis"` (#561) is the store such a deployment selects.
|
|
185
|
-
const consentStoreAdapter = config.consentStore?.adapter ?? "none";
|
|
186
|
-
// #456: adapter switch for the federation token store. `"memory"` by
|
|
187
|
-
// default, the template's local-dev shape; `"redis"` mounts
|
|
188
|
-
// `redisFederationTokenStoreModule` off the shared socket — what the README
|
|
189
|
-
// promised and what the previous factory-based module never delivered: it
|
|
190
|
-
// built the Redis store without a client and failed at boot.
|
|
191
|
-
const federationTokenStoreAdapter = config.federationTokenStore?.type ?? "memory";
|
|
192
|
-
// #593 slice 7: federation grants — a user's standing consent that a client
|
|
193
|
-
// may obtain upstream tokens with no session behind the call. Off by
|
|
194
|
-
// default, and off installs nothing: no store, no socket, no boot
|
|
195
|
-
// requirement a deployment that never asked for grants would have to meet.
|
|
196
|
-
// On, the routes and the background registry the shutdown drains come as a
|
|
197
|
-
// pair, each store follows its own switch (grants in Redis with acquisition
|
|
198
|
-
// in memory is a supported single-replica shape), and the subject-revocation
|
|
199
|
-
// service is installed so a credential change can reach the grants — its
|
|
200
|
-
// `federationGrantStore` edge is what makes "revoke everything this subject
|
|
201
|
-
// holds" include them. Enabling the feature is also a statement that the
|
|
202
|
-
// deployment has a consent page, a callback per connection and a user
|
|
203
|
-
// repository that covers each connection's registration; the routes module
|
|
204
|
-
// refuses at boot what is missing, naming it.
|
|
205
|
-
const federationGrantsEnabled = config.federationGrants?.enabled === true;
|
|
206
|
-
const federationGrantStoreAdapter = config.federationGrantStore?.adapter ?? "memory";
|
|
207
|
-
const federationGrantIntentStoreAdapter = config.federationGrantIntentStore?.adapter ?? "memory";
|
|
208
|
-
|
|
209
|
-
// OR-9: effective code-repo adapter. `oauth.code.adapter` is the
|
|
210
|
-
// authoritative switch; the legacy `repositories.code.type = "redis"`
|
|
211
|
-
// path is honored with a deprecation warn so existing operators
|
|
212
|
-
// relying on `CLIENT_CODE_TYPE=redis` env-var overrides keep working
|
|
213
|
-
// until they migrate. See CHANGELOG for the removal version.
|
|
214
|
-
const oauthCodeAdapter = config.oauth?.code?.adapter;
|
|
215
|
-
const legacyCodeType = (config.repositories?.code as { type?: string } | undefined)?.type;
|
|
216
|
-
let codeRepositoryAdapter: "memory" | "redis";
|
|
217
|
-
if (oauthCodeAdapter !== undefined) {
|
|
218
|
-
codeRepositoryAdapter = oauthCodeAdapter;
|
|
219
|
-
} else if (legacyCodeType === "redis") {
|
|
220
|
-
logger.warn(
|
|
221
|
-
{
|
|
222
|
-
key: "repositories.code.type",
|
|
223
|
-
env: "CLIENT_CODE_TYPE",
|
|
224
|
-
replacement: "oauth.code.adapter",
|
|
225
|
-
replacementEnv: "OAUTH_CODE_ADAPTER",
|
|
226
|
-
},
|
|
227
|
-
"config_key_deprecated",
|
|
228
|
-
);
|
|
229
|
-
codeRepositoryAdapter = "redis";
|
|
230
|
-
} else {
|
|
231
|
-
codeRepositoryAdapter = "memory";
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// The access-token lifetime's deprecated alias: `oauth.accessToken.expiresIn`
|
|
235
|
-
// (OAUTH_ACCESS_TOKEN_EXPIRES_IN) is still read as the default while
|
|
236
|
-
// `defaultExpiresIn` is unset — `resolveAccessTokenLifetime` does that, and
|
|
237
|
-
// this only says so. Core's `reference.conf` keeps the shipped lifetime on
|
|
238
|
-
// the deprecated key, so "the alias supplied the default" is every
|
|
239
|
-
// deployment that set nothing; an override of it is the one case with
|
|
240
|
-
// something to move, the reading `legacyCodeType === "redis"` gives above.
|
|
241
|
-
// See CHANGELOG for the removal version.
|
|
242
|
-
const accessToken = config.oauth.accessToken;
|
|
243
|
-
if (
|
|
244
|
-
accessToken.defaultExpiresIn === undefined &&
|
|
245
|
-
accessToken.expiresIn !== SHIPPED_ACCESS_TOKEN_EXPIRES_IN
|
|
246
|
-
) {
|
|
247
|
-
logger.warn(
|
|
248
|
-
{
|
|
249
|
-
key: "oauth.accessToken.expiresIn",
|
|
250
|
-
env: "OAUTH_ACCESS_TOKEN_EXPIRES_IN",
|
|
251
|
-
replacement: "oauth.accessToken.defaultExpiresIn",
|
|
252
|
-
replacementEnv: "OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN",
|
|
253
|
-
},
|
|
254
|
-
"config_key_deprecated",
|
|
255
|
-
);
|
|
256
|
-
}
|
|
143
|
+
// MFA: the template's own switch, `mfaMode` (MFA_MODE). `off` installs
|
|
144
|
+
// nothing of MFA; `optional` and `required` install what `mfaModulesFor`
|
|
145
|
+
// lists, the configuration then expects `mfa`
|
|
146
|
+
// (`expectedSessionRequirements`) and `mfa.mode` is written from the
|
|
147
|
+
// switch (`resolveForBoot`).
|
|
148
|
+
const mfaInstalled = config.mfaMode !== "off";
|
|
257
149
|
|
|
258
150
|
const refreshTokenFamilyModules: readonly Module[] = overrides.refreshTokenFamilyModules ?? [
|
|
259
151
|
redisRefreshTokenFamilyStoreModule,
|
|
260
152
|
];
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
// would otherwise boot-fail on the missing `refreshTokenFamilyClient`
|
|
265
|
-
// component (Copilot review on PR #121).
|
|
153
|
+
// Whether the family modules keep the Redis-backed store (the default
|
|
154
|
+
// does). Without the shared clients module added for it, that store fails
|
|
155
|
+
// boot on the missing `refreshTokenFamilyClient` component.
|
|
266
156
|
const refreshTokenFamilyUsesRedis = refreshTokenFamilyModules.some(
|
|
267
157
|
(m) => m.name === "redis-refresh-token-family-store",
|
|
268
158
|
);
|
|
159
|
+
// When ANY selection is `"redis"`, or the refresh-token family modules keep
|
|
160
|
+
// the Redis store (the default), the shared `standaloneRedisClientsModule`
|
|
161
|
+
// is added once, one ioredis socket per replica; memory-only deployments
|
|
162
|
+
// open none.
|
|
269
163
|
const usingRedisAnywhere =
|
|
270
164
|
refreshTokenFamilyUsesRedis ||
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
//
|
|
165
|
+
adapters.rateLimiter === "redis" ||
|
|
166
|
+
adapters.attemptCounter === "redis" ||
|
|
167
|
+
adapters.userSessionStores === "redis" ||
|
|
168
|
+
adapters.codeRepository === "redis" ||
|
|
169
|
+
adapters.accessTokenDenylist === "redis" ||
|
|
170
|
+
adapters.replaySeenSet === "redis" ||
|
|
171
|
+
adapters.consentStore === "redis" ||
|
|
172
|
+
adapters.federationTokenStore === "redis" ||
|
|
173
|
+
// Only while the feature is on: a selection left at "redis" for a
|
|
174
|
+
// feature that is off must not open a socket.
|
|
280
175
|
(federationGrantsEnabled &&
|
|
281
|
-
(
|
|
176
|
+
(adapters.federationGrantStore === "redis" ||
|
|
177
|
+
adapters.federationGrantIntentStore === "redis")) ||
|
|
178
|
+
(mfaInstalled &&
|
|
179
|
+
(adapters.mfaFactorStore === "redis" || adapters.mfaTransactionStore === "redis"));
|
|
282
180
|
|
|
283
|
-
// The four user-session stores
|
|
284
|
-
// federation-token store is always
|
|
285
|
-
//
|
|
286
|
-
// federation-token-store provider entirely (Copilot review on PR #121);
|
|
287
|
-
// splitting `storesModule` fixed that boot failure.
|
|
181
|
+
// The four user-session stores and the subject-level revocation pair
|
|
182
|
+
// follow `adapters.userSessionStores`; the federation-token store is always
|
|
183
|
+
// wired and follows its own selection below.
|
|
288
184
|
const sessionStoresModules: Module[] =
|
|
289
|
-
|
|
185
|
+
adapters.userSessionStores === "redis"
|
|
290
186
|
? [redisSessionStoresModule]
|
|
291
|
-
:
|
|
292
|
-
? [overrides.storesModule]
|
|
293
|
-
: [inMemorySessionStoresModule];
|
|
187
|
+
: [inMemorySessionStoresModule];
|
|
294
188
|
|
|
295
189
|
const rateLimiterModules: Module[] =
|
|
296
|
-
|
|
190
|
+
adapters.rateLimiter === "redis" ? [redisRateLimiterModule] : [memoryRateLimiterModule];
|
|
297
191
|
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
192
|
+
// `memory` installs no counter: the login's attempt guard counts per
|
|
193
|
+
// process, which `core.deployment.mode = "multi"` refuses.
|
|
194
|
+
const attemptCounterModules: Module[] =
|
|
195
|
+
adapters.attemptCounter === "redis" ? [redisAttemptCounterModule] : [];
|
|
196
|
+
|
|
197
|
+
// Mutually exclusive: both modules provide the `codeRepository` slot, and
|
|
198
|
+
// including both would be a boot-time slot collision.
|
|
302
199
|
const codeRepositoryModules: Module[] =
|
|
303
|
-
|
|
200
|
+
adapters.codeRepository === "redis"
|
|
304
201
|
? [redisCodeRepositoryModule]
|
|
305
202
|
: [inMemoryCodeRepositoryModule];
|
|
306
203
|
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
204
|
+
// The RFC 7009 access-token denylist has no "none": `oauthEndpointsModule` mounts
|
|
205
|
+
// `/oauth/revoke` and reads the `accessTokenDenylist` slot, and core's boot
|
|
206
|
+
// validator refuses a composition that reads the slot with nothing filling
|
|
207
|
+
// it, since the endpoint would answer 200 while the token kept working. The
|
|
208
|
+
// memory denylist is a dev convenience: it forks per replica, so
|
|
209
|
+
// `core.deployment.mode = "multi"` refuses it by name. The template ships
|
|
210
|
+
// `"redis"`.
|
|
311
211
|
const accessTokenDenylistModules: Module[] =
|
|
312
|
-
|
|
212
|
+
adapters.accessTokenDenylist === "redis"
|
|
313
213
|
? [redisAccessTokenDenylistModule]
|
|
314
214
|
: [memoryAccessTokenDenylistModule];
|
|
315
215
|
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
216
|
+
// Opt-in: `"none"` (the default) wires nothing, and a client that is not
|
|
217
|
+
// first-party is refused. Each module provides both slots the consent step
|
|
218
|
+
// needs (the consent records and the requests parked while the page asks),
|
|
219
|
+
// so the two cannot be wired apart. The memory one declares itself
|
|
220
|
+
// replica-unsafe and `core.deployment.mode = "multi"` refuses it by name;
|
|
221
|
+
// the Redis one shares both over the ioredis socket.
|
|
321
222
|
const consentStoreModules: Module[] =
|
|
322
|
-
|
|
223
|
+
adapters.consentStore === "redis"
|
|
323
224
|
? [redisConsentStoreModule]
|
|
324
|
-
:
|
|
225
|
+
: adapters.consentStore === "memory"
|
|
325
226
|
? [memoryConsentStoreModule]
|
|
326
227
|
: [];
|
|
327
228
|
|
|
328
|
-
//
|
|
329
|
-
// shape as the session stores. One module per adapter, so the memory one
|
|
330
|
-
// can declare `replicaSafety` on its manifest and the Redis one can
|
|
331
|
-
// `require` its client slot. The previous single module chose the adapter
|
|
332
|
-
// at runtime under one name — the replica guard could not tell the two
|
|
333
|
-
// apart, and the Redis branch had no client to hand the builder.
|
|
334
|
-
//
|
|
335
|
-
// #473: the Redis module is built for this composition root so its
|
|
336
|
-
// plaintext guard knows which environment selected the config; it reads
|
|
337
|
-
// `deployment.mode` off the config itself.
|
|
229
|
+
// The jti single-use record behind private_key_jwt client authentication.
|
|
338
230
|
const replaySeenSetModules: Module[] =
|
|
339
|
-
|
|
231
|
+
adapters.replaySeenSet === "redis" ? [redisReplaySeenSetModule] : [memoryReplaySeenSetModule];
|
|
340
232
|
|
|
233
|
+
// One federation-token-store module per adapter, so the memory one can
|
|
234
|
+
// declare `replicaSafety` on its manifest and the Redis one can `require`
|
|
235
|
+
// its client slot. The Redis module is built for this composition root so
|
|
236
|
+
// its plaintext guard knows which environment selected the config; it
|
|
237
|
+
// reads the replica count from core's `deploymentMode` slot.
|
|
341
238
|
const federationTokenStoreModules: Module[] =
|
|
342
|
-
|
|
239
|
+
adapters.federationTokenStore === "redis"
|
|
343
240
|
? [redisFederationTokenStoreModuleFor({ environment: overrides.environment })]
|
|
344
241
|
: [inMemoryFederationTokenStoreModule];
|
|
345
242
|
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
// routes module itself
|
|
353
|
-
//
|
|
243
|
+
// One module per store, nothing while the feature is off or the store is
|
|
244
|
+
// `"none"` (the default), which the routes module refuses at boot naming
|
|
245
|
+
// the store. The Redis grant store is built for this composition root so
|
|
246
|
+
// its plaintext guard knows which environment selected the config, as the
|
|
247
|
+
// federation-token store's is. Both memory modules declare
|
|
248
|
+
// `replicaSafety`, so `core.deployment.mode = "multi"` refuses them by name;
|
|
249
|
+
// the routes module itself refuses a Redis grant store beside memory
|
|
250
|
+
// user-session stores, because the grants would outlive the boundary that
|
|
251
|
+
// ends them.
|
|
354
252
|
const federationGrantStoreModules: Module[] = !federationGrantsEnabled
|
|
355
253
|
? []
|
|
356
|
-
:
|
|
254
|
+
: adapters.federationGrantStore === "redis"
|
|
357
255
|
? [
|
|
358
256
|
overrides.environment === undefined
|
|
359
257
|
? redisFederationGrantStoreModuleFor()
|
|
360
258
|
: redisFederationGrantStoreModuleFor({ environment: overrides.environment }),
|
|
361
259
|
]
|
|
362
|
-
:
|
|
260
|
+
: adapters.federationGrantStore === "memory"
|
|
261
|
+
? [memoryFederationGrantStoreModule]
|
|
262
|
+
: [];
|
|
363
263
|
const federationGrantIntentStoreModules: Module[] = !federationGrantsEnabled
|
|
364
264
|
? []
|
|
365
|
-
:
|
|
265
|
+
: adapters.federationGrantIntentStore === "redis"
|
|
366
266
|
? [redisFederationGrantIntentStoreModule]
|
|
367
|
-
:
|
|
267
|
+
: adapters.federationGrantIntentStore === "memory"
|
|
268
|
+
? [memoryFederationGrantIntentStoreModule]
|
|
269
|
+
: [];
|
|
270
|
+
|
|
271
|
+
// What the MFA switch installs (`mfaSwitch.mts`): nothing while it is off.
|
|
272
|
+
const mfaInstalledModules = mfaModulesFor({
|
|
273
|
+
mode: config.mfaMode,
|
|
274
|
+
adapters,
|
|
275
|
+
storeTransport: config.storeTransport,
|
|
276
|
+
environment: overrides.environment,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// The mail sender behind core's `mailSender` slot: the deployment's own when
|
|
280
|
+
// given, otherwise the bundled one for the environment. The development one
|
|
281
|
+
// logs each code and refuses the boot where the configuration or NODE_ENV
|
|
282
|
+
// is production or staging, or the deployment multi-replica.
|
|
283
|
+
const mailSenderModules: readonly Module[] =
|
|
284
|
+
overrides.mailSenderModules ??
|
|
285
|
+
(overrides.environment === undefined
|
|
286
|
+
? []
|
|
287
|
+
: overrides.environment === "development"
|
|
288
|
+
? [standardDevelopmentMailSenderModule({ environment: overrides.environment })]
|
|
289
|
+
: [standardSmtpMailSenderModule]);
|
|
368
290
|
|
|
369
291
|
return [
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// `deployment.mode = "multi"` refuses it by name like the other stores.
|
|
379
|
-
sessionStoreModuleFor(config),
|
|
380
|
-
// #593 slice 7: the grants routes mount under `/oauth` beside
|
|
381
|
-
// `oauthModule`'s router, which parses the bodies of its own routes
|
|
382
|
-
// only, so each parses its own requests and their place relative to
|
|
383
|
-
// `oauthModule` does not matter. The browser half sits after the
|
|
384
|
-
// session middleware by its own `after`, wherever it is listed.
|
|
292
|
+
// MUST stay first: it declares no `before`/`after`, so this position is
|
|
293
|
+
// what mounts express-session ahead of every session-consuming module.
|
|
294
|
+
// It declares `session-store.storage.type = "memory"` replica-unsafe from
|
|
295
|
+
// its own section, so `core.deployment.mode = "multi"` refuses it.
|
|
296
|
+
sessionStoreModule,
|
|
297
|
+
// Under `/oauth` beside `oauthEndpointsModule`, each parsing its own requests, so
|
|
298
|
+
// their relative order does not matter; the browser half orders itself
|
|
299
|
+
// after the session middleware by its own `after`.
|
|
385
300
|
...(federationGrantsEnabled ? federationGrantsModules : []),
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// issuer config. Co-installed with oauthModule so the discovery
|
|
393
|
-
// `jwks_uri` (advertised by oauthModule when an issuer is set) always
|
|
394
|
-
// resolves to this mounted route.
|
|
301
|
+
oauthEndpointsModule,
|
|
302
|
+
oauthSessionGrantModule,
|
|
303
|
+
oauthAuthorizationGrantsModule,
|
|
304
|
+
// Always wired: a provider that signs tokens must publish its
|
|
305
|
+
// verification keys regardless of OIDC issuer config, and `oauthEndpointsModule`'s
|
|
306
|
+
// discovery `jwks_uri` (advertised when an issuer is set) must resolve.
|
|
395
307
|
jwksModule,
|
|
396
308
|
sessionModule,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
309
|
+
// The federation types the template bundles, always: each contributes
|
|
310
|
+
// its type, and boot hands it every enabled `core.federations` entry
|
|
311
|
+
// that names it, under the entry's name. Nothing here reads an entry.
|
|
312
|
+
googleFederationTypeModule(),
|
|
313
|
+
oidcFederationTypeModule(),
|
|
314
|
+
// The template's own settings: `logging {}` and `http {}`.
|
|
315
|
+
loggingModule,
|
|
316
|
+
httpModule,
|
|
401
317
|
overrides.keyStoreModule ?? keyStoreModule,
|
|
402
|
-
overrides.repositoriesModule ??
|
|
403
|
-
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
overrides.auditSinkModule ??
|
|
410
|
-
|
|
411
|
-
// actually needs Redis. Memory-only deployments skip this so they
|
|
412
|
-
// don't open an unused socket.
|
|
318
|
+
overrides.repositoriesModule ??
|
|
319
|
+
repositoriesModuleFor({ client: adapters.clientRepository, user: adapters.userRepository }),
|
|
320
|
+
// The audit sink, always wired: `emitAuditEvent` no-ops on an empty slot,
|
|
321
|
+
// so the routes' security events (`token.issued.failure`,
|
|
322
|
+
// `authorize.rejected`, `rate_limit.unavailable`, …) would go nowhere.
|
|
323
|
+
// Which sink is a selection (`adapters.auditSink`); whether there is one
|
|
324
|
+
// is not.
|
|
325
|
+
overrides.auditSinkModule ?? auditSinkModuleFor(adapters.auditSink),
|
|
326
|
+
...mailSenderModules,
|
|
413
327
|
...(usingRedisAnywhere ? [standaloneRedisClientsModule] : []),
|
|
414
|
-
// Federation-token store: redis (multi-replica) or memory (dev). Always
|
|
415
|
-
// wired, independent of the `userSessionStores.adapter` switch — it was
|
|
416
|
-
// bundled into `storesModule` pre-Wave-5d, and the redis session-stores
|
|
417
|
-
// branch dropped the provider.
|
|
418
328
|
...federationTokenStoreModules,
|
|
419
|
-
// #593 slice 7: the grant store and the intent store, when the feature is on.
|
|
420
329
|
...federationGrantStoreModules,
|
|
421
330
|
...federationGrantIntentStoreModules,
|
|
422
|
-
// User-session-store family: redis (multi-replica) or memory (dev).
|
|
423
331
|
...sessionStoresModules,
|
|
424
|
-
//
|
|
332
|
+
// Core's session lifecycle, over the session stores' lifecycle store:
|
|
333
|
+
// a login opens its session's record, a code exchange joins its relying
|
|
334
|
+
// party and family to it, and both logout endpoints close it, which
|
|
335
|
+
// revokes the families and tells the relying parties. Its sweep resumes
|
|
336
|
+
// a close left pending.
|
|
337
|
+
sessionLifecycleModule,
|
|
425
338
|
...rateLimiterModules,
|
|
426
|
-
|
|
339
|
+
...attemptCounterModules,
|
|
427
340
|
...codeRepositoryModules,
|
|
428
|
-
// RFC 7009 access-token denylist: redis (shared revocations) or memory
|
|
429
|
-
// (single-instance dev). Always wired — see the switch above.
|
|
430
341
|
...accessTokenDenylistModules,
|
|
431
342
|
...replaySeenSetModules,
|
|
432
|
-
// Consent records for clients that are not first-party: redis
|
|
433
|
-
// (multi-replica, #561), memory (single-instance), or nothing (#527).
|
|
434
343
|
...consentStoreModules,
|
|
435
|
-
// RT family store: redis by default (closes OR-1); override path
|
|
436
|
-
// swaps to `[memoryRefreshTokenFamilyStoreModule]` for unit tests.
|
|
437
344
|
...refreshTokenFamilyModules,
|
|
438
345
|
defaultRefreshTokenFamilyRotationModule,
|
|
439
346
|
defaultRefreshTokenFamilyRevocationModule,
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
|
|
347
|
+
// MFA's modules and stores; the MFA routes order themselves after the
|
|
348
|
+
// session middleware.
|
|
349
|
+
...mfaInstalledModules,
|
|
350
|
+
// The composed "end everything this subject holds" a credential change
|
|
351
|
+
// calls, reached as `handle.components.subjectRevocationService`.
|
|
352
|
+
// Installed with federation grants, which is what makes it reach the
|
|
353
|
+
// grants, and with MFA, whose operator reset ends every session and
|
|
354
|
+
// token of the subject's through it; otherwise a Store calls core's
|
|
355
|
+
// revokeAllForSubject.
|
|
356
|
+
...(federationGrantsEnabled || mfaInstalled ? [subjectRevocationServiceModule] : []),
|
|
446
357
|
];
|
|
447
358
|
}
|