@o3co/create-auth-provider 0.17.0-rc.1 → 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/package.json +1 -1
- package/templates/standalone/.env.example +0 -3
- package/templates/standalone/README.ja.md +42 -18
- package/templates/standalone/README.md +120 -44
- package/templates/standalone/config/application.conf +40 -36
- package/templates/standalone/config/reference.conf +21 -2
- package/templates/standalone/docker-compose.production.yml +5 -2
- package/templates/standalone/package.json +1 -1
- package/templates/standalone/src/__tests__/adapters.test.mts +80 -19
- package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +87 -0
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +112 -98
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +27 -6
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +86 -51
- package/templates/standalone/src/__tests__/audit-sink.test.mts +10 -9
- package/templates/standalone/src/__tests__/config-notices.test.mts +154 -0
- package/templates/standalone/src/__tests__/core-federations.test.mts +48 -25
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +1 -1
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +349 -54
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +15 -21
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +9 -15
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +10 -4
- package/templates/standalone/src/__tests__/federation-types.test.mts +239 -0
- package/templates/standalone/src/__tests__/google-session.fixture.mts +60 -67
- package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +1 -1
- package/templates/standalone/src/__tests__/library-references.fixture.mts +51 -46
- package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +216 -0
- package/templates/standalone/src/__tests__/mailpit-compose.test.mts +0 -2
- package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +122 -0
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +70 -25
- package/templates/standalone/src/__tests__/own-modules.test.mts +40 -11
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +3 -3
- package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +8 -4
- package/templates/standalone/src/__tests__/replica-safety.test.mts +127 -19
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +15 -20
- package/templates/standalone/src/__tests__/session-lifecycle.test.mts +215 -0
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +3 -7
- package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +130 -0
- package/templates/standalone/src/__tests__/session-requirements.test.mts +114 -31
- package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +270 -0
- package/templates/standalone/src/__tests__/smoke.test.mts +19 -28
- package/templates/standalone/src/__tests__/store-credential.test.mts +8 -13
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +50 -34
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +242 -76
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +23 -31
- package/templates/standalone/src/adapters.mts +31 -18
- package/templates/standalone/src/app.mts +6 -5
- package/templates/standalone/src/bootRefusal.mts +117 -0
- package/templates/standalone/src/buildModules.mts +46 -96
- package/templates/standalone/src/configPath.mts +180 -153
- package/templates/standalone/src/mfaSwitch.mts +43 -20
- package/templates/standalone/src/modules.mts +25 -213
- package/templates/standalone/src/rootRenames.mts +35 -18
- package/templates/standalone/src/sections.mts +1 -0
- package/templates/standalone/tests/index.test.js +4 -3
- package/templates/versions.json +18 -18
- package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +0 -134
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +0 -202
- package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +0 -144
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @o3co/auth-provider-standalone
|
|
2
2
|
|
|
3
|
-
Last updated: 2026-10-
|
|
3
|
+
Last updated: 2026-10-06
|
|
4
4
|
|
|
5
5
|
Deployable server template for auth.provider. This is the composition root — it reads config, loads modules, and starts the Express server. Generated by `@o3co/create-auth-provider`.
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ the Google and generic OpenID Connect federation adapters, `-federation-grants`,
|
|
|
26
26
|
slot), the signing key store, the client and user repositories, the audit
|
|
27
27
|
sink, the one shared
|
|
28
28
|
Redis connection, the in-memory user-session stores, code repository and
|
|
29
|
-
federation token store
|
|
29
|
+
federation token store —
|
|
30
30
|
[`src/modules.mts`](src/modules.mts), with the defaults of the sections they
|
|
31
31
|
own in [`config/reference.conf`](config/reference.conf). They are the
|
|
32
32
|
scaffold's rather than a package's because each builds its component from
|
|
@@ -104,7 +104,7 @@ Nothing secret lives in `clients.yaml` for such a client.
|
|
|
104
104
|
|
|
105
105
|
**What this does not do.** It does not make `/authorize` safe against forced navigation for a client that *is* first-party — that remains the accepted model here. What it prevents is a client that should never have been trusted with a silent code being registered into that position by accident.
|
|
106
106
|
|
|
107
|
-
**Unmarked registrations.** A registration without the field is not first-party: on the defaults it is refused at `/authorize` until you mark it, with no admit-with-warning window. Mark every client you operate before pointing it at `/authorize`. `oauth.authorize.allowUnmarkedClients` is not a setting: a config or environment that sets it (`OAUTH_AUTHORIZE_ALLOW_UNMARKED_CLIENTS`, any value) fails at boot
|
|
107
|
+
**Unmarked registrations.** A registration without the field is not first-party: on the defaults it is refused at `/authorize` until you mark it, with no admit-with-warning window. Mark every client you operate before pointing it at `/authorize`. `oauth.authorize.allowUnmarkedClients` is not a setting: a config or environment that sets it (`OAUTH_AUTHORIZE_ALLOW_UNMARKED_CLIENTS`, any value) fails at boot rather than being silently ignored: the oauth module refuses the key as removed (`config-path-relocated`), and the variable as one bound to a removed key (`environment-variable-renamed`). Mark your clients, then delete the key and unset the variable.
|
|
108
108
|
|
|
109
109
|
## Multi-replica deployments
|
|
110
110
|
|
|
@@ -127,7 +127,11 @@ It takes four shapes, all of them Express's own:
|
|
|
127
127
|
|
|
128
128
|
An allowlist is a network control, not a cryptographic one. The edge must also **strip** inbound `X-Forwarded-*` headers rather than appending to them, and the hop between it and this process must not be reachable by anyone able to spoof a source address.
|
|
129
129
|
|
|
130
|
-
**Point `adapters.rateLimiter` at Redis.** It defaults to `"memory"`, which is per-process: with N replicas every configured limit is effectively N times larger and resets on every deploy. The memory adapter is also **evadable under bucket exhaustion**: it caps itself at 10,000 buckets and, at the cap, admitting a new key evicts the bucket closest to reset — so an attacker who can present many source IPs (and `req.ip` is client-influenced whenever `HTTP_TRUST_PROXY` is broader than your actual hops) can churn the table until a target's counter is evicted and starts over. That is acceptable for one dev process; it is not a production rate limit.
|
|
130
|
+
**Point `adapters.rateLimiter` at Redis.** It defaults to `"memory"`, which is per-process: with N replicas every configured limit is effectively N times larger and resets on every deploy. The memory adapter is also **evadable under bucket exhaustion**: it caps itself at 10,000 buckets and, at the cap, admitting a new key evicts the bucket closest to reset — so an attacker who can present many source IPs (and `req.ip` is client-influenced whenever `HTTP_TRUST_PROXY` is broader than your actual hops) can churn the table until a target's counter is evicted and starts over. That is acceptable for one dev process; it is not a production rate limit. `/session/login` does not run on this component, nor does `POST /oauth/device/verification` once you add the device grant: each one's own attempt limit (`session.rateLimit.login`, `device-grant.rateLimit`) is counted on the attempt counter, and no limiter setting changes it.
|
|
131
|
+
|
|
132
|
+
**The MFA routes' budget is the limiter's.** The MFA package ships no budget for its routes: every `/session/mfa` POST is limited per client address under the prefix `mfa` by the wired limiter alone. `config/reference.conf` gives both limiters `limits.mfa { limit = 60, windowSeconds = 300 }` (`core-rate-limiter-memory.limits.mfa`, `redis-rate-limiter.limits.mfa`), so switching `adapters.rateLimiter` keeps it. It is a default there, so the limiter the composition does not wire carries it without boot naming its section; change it for the wired limiter in `config/application.conf` or a layer of your own. Without an entry the limiter's `defaultLimit`, 60 per 60 s, applies. The MFA lock — attempts per transaction, the backoff, the weekly failures — is the MFA package's own and no limiter setting changes it.
|
|
133
|
+
|
|
134
|
+
**Point `adapters.attemptCounter` at Redis.** It defaults to `"memory"`, which wires no counter: the login — and device verification, when you add the device grant — counts its attempts per process, so with N replicas `session.rateLimit.login` (and `device-grant.rateLimit`) allows N times its limit and resets on every deploy, and `CORE_DEPLOYMENT_MODE=multi` refuses the boot. `"redis"` (`ADAPTERS_ATTEMPT_COUNTER=redis`) installs `redisAttemptCounterModule` on the shared Redis socket. Its Redis must run `maxmemory-policy noeviction` (Redis's default): an evicted window would start the count over. The module refuses to boot on any other policy it reads; when the server will not tell it (`INFO` and `CONFIG` refused or renamed) it boots and warns `attempt_counter_durability_unchecked`, so confirm the policy where the server is configured. Both fail closed: while the counter cannot answer, `/session/login` and `POST /oauth/device/verification` answer `503`, whatever `redis-rate-limiter.failMode` says.
|
|
131
135
|
|
|
132
136
|
**Behind a BFF, raise `limits.token` before you need to.** The OAuth-endpoint
|
|
133
137
|
rate limits key on `req.ip` (`packages/core/src/ratelimit/guard.mts` builds the
|
|
@@ -173,7 +177,7 @@ raising it globally to accommodate one topology would weaken it for the other.
|
|
|
173
177
|
Put the throttling that protects the BFF's *own* users in front of the BFF,
|
|
174
178
|
where per-user identity still exists.
|
|
175
179
|
|
|
176
|
-
**Set `CORE_DEPLOYMENT_MODE=multi` once you run more than one replica.** Boot then *fails* if any in-memory store that has to be shared is still wired, naming every offender and what it costs — user sessions forking (back-channel logout reaches one replica, a logged-out session stays valid on the others), rate-limit counters multiplying, access-token revocation not propagating, a single-use client assertion or WebAuthn challenge replayable once per replica. The check reads the declaration each installed module carries on its own manifest rather than a list of library module names, so this template's own in-memory modules — the user-session stores (`ADAPTERS_USER_SESSION_STORES=memory`), the authorization-code repository (`ADAPTERS_CODE_REPOSITORY=memory`) and the federation token store (`ADAPTERS_FEDERATION_TOKEN_STORE=memory`, the default) — are refused by name too. So are express-session's own store under `SESSION_STORE_STORAGE_TYPE=memory` (#474) and the default memory rate limiter (`core-rate-limiter-memory`, `ADAPTERS_RATE_LIMITER=memory`). With the mode unset nothing is refused: every one of them is listed in a single `replica_unsafe_adapters` warning at boot. (The
|
|
180
|
+
**Set `CORE_DEPLOYMENT_MODE=multi` once you run more than one replica.** Boot then *fails* if any in-memory store that has to be shared is still wired, naming every offender and what it costs — user sessions forking (back-channel logout reaches one replica, a logged-out session stays valid on the others), rate-limit counters multiplying, access-token revocation not propagating, a single-use client assertion or WebAuthn challenge replayable once per replica. The check reads the declaration each installed module carries on its own manifest rather than a list of library module names, so this template's own in-memory modules — the user-session stores (`ADAPTERS_USER_SESSION_STORES=memory`), the authorization-code repository (`ADAPTERS_CODE_REPOSITORY=memory`) and the federation token store (`ADAPTERS_FEDERATION_TOKEN_STORE=memory`, the default) — are refused by name too. So are express-session's own store under `SESSION_STORE_STORAGE_TYPE=memory` (#474) and the default memory rate limiter (`core-rate-limiter-memory`, `ADAPTERS_RATE_LIMITER=memory`). With the mode unset nothing is refused: every one of them is listed in a single `replica_unsafe_adapters` warning at boot. (The WebAuthn-options route carries a per-process fallback limiter that warns on its own, but only in a composition that wires no `rateLimiter` at all, which this template never does. The login counts per process under `ADAPTERS_ATTEMPT_COUNTER=memory`: `multi` refuses the boot, naming the login, and an unset mode warns `attempt_counter_not_shared`; see the operator runbook.) With `CORE_DEPLOYMENT_MODE=single` the check is silent, because you have said there is one replica. This template does not install DPoP; a composition that adds it records every accepted proof in the same replay seen-set as `private_key_jwt` (`ADAPTERS_REPLAY_SEEN_SET`), so it gets the same answer — `memory` is refused under `CORE_DEPLOYMENT_MODE=multi` and listed in the unset-mode warning, and the shipped `redis` shares DPoP's records across replicas — see the dpop package's [operator requirements](../../packages/dpop/README.md#operator-requirements).
|
|
177
181
|
|
|
178
182
|
The variable sets `core.deployment.mode`. `DEPLOYMENT_MODE`, its old name, refuses boot set alone or beside `CORE_DEPLOYMENT_MODE` at a different value; beside it at the same value, it boots.
|
|
179
183
|
|
|
@@ -296,7 +300,10 @@ Configuration is loaded from `config/application.conf` (HOCON format), over the
|
|
|
296
300
|
`http.cors`), `key-store`, the shared Redis connection's `redis-clients`,
|
|
297
301
|
`repositories`, the in-process code repository's
|
|
298
302
|
`standalone-in-memory-code-repository` and `audit-sink` — and of the
|
|
299
|
-
composition root's own `adapters` and `mfaMode` (below)
|
|
303
|
+
composition root's own `adapters` and `mfaMode` (below), and the MFA
|
|
304
|
+
routes' budget in both rate limiters' sections
|
|
305
|
+
(`core-rate-limiter-memory.limits.mfa`, `redis-rate-limiter.limits.mfa`;
|
|
306
|
+
see [Multi-replica deployments](#multi-replica-deployments)). Set a deployment's own value in
|
|
300
307
|
the two files above, not there. `config/reference.conf` binds each of these
|
|
301
308
|
keys' variables beside its default, so a value either file above sets wins
|
|
302
309
|
over the variable, except for `HTTP_PORT`, `HTTP_TRUST_PROXY`,
|
|
@@ -311,35 +318,61 @@ It reads the two files above once, under one snapshot of the environment
|
|
|
311
318
|
[`src/configPath.mts`](src/configPath.mts)) — so a file replaced, or a
|
|
312
319
|
variable changed, while the process starts cannot make boot parse something
|
|
313
320
|
other than what the modules were chosen by. First, before it knows its
|
|
314
|
-
modules, it reads
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
own schema (`readAdapters`): a selection still written at the path it moved
|
|
322
|
-
from, or a variable renamed with it, is refused there, before any module is
|
|
323
|
-
chosen, naming the new path and variable. It reads the MFA switch `mfaMode`
|
|
321
|
+
modules, it reads what `buildModules` chooses them by — never a federation
|
|
322
|
+
entry — from the two files above over the template's `config/reference.conf`
|
|
323
|
+
alone (`readSwitches`). It reads `adapters` — which adapter fills each slot,
|
|
324
|
+
the composition root's own section — with the template's own schema
|
|
325
|
+
(`readAdapters`): a selection still written at the path it moved from, or a
|
|
326
|
+
variable renamed with it, is refused there, before any module is chosen,
|
|
327
|
+
naming the new path and variable. It reads the MFA switch `mfaMode`
|
|
324
328
|
(`MFA_MODE`), the composition root's own key, the same way, with the
|
|
325
329
|
template's own schema (`readMfaSwitch`), and beside them the user
|
|
326
330
|
repository's HTTP settings, unparsed, which the Store-backed MFA factor store
|
|
327
|
-
is built over.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
+
is built over. It reads whether federation grants are installed from
|
|
332
|
+
`federation-grants.enabled` (`FEDERATION_GRANTS_ENABLED`, which
|
|
333
|
+
`application.conf` binds for this), with core's `coerceBooleanFromEnv`: a
|
|
334
|
+
value that does not read as `false` installs the federation-grants modules,
|
|
335
|
+
so that boot refuses a bad value at `federation-grants.enabled` rather than
|
|
336
|
+
the feature reading as off, and a setting at the section's old path,
|
|
337
|
+
`federationGrants`, installs them too, for boot to refuse naming the new
|
|
338
|
+
path. Boot is not handed `adapters` or `mfaMode`. Phase one reads no other
|
|
339
|
+
key, and sees nothing a package's `reference.conf` alone sets — none is
|
|
340
|
+
layered yet. A module you add to `buildModules` reads its own section at
|
|
341
|
+
boot (`deps.section`), never when it is built. The log level is read before boot too, as
|
|
331
342
|
the `logging` module's section, with that module's schema, over the template's
|
|
332
343
|
`reference.conf` (`readLogging`): the logger exists before boot, since the
|
|
333
|
-
template logs while it reads its configuration and chooses its modules.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
344
|
+
template logs while it reads its configuration and chooses its modules.
|
|
345
|
+
What the template refuses of the configuration before boot — `adapters`,
|
|
346
|
+
`mfaMode` and `logging` in phase one, and what it builds for boot below
|
|
347
|
+
(`resolveForBoot`) — is a `BootError` under the reason boot raises for the
|
|
348
|
+
same case (`config-path-relocated`, `environment-variable-renamed`,
|
|
349
|
+
`config-validation-failed`, `module-section-path-invalid`;
|
|
350
|
+
[`src/bootRefusal.mts`](src/bootRefusal.mts)), so an alert on a boot error's
|
|
351
|
+
reason sees it too. Not every failure to start carries a reason: a
|
|
352
|
+
`CONFIG_ENV` naming a file outside `config/` is a plain `Error`, a missing
|
|
353
|
+
`{ENV}.conf` or a file HOCON cannot parse is the HOCON library's error, and
|
|
354
|
+
a failure after boot, such as a port the listener cannot bind, is its own. Then it hands `createApp` the configuration as resolved over every
|
|
355
|
+
loaded module's `reference.conf` (`resolveForBoot`), unparsed, with
|
|
356
|
+
`core.sessionRequirements` as the MFA switch expects it written in — the
|
|
357
|
+
configuration's list with `mfa` added when the switch installs MFA; a value
|
|
358
|
+
that is not a list of names is handed on as written, for boot to refuse — and
|
|
359
|
+
the `mfa` section as the MFA switch decides it (see [Multi-factor authentication](#multi-factor-authentication)):
|
|
337
360
|
boot parses it once, with every loaded module's schema. What
|
|
338
361
|
the template reads after boot — the trusted hops, the port, the readiness
|
|
339
362
|
deadline — it reads from the `http` module: core's `httpSettings` slot and the
|
|
340
|
-
template's `httpHostSettings`.
|
|
341
|
-
|
|
342
|
-
|
|
363
|
+
template's `httpHostSettings`. Beside it, the template hands boot the
|
|
364
|
+
configuration's defaults (`configDefaultsFor`): the same `reference.conf`
|
|
365
|
+
files with neither of the two files above and no environment. Boot names a
|
|
366
|
+
top-level section no loaded module owns by them, once, at warn, and keeps it:
|
|
367
|
+
a section a loaded package's `reference.conf` sets for a module the
|
|
368
|
+
composition does not load (`redis-clients` with no store on Redis, say) is
|
|
369
|
+
silent while it is as that file sets it, and `config_sections_not_loaded`
|
|
370
|
+
once your files or a variable change it — the setting reaches nothing; a
|
|
371
|
+
section no loaded package sets is `config_sections_ignored`, which is where a
|
|
372
|
+
misspelt section name shows. A renamed variable a loaded package's
|
|
373
|
+
`reference.conf` captures and no loaded module declares is named
|
|
374
|
+
`environment_variables_not_applied`. The [operator
|
|
375
|
+
runbook](../../docs/operator-runbook.md) has a row for each.
|
|
343
376
|
|
|
344
377
|
Values in the overlay take precedence over `application.conf`. The scaffold
|
|
345
378
|
ships with `development.conf`, `production.conf` and `mailpit.conf` (the
|
|
@@ -359,6 +392,7 @@ fails before boot, naming it.
|
|
|
359
392
|
| Variable | Default | Description |
|
|
360
393
|
|---|---|---|
|
|
361
394
|
| `ADAPTERS_RATE_LIMITER` | `memory` | `adapters.rateLimiter`: `memory` or `redis`. See [Multi-replica deployments](#multi-replica-deployments) |
|
|
395
|
+
| `ADAPTERS_ATTEMPT_COUNTER` | `memory` | `adapters.attemptCounter`: the counter behind the login's attempt limit (and device verification's, when the device grant is added), `memory` (none: per process) or `redis`. See [Multi-replica deployments](#multi-replica-deployments) |
|
|
362
396
|
| `ADAPTERS_USER_SESSION_STORES` | `memory` | `adapters.userSessionStores`: the user-session stores, `memory` or `redis`. Keep it with `SESSION_STORE_STORAGE_TYPE` (see [Docker](#docker)) |
|
|
363
397
|
| `ADAPTERS_ACCESS_TOKEN_DENYLIST` | `redis` | `adapters.accessTokenDenylist`: `memory` or `redis` |
|
|
364
398
|
| `ADAPTERS_REPLAY_SEEN_SET` | `redis` | `adapters.replaySeenSet`: the replay seen-set behind `private_key_jwt`, `memory` or `redis` |
|
|
@@ -447,7 +481,6 @@ every relying party must be handed the shared secret — which also lets it
|
|
|
447
481
|
|---|---|---|
|
|
448
482
|
| `OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN` | `3600` | Access token lifetime in seconds that every grant mints when the request asks for none. Whole positive number, at most one year (`31536000`). |
|
|
449
483
|
| `OAUTH_ACCESS_TOKEN_MAX_EXPIRES_IN` | the default | The most a token-exchange request's `expires_in` parameter can obtain; a larger request is clamped to it. Unset means the default, so no token is extended unless you set this. A default above it is a boot failure naming both keys. It also bounds how long an exchanged token outlives a revocation at a resource server that validates it offline. |
|
|
450
|
-
| `OAUTH_ACCESS_TOKEN_EXPIRES_IN` | — | **Deprecated** alias of `OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN` (config key `oauth.accessToken.expiresIn`, of `oauth.accessToken.defaultExpiresIn`), read only while the new one is unset; while it still decides the default, boot logs `config_key_deprecated` (warn). Move the value to the new variable. |
|
|
451
484
|
| `OAUTH_REFRESH_TOKEN_EXPIRES_IN` | `86400` | Refresh token lifetime in seconds. Whole positive number, at most one year (`31536000`). |
|
|
452
485
|
|
|
453
486
|
Each is read only as decimal digits. Exporting one as an empty string is a
|
|
@@ -457,6 +490,12 @@ boot failure, not a fallback (HOCON resolves `FOO=` to `""`), and so is a hex
|
|
|
457
490
|
Only token exchange (RFC 8693) reads an `expires_in` request parameter; every
|
|
458
491
|
other grant ignores it and mints the default.
|
|
459
492
|
|
|
493
|
+
The default's old name, `OAUTH_ACCESS_TOKEN_EXPIRES_IN` (config key
|
|
494
|
+
`oauth.accessToken.expiresIn`), refuses boot set alone or beside
|
|
495
|
+
`OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN` at a different value; beside it at the
|
|
496
|
+
same value, it boots. The old key refuses boot at any value. Move the value to
|
|
497
|
+
the new name rather than deleting it: without it, the default is `3600`.
|
|
498
|
+
|
|
460
499
|
### Grant Types
|
|
461
500
|
|
|
462
501
|
| Variable | Default | Description |
|
|
@@ -468,11 +507,11 @@ other grant ignores it and mints the default.
|
|
|
468
507
|
| `OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED` | `false` | Enable the jwt-bearer grant type (RFC 7523) |
|
|
469
508
|
|
|
470
509
|
Each switch is its module's key — `oauth-session.enabled`, and
|
|
471
|
-
`oauth-authorization.grants.<grant>.enabled` for the others
|
|
472
|
-
|
|
473
|
-
the
|
|
474
|
-
alone or beside the new name at a different
|
|
475
|
-
they boot.
|
|
510
|
+
`oauth-authorization.grants.<grant>.enabled` for the others. Both modules are
|
|
511
|
+
always loaded and read their switches at boot, from the configuration boot
|
|
512
|
+
parses; the template reads none of them before boot. `OAUTH_GRANTS_<GRANT>_ENABLED`,
|
|
513
|
+
the old names, refuse boot set alone or beside the new name at a different
|
|
514
|
+
value; beside it at the same value, they boot.
|
|
476
515
|
|
|
477
516
|
### Session
|
|
478
517
|
|
|
@@ -601,6 +640,44 @@ key's path under `core.federations`; a `FEDERATIONS_GOOGLE_*` or
|
|
|
601
640
|
`FEDERATIONS_OIDC_*` variable set alone, or beside its new name at a different
|
|
602
641
|
value, is refused before any module is chosen.
|
|
603
642
|
|
|
643
|
+
Every entry names its `type`, and boot hands each enabled entry to the module
|
|
644
|
+
that handles that type, under the entry's name. The template always loads the
|
|
645
|
+
two types it bundles — `google` (`@o3co/auth-provider-federation-google`) and
|
|
646
|
+
`oidc` (`@o3co/auth-provider-federation-oidc`) — whatever the map says, and
|
|
647
|
+
reads no entry itself: a key an entry carries is read, and a malformed one
|
|
648
|
+
refused at its path (`core.federations.<name>.<key>`), by core and by the
|
|
649
|
+
type's schema. An entry with no `type`, enabled or not, is refused at
|
|
650
|
+
`core.federations.<name>.type` (`config-validation-failed`); an enabled entry
|
|
651
|
+
of a type no loaded module handles refuses the boot
|
|
652
|
+
(`federation-type-unhandled`), naming the entry.
|
|
653
|
+
GitHub and Apple are not bundled: a deployment that wants one adds its
|
|
654
|
+
package and its module to `buildModules` — GitHub's type module,
|
|
655
|
+
`githubFederationTypeModule()`, with entries of `type = "github"`; Apple's
|
|
656
|
+
per its [README](../../packages/federation-apple/README.md).
|
|
657
|
+
|
|
658
|
+
`core.federations.google` ships with `type = "google"`. A second Google
|
|
659
|
+
client is a second entry of that type, under its own name and with its own
|
|
660
|
+
callback:
|
|
661
|
+
|
|
662
|
+
```hocon
|
|
663
|
+
core.federations {
|
|
664
|
+
google-work {
|
|
665
|
+
enabled = true
|
|
666
|
+
type = "google"
|
|
667
|
+
clientId = ${GOOGLE_WORK_CLIENT_ID}
|
|
668
|
+
clientSecret = ${GOOGLE_WORK_CLIENT_SECRET}
|
|
669
|
+
callbackURL = "https://auth.example.com/session/oauth/federation/google-work/callback"
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
The keys a `google` entry takes beside the variables below — `redirectAllowlist`,
|
|
675
|
+
`sessionDomain`, `authCallbackUrl`, `clientUrl`,
|
|
676
|
+
`requireAuthorizationResponseIss`, `endSessionEndpoint` — are listed in full
|
|
677
|
+
in [the package's README](../../packages/federation-google/README.md);
|
|
678
|
+
`config/application.conf` shows every one of them but `endSessionEndpoint`.
|
|
679
|
+
A key the type does not name refuses the entry.
|
|
680
|
+
|
|
604
681
|
| Variable | Default | Description |
|
|
605
682
|
|---|---|---|
|
|
606
683
|
| `CORE_FEDERATIONS_GOOGLE_ENABLED` | `false` | Enable Google OAuth federation |
|
|
@@ -641,9 +718,8 @@ core.federations {
|
|
|
641
718
|
}
|
|
642
719
|
```
|
|
643
720
|
|
|
644
|
-
The entry's `type` names the implementation
|
|
645
|
-
`
|
|
646
|
-
OIDC instance named `google`, and the built-in module is not composed.
|
|
721
|
+
The entry's `type` names the implementation, never its name: an entry named
|
|
722
|
+
`google` with `type = "oidc"` is a generic OIDC federation named `google`.
|
|
647
723
|
|
|
648
724
|
Every field the package accepts — `scopes`, `discovery` / `endpoints`,
|
|
649
725
|
`privateKey`, `userInfo`, `idTokenSignedResponseAlg`, `clockToleranceSeconds` —
|
|
@@ -997,7 +1073,7 @@ auth.provider instances cannot collide in the same database:
|
|
|
997
1073
|
|
|
998
1074
|
| Variable | Default | Description |
|
|
999
1075
|
|---|---|---|
|
|
1000
|
-
| `REDIS_SESSION_STORES_KEY_PREFIX` | `ss:` | Outer prefix for user sessions,
|
|
1076
|
+
| `REDIS_SESSION_STORES_KEY_PREFIX` | `ss:` | Outer prefix for user sessions, the subject session index, the subject-revocation boundaries and the session lifecycle records. |
|
|
1001
1077
|
| `REDIS_REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX` | `rtfam:` | Prefix for refresh-token family records. |
|
|
1002
1078
|
| `REDIS_CODE_REPOSITORY_KEY_PREFIX` | `oauth:code:` | Prefix for OAuth authorization codes. |
|
|
1003
1079
|
| `REDIS_FEDERATION_TOKEN_STORE_KEY_PREFIX` | `ft:` | Prefix for federation token records, their per-session index and their lock keys. |
|
|
@@ -1021,15 +1097,15 @@ are configured per federation (`CORE_FEDERATIONS_GOOGLE_CALLBACK_URL`,
|
|
|
1021
1097
|
|
|
1022
1098
|
[`src/buildModules.mts`](src/buildModules.mts) is the single source of truth for which modules are composed and in what order — read it rather than a copy of it. The boot planner resolves `requires` / `provides` topologically, but it mounts routes and middleware in list order unless a module declares `before` / `after` — so position matters for anything that mounts. These are the rules to keep when you edit the list:
|
|
1023
1099
|
|
|
1024
|
-
1. **`
|
|
1025
|
-
2. **Under `/oauth`, the order does not matter.** `federationGrantsModules` (while federation grants are enabled), `
|
|
1100
|
+
1. **`sessionStoreModule` stays first.** It mounts the `express-session` middleware and declares no `before` / `after`, so its position in the list is what puts it ahead of every route that reads `req.session`. It declares its replica safety from its own section, so `session-store.storage.type = "memory"` is replica-unsafe and refused under `core.deployment.mode = "multi"`.
|
|
1101
|
+
2. **Under `/oauth`, the order does not matter.** `federationGrantsModules` (while federation grants are enabled), `oauthEndpointsModule` and any module of your own may all mount routes under `/oauth`. `oauthEndpointsModule`'s router parses the bodies of its own routes only, so each module's requests reach its own parsers whatever the list order — provided every module under `/oauth` parses its own body and scopes its parsers to exactly its own paths, as the shipped ones do — a route (`router.all(path, parser)` or the route's own handler list), not `router.use(path, parser)`, which matches every path beneath `path` as well. The federation grants browser half orders itself after the session middleware with its own `after`.
|
|
1026
1102
|
3. **One module per store slot.** Each adapter switch — `adapters.federationTokenStore`, `adapters.userSessionStores`, `adapters.rateLimiter`, `adapters.codeRepository`, `adapters.accessTokenDenylist`, `adapters.replaySeenSet`, `adapters.consentStore`, the two federation-grant store switches and the two MFA store switches — picks one of a memory / Redis pair (or, for the MFA factors, the Store). Both provide the same slot, so wiring both is a boot-time slot collision. `adapters.consentStore = "none"` wires neither, the federation-grant stores are wired only while the feature is enabled, and the MFA stores only while `MFA_MODE` installs MFA.
|
|
1027
1103
|
4. **The shared Redis connection comes with the first Redis-backed module.** `standaloneRedisClientsModule` opens the one ioredis connection every Redis adapter here uses, from its own section (`redis-clients`), and is added whenever a composed module needs one. The refresh-token family store is on Redis in the shipped composition, so a deployment always has it; the in-memory family store is a test override (`overrides.refreshTokenFamilyModules`).
|
|
1028
1104
|
5. **The template's own settings modules are always composed.** `loggingModule` and `httpModule` own `logging {}` and `http {}`, the CORS list among `http`'s keys (`http.cors.allowedOrigins`). `httpModule` provides core's `httpSettings`, which is authoritative, so no `overrideComponents` entry replaces it while the module is loaded; it also provides the template's `httpHostSettings`, which `app.mts` reads after boot for the port and the readiness deadline. The logger is built before boot from the `logging` section (`readLogging`), not by a module, and handed to boot as the `logger` component.
|
|
1029
|
-
6. **
|
|
1030
|
-
7. **The mail sender follows the environment.** `@o3co/auth-provider-standard`'s development sender, which logs each code, where the configuration was selected as `development`; its module installs only where that name, and `CONFIG_ENV` and `NODE_ENV` wherever they are set, each read `development` or `test`, and refuses the boot otherwise, or where `core.deployment.mode` is `multi`. Under any other name, the SMTP sender's module, whose section is `standard-smtp-mail-sender` ([the package's README](../../packages/standard/README.md)); its sender is built only where something reads the `mailSender` slot, and there the boot needs `STANDARD_SMTP_MAIL_SENDER_HOST` and `STANDARD_SMTP_MAIL_SENDER_FROM`. While `MFA_MODE` is `off` nothing reads the slot, so the template boots without them; with MFA on, the MFA module and the operator reset read it. A sender of your own replaces this choice: pass its module where [`src/app.mts`](src/app.mts) calls `buildModules`, as `buildModules(switches, { environment: env,
|
|
1105
|
+
6. **The federation types are always composed.** `googleFederationTypeModule()` and `oidcFederationTypeModule()` are listed whatever `core.federations` says: each contributes its type, and boot builds one federation per enabled entry of it, under the entry's name, so a federation is added or removed in configuration alone. Nothing in the list reads an entry. A module that contributes a federation under an entry's name directly beside an entry dispatched by its type is a boot-time `duplicate-contribute`: add a federation of another kind as a type module, and give its entries that type.
|
|
1106
|
+
7. **The mail sender follows the environment.** `@o3co/auth-provider-standard`'s development sender, which logs each code, where the configuration was selected as `development`; its module installs only where that name, and `CONFIG_ENV` and `NODE_ENV` wherever they are set, each read `development` or `test`, and refuses the boot otherwise, or where `core.deployment.mode` is `multi`. Under any other name, the SMTP sender's module, whose section is `standard-smtp-mail-sender` ([the package's README](../../packages/standard/README.md)); its sender is built only where something reads the `mailSender` slot, and there the boot needs `STANDARD_SMTP_MAIL_SENDER_HOST` and `STANDARD_SMTP_MAIL_SENDER_FROM`. While `MFA_MODE` is `off` nothing reads the slot, so the template boots without them; with MFA on, the MFA module and the operator reset read it. A sender of your own replaces this choice: pass its module where [`src/app.mts`](src/app.mts) calls `buildModules`, as `buildModules(switches, { environment: env, mailSenderModules: [mySenderModule] })`. Given, `mailSenderModules` is installed under every environment name and the bundled sender is not, so the two never both provide the slot; an empty list installs no sender, and a module that needs one (the MFA email factor, or the MFA module with `requireEmailProof = "always"`) then refuses the boot.
|
|
1031
1107
|
|
|
1032
|
-
`jwksModule` (from core) is always composed: a provider that signs tokens publishes its verification keys whether or not an issuer is configured. What each route module mounts is in its package's README.
|
|
1108
|
+
`jwksModule` (from core) is always composed: a provider that signs tokens publishes its verification keys whether or not an issuer is configured. What each route module mounts is in its package's README. The template composes core's `sessionLifecycleModule`: a login opens its session's record, a code exchange joins it, and both `POST /session/logout` and `POST /oauth/logout` close the session through the lifecycle, which revokes the session's refresh-token families and tells its relying parties. See [Which logout endpoint invalidates what](../../docs/operator-runbook.md#which-logout-endpoint-invalidates-what).
|
|
1033
1109
|
|
|
1034
1110
|
## Built-in Routes
|
|
1035
1111
|
|
|
@@ -1243,14 +1319,14 @@ unready one.
|
|
|
1243
1319
|
|
|
1244
1320
|
## Adding Custom Modules
|
|
1245
1321
|
|
|
1246
|
-
To add a custom module, import it in `src/buildModules.mts` and add it to the array `buildModules` returns. The boot planner resolves dependencies by what modules `require` and `provide`, but it mounts routes and middleware in list order wherever no `before` / `after` says otherwise. So position matters for middleware that other routes depend on — the session store module, rule 1 of [Module Composition Order](#module-composition-order). A route under a prefix another module also uses, such as `/oauth`, needs no particular position (rule 2), provided it parses its own body and scopes its parsers to exactly its own paths: `
|
|
1322
|
+
To add a custom module, import it in `src/buildModules.mts` and add it to the array `buildModules` returns. The boot planner resolves dependencies by what modules `require` and `provide`, but it mounts routes and middleware in list order wherever no `before` / `after` says otherwise. So position matters for middleware that other routes depend on — the session store module, rule 1 of [Module Composition Order](#module-composition-order). A route under a prefix another module also uses, such as `/oauth`, needs no particular position (rule 2), provided it parses its own body and scopes its parsers to exactly its own paths: `oauthEndpointsModule`'s router parses only its own routes', and a parser that ran for every request under the prefix would read other modules' bodies for them. A module that mounts nothing of the first kind can go at the end:
|
|
1247
1323
|
|
|
1248
1324
|
```diff
|
|
1249
1325
|
// src/buildModules.mts
|
|
1250
1326
|
+import { myCustomModule } from "./my-custom-module.mjs";
|
|
1251
1327
|
…
|
|
1252
1328
|
return [
|
|
1253
|
-
|
|
1329
|
+
sessionStoreModule,
|
|
1254
1330
|
…
|
|
1255
1331
|
...(federationGrantsEnabled || mfaInstalled ? [subjectRevocationServiceModule] : []),
|
|
1256
1332
|
+ myCustomModule,
|
|
@@ -28,12 +28,11 @@
|
|
|
28
28
|
# `oauth-authorization.grants.clientCredentials.enabled = true` in their own
|
|
29
29
|
# layer.
|
|
30
30
|
#
|
|
31
|
-
# The
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# bound below it.
|
|
31
|
+
# The modules read these switches at boot, from the configuration boot parses;
|
|
32
|
+
# the template reads none of them before. This file writes the template's
|
|
33
|
+
# choices and binds each variable again: the env-override line is repeated
|
|
34
|
+
# after each value because a layer's own value wins over a variable bound
|
|
35
|
+
# below it.
|
|
37
36
|
#
|
|
38
37
|
# PKCE is mandatory for every authorization-code client and `S256` is the only
|
|
39
38
|
# method the provider accepts -- fixed policy (OAuth 2.1 §4.1.1 / RFC 9700
|
|
@@ -94,8 +93,9 @@ oauth {
|
|
|
94
93
|
# /authorize refuses any client not marked `firstParty: true` in
|
|
95
94
|
# clients.yaml — the template's example clients are marked. There is no
|
|
96
95
|
# opt-out: the one-time `allowUnmarkedClients` migration flag is removed,
|
|
97
|
-
# and a config or environment still setting it fails at boot
|
|
98
|
-
#
|
|
96
|
+
# and a config or environment still setting it fails at boot: the oauth
|
|
97
|
+
# module refuses the key as removed, and an exported
|
|
98
|
+
# OAUTH_AUTHORIZE_ALLOW_UNMARKED_CLIENTS as the variable of a removed key.
|
|
99
99
|
# POST /oauth/revoke revokes access tokens by writing their `jti` to the
|
|
100
100
|
# denylist wired below, and boot FAILS if that denylist is missing — an
|
|
101
101
|
# RFC 7009 endpoint must answer 200, so an unwired denylist would tell every
|
|
@@ -154,30 +154,20 @@ core.sessionRequirements.expected = []
|
|
|
154
154
|
# the recorded decision not to make that check) — each refused at boot when
|
|
155
155
|
# missing. Connections and the Redis grant store's key ring have no
|
|
156
156
|
# environment form; write them in a deployment-owned HOCON layer (README:
|
|
157
|
-
# Federation Grants). The
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
# The Redis grant store seals every credential; "required" needs its key ring,
|
|
171
|
-
# `redis-federation-grant-store.encryptionKeys`.
|
|
172
|
-
redis-federation-grant-store {
|
|
173
|
-
encryptionMode = "required"
|
|
174
|
-
encryptionMode = ${?REDIS_FEDERATION_GRANT_STORE_ENCRYPTION_MODE}
|
|
175
|
-
}
|
|
157
|
+
# Federation Grants). The section's defaults and its other variables
|
|
158
|
+
# (FEDERATION_GRANTS_IDENTITY_LOOKUP, FEDERATION_GRANTS_CONSENT_URL) are the
|
|
159
|
+
# federation-grants package's reference.conf, and the Redis grant store's
|
|
160
|
+
# (REDIS_FEDERATION_GRANT_STORE_ENCRYPTION_MODE, "required" by default) the
|
|
161
|
+
# Redis package's: each is layered only where a module of its package is
|
|
162
|
+
# installed. The
|
|
163
|
+
# template reads `federation-grants.enabled` before it chooses its modules,
|
|
164
|
+
# from its own files, which is why this file binds FEDERATION_GRANTS_ENABLED;
|
|
165
|
+
# unset, it writes nothing, and the feature is off.
|
|
166
|
+
federation-grants.enabled = ${?FEDERATION_GRANTS_ENABLED}
|
|
176
167
|
|
|
177
168
|
# The session store's section: the session cookie and the store
|
|
178
|
-
# express-session keeps sessions in.
|
|
179
|
-
#
|
|
180
|
-
# its own files, which is why this file binds SESSION_STORE_STORAGE_TYPE too.
|
|
169
|
+
# express-session keeps sessions in. A memory store declares itself
|
|
170
|
+
# replica-unsafe from this section.
|
|
181
171
|
session-store {
|
|
182
172
|
secret = ${?SESSION_STORE_SECRET}
|
|
183
173
|
# A `__Host-` name requires Secure, Path=/ and no Domain attribute. If
|
|
@@ -218,10 +208,11 @@ session {
|
|
|
218
208
|
# ignored. The federation flow has a separate list per provider (see
|
|
219
209
|
# `core.federations` below).
|
|
220
210
|
redirectAllowlist = []
|
|
221
|
-
#
|
|
222
|
-
# windowMs (milliseconds)
|
|
223
|
-
#
|
|
224
|
-
#
|
|
211
|
+
# POST /session/login's own attempt limit (brute-force protection), in
|
|
212
|
+
# windowMs (milliseconds), counted on the attempt counter
|
|
213
|
+
# (`adapters.attemptCounter`); kept in this layer so operators can tweak
|
|
214
|
+
# it. No rate limiter changes it. It does NOT govern OAuth endpoint rate
|
|
215
|
+
# limiting: the `rateLimiter` component
|
|
225
216
|
# (memoryRateLimiterModule or redisRateLimiterModule) reads its own section,
|
|
226
217
|
# `core-rate-limiter-memory` / `redis-rate-limiter` (with `windowSeconds`
|
|
227
218
|
# instead of `windowMs`). Switch via `adapters.rateLimiter = "memory" |
|
|
@@ -233,15 +224,28 @@ session {
|
|
|
233
224
|
}
|
|
234
225
|
}
|
|
235
226
|
|
|
227
|
+
# The MFA routes' budget, 60 per 300 s, is config/reference.conf's, given to
|
|
228
|
+
# both limiters so that switching `adapters.rateLimiter` keeps it. Change it
|
|
229
|
+
# for the limiter this deployment wires, in its own section:
|
|
230
|
+
#
|
|
231
|
+
# redis-rate-limiter.limits.mfa { limit = 30, windowSeconds = 300 }
|
|
232
|
+
|
|
236
233
|
# The federations, in core's own section, keyed by the name each is reached
|
|
237
234
|
# at (/session/oauth/federation/<name>). A key of one is bound to the
|
|
238
|
-
# variable named after its path, CORE_FEDERATIONS_<NAME>_<KEY>.
|
|
235
|
+
# variable named after its path, CORE_FEDERATIONS_<NAME>_<KEY>. Each entry
|
|
236
|
+
# names its `type`, and boot hands an enabled entry to the module that handles
|
|
237
|
+
# that type: the template loads the `google` and `oidc` types, so an entry of
|
|
238
|
+
# either type under any name is a federation of its own. An entry without a
|
|
239
|
+
# `type`, enabled or not, refuses the boot, and so does an enabled entry of a
|
|
240
|
+
# type no loaded module handles.
|
|
239
241
|
core.federations {
|
|
240
242
|
# Template ships a Google federation declaration disabled-by-default.
|
|
241
|
-
# Operators enable + populate credentials via env vars.
|
|
243
|
+
# Operators enable + populate credentials via env vars. A second Google
|
|
244
|
+
# client is another entry with `type = "google"` and its own callbackURL.
|
|
242
245
|
google {
|
|
243
246
|
enabled = false
|
|
244
247
|
enabled = ${?CORE_FEDERATIONS_GOOGLE_ENABLED}
|
|
248
|
+
type = "google"
|
|
245
249
|
clientId = ${?CORE_FEDERATIONS_GOOGLE_CLIENT_ID}
|
|
246
250
|
clientSecret = ${?CORE_FEDERATIONS_GOOGLE_CLIENT_SECRET}
|
|
247
251
|
callbackURL = "http://localhost:3000/session/oauth/federation/google/callback"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Defaults of the standalone template's own modules (src/modules.mts), each in
|
|
2
2
|
# the section named after its module, and of its own `adapters` and
|
|
3
|
-
# `mfaMode`, each key with its variable, named after its path
|
|
3
|
+
# `mfaMode`, each key with its variable, named after its path, and the MFA
|
|
4
|
+
# routes' budget in each rate limiter's section (below `mfaMode`). Layered
|
|
4
5
|
# beneath application.conf and {env}.conf: set a deployment's own values
|
|
5
6
|
# there, not here. A value either file sets wins over
|
|
6
7
|
# the variable bound here, except for HTTP_PORT, HTTP_TRUST_PROXY,
|
|
@@ -217,9 +218,16 @@ audit-sink {}
|
|
|
217
218
|
# replica, and `core.deployment.mode = "multi"` refuses each memory store by
|
|
218
219
|
# name. Each Redis store shares the one socket `redis-clients` opens.
|
|
219
220
|
adapters {
|
|
220
|
-
# The OAuth endpoints' and
|
|
221
|
+
# The rate-limit counters: the OAuth endpoints' and the MFA routes'.
|
|
221
222
|
rateLimiter = "memory"
|
|
222
223
|
rateLimiter = ${?ADAPTERS_RATE_LIMITER}
|
|
224
|
+
# The counter behind /session/login's own attempt limit
|
|
225
|
+
# (session.rateLimit.login), and device verification's
|
|
226
|
+
# (device-grant.rateLimit) when the device grant is added. `memory` wires
|
|
227
|
+
# none: each counts per process, which `core.deployment.mode = "multi"`
|
|
228
|
+
# refuses. `redis` needs a server whose maxmemory-policy is `noeviction`.
|
|
229
|
+
attemptCounter = "memory"
|
|
230
|
+
attemptCounter = ${?ADAPTERS_ATTEMPT_COUNTER}
|
|
223
231
|
# The four user-session stores and the subject-level revocation pair. They
|
|
224
232
|
# must be on Redis whenever express-session's store is, or a restart leaves
|
|
225
233
|
# every browser a session whose user session is gone.
|
|
@@ -294,6 +302,17 @@ adapters {
|
|
|
294
302
|
mfaMode = "required"
|
|
295
303
|
mfaMode = ${?MFA_MODE}
|
|
296
304
|
|
|
305
|
+
# The MFA routes' budget: the one setting this file writes in sections of
|
|
306
|
+
# modules other than the template's own. Every /session/mfa POST is limited
|
|
307
|
+
# per client address under the prefix `mfa` (`mfa:ip:<ip>`) by the wired
|
|
308
|
+
# limiter alone: the MFA package ships no budget of its own. Both limiters
|
|
309
|
+
# carry it, so switching `adapters.rateLimiter` keeps it; written here, beside
|
|
310
|
+
# each package's own defaults, the limiter not wired has it as a default
|
|
311
|
+
# rather than as a setting nothing reads. Without an entry the limiter's
|
|
312
|
+
# `defaultLimit` (60 per 60 s) applies. The MFA lock is separate and unchanged.
|
|
313
|
+
core-rate-limiter-memory.limits.mfa { limit = 60, windowSeconds = 300 }
|
|
314
|
+
redis-rate-limiter.limits.mfa { limit = 60, windowSeconds = 300 }
|
|
315
|
+
|
|
297
316
|
# The old paths of these sections refuse boot naming the new ones: `cors`,
|
|
298
317
|
# `oauth.jwt.signingKey`, `refreshTokenFamilyStore.redis`,
|
|
299
318
|
# `repositories.code` and `audit.sink`; the old adapter switches
|
|
@@ -142,8 +142,8 @@ services:
|
|
|
142
142
|
SESSION_STORE_STORAGE_TYPE: redis
|
|
143
143
|
SESSION_STORE_STORAGE_REDIS_URL: redis://redis:6379
|
|
144
144
|
# The UserSession stores: the provider's own record of who is signed in,
|
|
145
|
-
#
|
|
146
|
-
#
|
|
145
|
+
# the subject index and revocation boundaries beside it, and the session
|
|
146
|
+
# lifecycle records. They MUST be on Redis whenever the express-session
|
|
147
147
|
# store above is, and this line is the fix for what happens when they
|
|
148
148
|
# are not.
|
|
149
149
|
#
|
|
@@ -161,6 +161,9 @@ services:
|
|
|
161
161
|
# The memory rate limiter is per-process and evadable under bucket
|
|
162
162
|
# exhaustion (see README) — production points it at Redis.
|
|
163
163
|
ADAPTERS_RATE_LIMITER: redis
|
|
164
|
+
# The counter behind the login's own attempt limit: on Redis, a restart
|
|
165
|
+
# does not reset it. Redis must keep `maxmemory-policy noeviction`.
|
|
166
|
+
ADAPTERS_ATTEMPT_COUNTER: redis
|
|
164
167
|
# The federation token store is deliberately left at its `memory`
|
|
165
168
|
# default, and that is coherent here rather than an oversight: this
|
|
166
169
|
# template ships every federation disabled, so nothing writes to the
|