@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
|
@@ -1,121 +1,102 @@
|
|
|
1
1
|
# Application-level overrides for the standalone template.
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
3
|
+
# Defaults come from config/reference.conf (the template's own modules) and
|
|
4
|
+
# from the reference.conf of each package its modules come from, core's last.
|
|
5
|
+
# Values omitted here inherit from them; add overrides only for keys this
|
|
6
|
+
# deployment intentionally diverges on. Write a value for a key the last
|
|
7
|
+
# block of this file binds above that block: one written below it wins over
|
|
8
|
+
# the variable.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# HTTP_TRUST_PROXY=10.0.0.0/8 (or `loopback` for a sidecar) rather than
|
|
14
|
-
# `true`, which trusts a forwarded client address from anyone who can reach
|
|
15
|
-
# this process. See the header comment in the library's reference.conf.
|
|
16
|
-
trustProxy = false
|
|
17
|
-
trustProxy = ${?HTTP_TRUST_PROXY}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
# #287: where the audit trail goes. Before this block the template wired no
|
|
21
|
-
# sink at all, so every security event the routes emit -- token.issued.failure,
|
|
22
|
-
# authorize.rejected / authorize.granted, logout.cascade_failed, and the shared
|
|
23
|
-
# rate-limit guard's rate_limit.unavailable -- was discarded by the artifact
|
|
24
|
-
# operators deploy. Nothing failed and nothing warned: `emitAuditEvent` is a
|
|
25
|
-
# no-op when the slot is empty.
|
|
10
|
+
# Which adapter fills each slot -- the stores, the repositories, the audit
|
|
11
|
+
# sink -- is the template's own `adapters` section, whose defaults
|
|
12
|
+
# config/reference.conf holds with each one's variable (ADAPTERS_*). Override
|
|
13
|
+
# one in a layer of your own or with its variable:
|
|
26
14
|
#
|
|
27
|
-
#
|
|
28
|
-
# JSON to stdout, but "logger" writes it through the same pino stream that
|
|
29
|
-
# carries every other line this template emits, so a log aggregator ingests
|
|
30
|
-
# application logs and audit events with one parser and separates them on
|
|
31
|
-
# `name` ("provider" vs "audit"). "console" writes the bare event with no log
|
|
32
|
-
# envelope -- no level, no time -- which most pipelines then timestamp by
|
|
33
|
-
# ingestion. Set AUDIT_SINK_TYPE=console if that is what your pipeline wants.
|
|
15
|
+
# adapters.rateLimiter = "redis"
|
|
34
16
|
#
|
|
35
17
|
# The audit stream's level is FIXED at info and `logging.level` does not gate
|
|
36
|
-
# it
|
|
37
|
-
# legitimate one
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
#
|
|
18
|
+
# it: LOGGING_LEVEL=warn is an ordinary production setting and
|
|
19
|
+
# LOGGING_LEVEL=silent a legitimate one, and either silencing the audit trail
|
|
20
|
+
# would lose the record of every security event.
|
|
21
|
+
|
|
22
|
+
# The grant switches, each in the section of the module that installs the
|
|
23
|
+
# grant. The template is a typical web-app OP, so it turns on the universal
|
|
24
|
+
# user-auth path: the session grant (oauth-session) and the authorization-code
|
|
25
|
+
# and refresh-token grants (oauth-authorization). client_credentials and the
|
|
26
|
+
# jwt-bearer grant stay off; deployments that need M2M flows set
|
|
27
|
+
# OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED=true or write
|
|
28
|
+
# `oauth-authorization.grants.clientCredentials.enabled = true` in their own
|
|
29
|
+
# layer.
|
|
30
|
+
#
|
|
31
|
+
# The template reads these switches before it chooses its modules, from its
|
|
32
|
+
# own files, so this file writes every switch and binds its variable: one set
|
|
33
|
+
# only in the package's reference.conf would read as unset there, and a
|
|
34
|
+
# variable bound only there would install no grant. The env-override line is
|
|
35
|
+
# repeated after each value because a layer's own value wins over a variable
|
|
36
|
+
# bound below it.
|
|
41
37
|
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
# PKCE is mandatory for every authorization-code client and `S256` is the only
|
|
39
|
+
# method the provider accepts -- fixed policy (OAuth 2.1 §4.1.1 / RFC 9700
|
|
40
|
+
# §2.1.1), not a deployment setting. A legacy client that genuinely cannot
|
|
41
|
+
# compute SHA-256 is opted into `plain` one client at a time, with
|
|
42
|
+
# `allowPlainPkce: true` in clients.yaml.
|
|
43
|
+
oauth-session {
|
|
44
|
+
enabled = true
|
|
45
|
+
enabled = ${?OAUTH_SESSION_ENABLED}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
oauth-authorization.grants {
|
|
49
|
+
authorizationCode {
|
|
50
|
+
enabled = true
|
|
51
|
+
enabled = ${?OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED}
|
|
52
|
+
}
|
|
53
|
+
refreshToken {
|
|
54
|
+
enabled = true
|
|
55
|
+
enabled = ${?OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED}
|
|
56
|
+
}
|
|
57
|
+
clientCredentials {
|
|
58
|
+
enabled = false
|
|
59
|
+
enabled = ${?OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED}
|
|
60
|
+
}
|
|
61
|
+
jwtBearer {
|
|
62
|
+
enabled = false
|
|
63
|
+
enabled = ${?OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED}
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
oauth {
|
|
53
|
-
|
|
54
|
-
# Template is a typical web-app OP — enable the universal user-auth path.
|
|
55
|
-
# client_credentials remains off (reference.conf default); deployments that
|
|
56
|
-
# need M2M flows set OAUTH_GRANTS_CLIENT_CREDENTIALS_ENABLED=true or add
|
|
57
|
-
# `client_credentials { enabled = true }` in their own application.conf.
|
|
58
|
-
#
|
|
59
|
-
# The env-override line (`enabled = ${?OAUTH_GRANTS_*_ENABLED}`) is repeated
|
|
60
|
-
# at this layer because HOCON precedence: application.conf wins over
|
|
61
|
-
# reference.conf, so without the env-override line at the template layer,
|
|
62
|
-
# an operator setting OAUTH_GRANTS_SESSION_ENABLED=false (etc.) cannot
|
|
63
|
-
# disable the grant — the env var only takes effect at the layer where
|
|
64
|
-
# the substitution is written.
|
|
65
|
-
session {
|
|
66
|
-
enabled = true
|
|
67
|
-
enabled = ${?OAUTH_GRANTS_SESSION_ENABLED}
|
|
68
|
-
}
|
|
69
|
-
# #273: there is no `pkce` block here any more. PKCE is mandatory for
|
|
70
|
-
# every authorization-code client and `S256` is the only method the
|
|
71
|
-
# provider accepts — that is fixed policy (OAuth 2.1 §4.1.1 / RFC 9700
|
|
72
|
-
# §2.1.1), not a deployment setting, so `requireS256` / `required` /
|
|
73
|
-
# `defaultMethod` / `supportedMethods` are gone. A legacy client that
|
|
74
|
-
# genuinely cannot compute SHA-256 is opted into `plain` one client at a
|
|
75
|
-
# time, with `allowPlainPkce: true` in clients.yaml. The
|
|
76
|
-
# OAUTH_GRANTS_AUTHORIZATION_CODE_PKCE_REQUIRE_S256 tombstone in core's
|
|
77
|
-
# reference.conf makes a still-exported env var warn at boot.
|
|
78
|
-
authorization_code {
|
|
79
|
-
enabled = true
|
|
80
|
-
enabled = ${?OAUTH_GRANTS_AUTHORIZATION_CODE_ENABLED}
|
|
81
|
-
}
|
|
82
|
-
refresh_token {
|
|
83
|
-
enabled = true
|
|
84
|
-
enabled = ${?OAUTH_GRANTS_REFRESH_TOKEN_ENABLED}
|
|
85
|
-
}
|
|
86
|
-
# client_credentials: omitted -> inherits reference.conf default (enabled = false).
|
|
87
|
-
# Operators that need M2M flows set OAUTH_GRANTS_CLIENT_CREDENTIALS_ENABLED=true
|
|
88
|
-
# or add `client_credentials { enabled = true }` here.
|
|
89
|
-
}
|
|
90
|
-
# OR-9: adapter switch for the OAuth authorization-code repository. When
|
|
91
|
-
# set to "redis", the standalone composition root wires
|
|
92
|
-
# `redisCodeRepositoryModule` against the shared ioredis socket
|
|
93
|
-
# (`standaloneRedisClientsModule`). Memory-only deployments lose codes on
|
|
94
|
-
# restart and across replicas — multi-replica production MUST use "redis".
|
|
95
|
-
# Supersedes the legacy `repositories.code.type` switch (kept below as
|
|
96
|
-
# deprecated for backward compat — see CHANGELOG for the removal version).
|
|
97
|
-
# #481: the Authentication Context Class References this deployment can
|
|
68
|
+
# The Authentication Context Class References this deployment can
|
|
98
69
|
# vouch for, each mapped to the RFC 8176 `amr` values a session must carry
|
|
99
|
-
# (`pwd` for a password login, `
|
|
100
|
-
#
|
|
70
|
+
# (`pwd` for a password login, `fed` for a federated login) — or to a list
|
|
71
|
+
# of such lists, any one of which satisfies it. `/authorize` answers
|
|
101
72
|
# `acr_values` from this table alone — anything else is
|
|
102
73
|
# `unmet_authentication_requirements` — and discovery advertises the keys.
|
|
103
74
|
#
|
|
104
75
|
# authorize.acrValues {
|
|
105
76
|
# "urn:example:pwd" = ["pwd"]
|
|
106
|
-
# "urn:example:mfa" = ["pwd", "mfa"]
|
|
107
77
|
# }
|
|
108
78
|
#
|
|
109
|
-
#
|
|
79
|
+
# An entry nothing this deployment installs can satisfy is dropped at boot:
|
|
80
|
+
# not advertised, answered `unmet_authentication_requirements`, and logged
|
|
81
|
+
# once as `acr_value_unsatisfiable`. One that needs `fed` needs a
|
|
82
|
+
# federation installed; one that needs `mfa`, `hwk` or `swk` needs MFA
|
|
83
|
+
# installed (MFA_MODE, below), or a federation that trusts its upstream
|
|
84
|
+
# IdP's `amr`. One that lacks only a second factor while MFA is off is
|
|
85
|
+
# logged at info, not warn.
|
|
86
|
+
#
|
|
87
|
+
# While MFA_MODE installs MFA, the template adds `"urn:o3co:acr:mfa" =
|
|
88
|
+
# ["mfa"]`, met by a session with a verified second factor, unless this
|
|
89
|
+
# table writes that entry itself; with MFA off it adds nothing.
|
|
90
|
+
# Phishing-resistant authentication would be:
|
|
91
|
+
#
|
|
92
|
+
# "urn:o3co:acr:phr" = [["hwk"], ["swk"]]
|
|
93
|
+
|
|
94
|
+
# /authorize refuses any client not marked `firstParty: true` in
|
|
110
95
|
# clients.yaml — the template's example clients are marked. There is no
|
|
111
|
-
# opt-out: the one-time `allowUnmarkedClients` migration flag
|
|
112
|
-
#
|
|
96
|
+
# opt-out: the one-time `allowUnmarkedClients` migration flag is removed,
|
|
97
|
+
# and a config or environment still setting it fails at boot with
|
|
113
98
|
# migration instructions (via the tombstone in core's reference.conf).
|
|
114
|
-
|
|
115
|
-
adapter = "redis"
|
|
116
|
-
adapter = ${?OAUTH_CODE_ADAPTER}
|
|
117
|
-
}
|
|
118
|
-
# #277: POST /oauth/revoke revokes access tokens by writing their `jti` to the
|
|
99
|
+
# POST /oauth/revoke revokes access tokens by writing their `jti` to the
|
|
119
100
|
# denylist wired below, and boot FAILS if that denylist is missing — an
|
|
120
101
|
# RFC 7009 endpoint must answer 200, so an unwired denylist would tell every
|
|
121
102
|
# operator their token is revoked while it kept working until expiry.
|
|
@@ -131,45 +112,52 @@ oauth {
|
|
|
131
112
|
}
|
|
132
113
|
}
|
|
133
114
|
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
115
|
+
# The session requirements this deployment expects, in core's own section:
|
|
116
|
+
# the extensions that change what "logged in" means, each registered by a
|
|
117
|
+
# module (the MFA package's is named `mfa`). Boot compares the list with what
|
|
118
|
+
# the installed modules register: a name here that nothing registers refuses
|
|
119
|
+
# the boot (`session-requirement-missing`), and so does a registered
|
|
120
|
+
# requirement the list leaves out (`session-requirements-undeclared`). When
|
|
121
|
+
# MFA_MODE (`mfaMode`, config/reference.conf) installs MFA, the template
|
|
122
|
+
# adds `mfa` to this list and writes `secondFactorAuthority = "mfa"` beside
|
|
123
|
+
# it (one written here other than `mfa` refuses the boot); a deployment that
|
|
124
|
+
# adds a module registering a
|
|
125
|
+
# requirement of its own adds its name here. No environment variable sets the
|
|
126
|
+
# list.
|
|
127
|
+
core.sessionRequirements.expected = []
|
|
148
128
|
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
129
|
+
# Multi-factor authentication, installed by MFA_MODE (`mfaMode`,
|
|
130
|
+
# config/reference.conf): "off" (unset) installs nothing of it; "optional" or
|
|
131
|
+
# "required" installs the MFA package's modules (the TOTP, recovery-code and
|
|
132
|
+
# email factors, the operator reset) and writes `mfa.mode` from the switch.
|
|
133
|
+
# The MFA package's reference.conf holds the `mfa` section with its
|
|
134
|
+
# variables: the key ring's first key, MFA_ENCRYPTION_KEY (`openssl rand
|
|
135
|
+
# -base64 32`; none, and the boot is refused), and the page a step-up starts
|
|
136
|
+
# on, MFA_PAGE_URL (`/mfa`). One Store call's time, MFA_STORE_TIMEOUT_MS
|
|
137
|
+
# (5000 ms), must be at least every store's own per-call timeout, the user
|
|
138
|
+
# directory's REPOSITORIES_USER_HTTP_TIMEOUT among them: raise the two
|
|
139
|
+
# together; above 37500 ms the boot is refused. Mail --
|
|
140
|
+
# the account-email proof, the email factor's codes -- goes through the mail
|
|
141
|
+
# sender, which needs STANDARD_SMTP_MAIL_SENDER_HOST and
|
|
142
|
+
# STANDARD_SMTP_MAIL_SENDER_FROM outside development. While MFA is off,
|
|
143
|
+
# nothing written under `mfa` is read.
|
|
161
144
|
|
|
162
|
-
#
|
|
145
|
+
# Federation grants — a user's standing consent that a backend or an
|
|
163
146
|
# agent may obtain upstream IdP tokens with no session behind the call. Off by
|
|
164
147
|
# default; FEDERATION_GRANTS_ENABLED=true installs the routes, the two stores
|
|
165
|
-
#
|
|
148
|
+
# `adapters.federationGrantStore` and `.federationGrantIntentStore` select
|
|
149
|
+
# (none by default, which boot refuses naming the store) and the
|
|
150
|
+
# subject-revocation service. Enabling it is a statement that
|
|
166
151
|
# the deployment has a consent page (FEDERATION_GRANTS_CONSENT_URL, no default),
|
|
167
152
|
# a `callbackURL` on every connection, and a user repository that covers each
|
|
168
153
|
# connection's registration (or FEDERATION_GRANTS_IDENTITY_LOOKUP=unsupported,
|
|
169
154
|
# the recorded decision not to make that check) — each refused at boot when
|
|
170
|
-
# missing. Connections and the
|
|
171
|
-
# write them in a deployment-owned HOCON layer (README:
|
|
172
|
-
|
|
155
|
+
# missing. Connections and the Redis grant store's key ring have no
|
|
156
|
+
# environment form; write them in a deployment-owned HOCON layer (README:
|
|
157
|
+
# Federation Grants). The template reads `federation-grants.enabled` before it
|
|
158
|
+
# chooses its modules, from its own files, which is why this file binds
|
|
159
|
+
# FEDERATION_GRANTS_ENABLED too.
|
|
160
|
+
federation-grants {
|
|
173
161
|
enabled = false
|
|
174
162
|
enabled = ${?FEDERATION_GRANTS_ENABLED}
|
|
175
163
|
identityLookup = "required"
|
|
@@ -177,103 +165,87 @@ federationGrants {
|
|
|
177
165
|
consent {
|
|
178
166
|
url = ${?FEDERATION_GRANTS_CONSENT_URL}
|
|
179
167
|
}
|
|
180
|
-
encryptionMode = "required"
|
|
181
|
-
encryptionMode = ${?FEDERATION_GRANTS_ENCRYPTION_MODE}
|
|
182
168
|
}
|
|
183
169
|
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
# what ends them. Single-instance local work can set both to "memory".
|
|
190
|
-
federationGrantStore {
|
|
191
|
-
adapter = "redis"
|
|
192
|
-
adapter = ${?FEDERATION_GRANT_STORE_ADAPTER}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
federationGrantIntentStore {
|
|
196
|
-
adapter = "redis"
|
|
197
|
-
adapter = ${?FEDERATION_GRANT_INTENT_STORE_ADAPTER}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
# #527: where consent to a client that is not first-party is recorded.
|
|
201
|
-
# "none" (the default) wires no store and such clients are refused at
|
|
202
|
-
# /authorize; "memory" is single-replica only (refused under
|
|
203
|
-
# DEPLOYMENT_MODE=multi by name); "redis" (#561) keeps the consent records and
|
|
204
|
-
# the requests parked while the consent page asks on the shared ioredis socket,
|
|
205
|
-
# and is what a deployment of more than one replica selects. Its key namespace
|
|
206
|
-
# is REDIS_CONSENT_STORE_KEY_PREFIX (default "consent:").
|
|
207
|
-
consentStore {
|
|
208
|
-
adapter = "none"
|
|
209
|
-
adapter = ${?CONSENT_STORE_ADAPTER}
|
|
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}
|
|
210
175
|
}
|
|
211
176
|
|
|
212
|
-
session
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
177
|
+
# The session store's section: the session cookie and the store
|
|
178
|
+
# express-session keeps sessions in. The template reads `storage` before it
|
|
179
|
+
# chooses its modules (a memory store declares itself replica-unsafe), from
|
|
180
|
+
# its own files, which is why this file binds SESSION_STORE_STORAGE_TYPE too.
|
|
181
|
+
session-store {
|
|
182
|
+
secret = ${?SESSION_STORE_SECRET}
|
|
183
|
+
# A `__Host-` name requires Secure, Path=/ and no Domain attribute. If
|
|
184
|
+
# SESSION_STORE_SECURE=false or SESSION_STORE_DOMAIN is set, also set
|
|
185
|
+
# SESSION_STORE_NAME to a name without the prefix.
|
|
217
186
|
name = "__Host-auth.session"
|
|
218
|
-
name = ${?
|
|
187
|
+
name = ${?SESSION_STORE_NAME}
|
|
219
188
|
maxAge = 3600000
|
|
220
|
-
maxAge = ${?
|
|
189
|
+
maxAge = ${?SESSION_STORE_MAX_AGE}
|
|
221
190
|
secure = true
|
|
222
|
-
secure = ${?
|
|
191
|
+
secure = ${?SESSION_STORE_SECURE}
|
|
223
192
|
sameSite = "lax"
|
|
224
|
-
sameSite = ${?
|
|
193
|
+
sameSite = ${?SESSION_STORE_SAME_SITE}
|
|
225
194
|
domain = null
|
|
226
|
-
domain = ${?
|
|
227
|
-
# #405: every URL `POST /session/login` may accept as `redirect_to`, matched
|
|
228
|
-
# EXACTLY -- scheme, host, port, path and query all significant. Empty (the
|
|
229
|
-
# default) refuses every `redirect_to`, so leave it alone unless your login
|
|
230
|
-
# page actually posts one.
|
|
231
|
-
#
|
|
232
|
-
# redirectAllowlist = ["https://app.example.com/welcome"]
|
|
233
|
-
#
|
|
234
|
-
# `http://` is accepted for loopback hosts only. With `domain` set above, an
|
|
235
|
-
# entry outside it fails boot rather than being silently ignored. The
|
|
236
|
-
# federation flow has a separate list per provider (see `federations` below).
|
|
237
|
-
redirectAllowlist = []
|
|
195
|
+
domain = ${?SESSION_STORE_DOMAIN}
|
|
238
196
|
storage {
|
|
239
197
|
type = "redis"
|
|
240
|
-
type = ${?
|
|
198
|
+
type = ${?SESSION_STORE_STORAGE_TYPE}
|
|
241
199
|
redis {
|
|
242
200
|
url = "redis://localhost:6379"
|
|
243
|
-
url = ${?
|
|
244
|
-
password = ${?
|
|
201
|
+
url = ${?SESSION_STORE_STORAGE_REDIS_URL}
|
|
202
|
+
password = ${?SESSION_STORE_STORAGE_REDIS_PASSWORD}
|
|
245
203
|
}
|
|
246
204
|
}
|
|
247
205
|
}
|
|
248
206
|
|
|
249
|
-
#
|
|
250
|
-
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
-
# `
|
|
259
|
-
#
|
|
260
|
-
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
#
|
|
264
|
-
#
|
|
207
|
+
# The session module's section: the login routes' settings.
|
|
208
|
+
session {
|
|
209
|
+
# Every URL `POST /session/login` may accept as `redirect_to`, matched
|
|
210
|
+
# EXACTLY -- scheme, host, port, path and query all significant. Empty (the
|
|
211
|
+
# default) refuses every `redirect_to`, so leave it alone unless your login
|
|
212
|
+
# page actually posts one.
|
|
213
|
+
#
|
|
214
|
+
# redirectAllowlist = ["https://app.example.com/welcome"]
|
|
215
|
+
#
|
|
216
|
+
# `http://` is accepted for loopback hosts only. With `session-store.domain`
|
|
217
|
+
# set above, an entry outside it fails boot rather than being silently
|
|
218
|
+
# ignored. The federation flow has a separate list per provider (see
|
|
219
|
+
# `core.federations` below).
|
|
220
|
+
redirectAllowlist = []
|
|
221
|
+
# Rate limiting for POST /session/login (brute-force protection), in
|
|
222
|
+
# windowMs (milliseconds); the session module contributes it as the `login`
|
|
223
|
+
# budget, kept in this layer so operators can tweak it. It does NOT govern
|
|
224
|
+
# OAuth endpoint rate limiting: the `rateLimiter` component
|
|
225
|
+
# (memoryRateLimiterModule or redisRateLimiterModule) reads its own section,
|
|
226
|
+
# `core-rate-limiter-memory` / `redis-rate-limiter` (with `windowSeconds`
|
|
227
|
+
# instead of `windowMs`). Switch via `adapters.rateLimiter = "memory" |
|
|
228
|
+
# "redis"` (see reference.conf). The Redis limiter's outage policy is its
|
|
229
|
+
# own key, `redis-rate-limiter.failMode` ("closed" by default;
|
|
230
|
+
# REDIS_RATE_LIMITER_FAIL_MODE sets it).
|
|
231
|
+
rateLimit {
|
|
232
|
+
login { windowMs = 900000, limit = 20 }
|
|
233
|
+
}
|
|
265
234
|
}
|
|
266
235
|
|
|
267
|
-
federations
|
|
236
|
+
# The federations, in core's own section, keyed by the name each is reached
|
|
237
|
+
# at (/session/oauth/federation/<name>). A key of one is bound to the
|
|
238
|
+
# variable named after its path, CORE_FEDERATIONS_<NAME>_<KEY>.
|
|
239
|
+
core.federations {
|
|
268
240
|
# Template ships a Google federation declaration disabled-by-default.
|
|
269
241
|
# Operators enable + populate credentials via env vars.
|
|
270
242
|
google {
|
|
271
243
|
enabled = false
|
|
272
|
-
enabled = ${?
|
|
273
|
-
clientId = ${?
|
|
274
|
-
clientSecret = ${?
|
|
244
|
+
enabled = ${?CORE_FEDERATIONS_GOOGLE_ENABLED}
|
|
245
|
+
clientId = ${?CORE_FEDERATIONS_GOOGLE_CLIENT_ID}
|
|
246
|
+
clientSecret = ${?CORE_FEDERATIONS_GOOGLE_CLIENT_SECRET}
|
|
275
247
|
callbackURL = "http://localhost:3000/session/oauth/federation/google/callback"
|
|
276
|
-
callbackURL = ${?
|
|
248
|
+
callbackURL = ${?CORE_FEDERATIONS_GOOGLE_CALLBACK_URL}
|
|
277
249
|
|
|
278
250
|
# Every URL a `redirect_to` query parameter may name on
|
|
279
251
|
# /session/oauth/federation/google. Matching is EXACT — scheme, host, port,
|
|
@@ -282,8 +254,8 @@ federations {
|
|
|
282
254
|
#
|
|
283
255
|
# An empty list refuses every `redirect_to`, which is the right setting for
|
|
284
256
|
# a deployment that does not use the parameter. It is NOT a way to allow
|
|
285
|
-
# everything:
|
|
286
|
-
#
|
|
257
|
+
# everything: nothing falls back to accepting any http(s) URL, which would
|
|
258
|
+
# make this an open redirect.
|
|
287
259
|
#
|
|
288
260
|
# https is required, except for a loopback host (localhost, 127.0.0.0/8,
|
|
289
261
|
# [::1]) — that carve-out is what lets a local dev front-end and a native
|
|
@@ -299,20 +271,20 @@ federations {
|
|
|
299
271
|
# Cookie / session domain. When set, every non-loopback redirectAllowlist
|
|
300
272
|
# entry must be inside it, checked at boot — an entry outside it fails
|
|
301
273
|
# startup rather than sitting here looking effective.
|
|
302
|
-
# sessionDomain = ${?
|
|
274
|
+
# sessionDomain = ${?CORE_FEDERATIONS_GOOGLE_SESSION_DOMAIN}
|
|
303
275
|
|
|
304
276
|
# Post-callback bridge page. Required only if `redirect_to` is used at all:
|
|
305
277
|
# the browser lands here as `<authCallbackUrl>?redirect_to=<encoded>`.
|
|
306
|
-
# authCallbackUrl = ${?
|
|
278
|
+
# authCallbackUrl = ${?CORE_FEDERATIONS_GOOGLE_AUTH_CALLBACK_URL}
|
|
307
279
|
|
|
308
280
|
# Where the browser goes after a callback carrying no `redirect_to`.
|
|
309
|
-
# clientUrl = ${?
|
|
281
|
+
# clientUrl = ${?CORE_FEDERATIONS_GOOGLE_CLIENT_URL}
|
|
310
282
|
|
|
311
283
|
# RFC 9207: Google always returns `iss` on the callback, so a callback
|
|
312
284
|
# without one is refused. `false` is the way out if Google ever stops
|
|
313
285
|
# sending it. It permits a missing `iss` only — one that is sent and is not
|
|
314
286
|
# Google's is refused either way.
|
|
315
|
-
# requireAuthorizationResponseIss = ${?
|
|
287
|
+
# requireAuthorizationResponseIss = ${?CORE_FEDERATIONS_GOOGLE_REQUIRE_AUTHORIZATION_RESPONSE_ISS}
|
|
316
288
|
|
|
317
289
|
# Whether sign-in asks Google for a refresh token. Unset is "offline":
|
|
318
290
|
# every sign-in shows Google's consent screen, because Google issues a
|
|
@@ -320,11 +292,11 @@ federations {
|
|
|
320
292
|
# POST /oauth/federation/google/token can refresh with. "online" shows the
|
|
321
293
|
# consent screen on the first sign-in only and gets no refresh token at
|
|
322
294
|
# all — for a deployment that uses Google to sign in and nothing more.
|
|
323
|
-
accessType = ${?
|
|
295
|
+
accessType = ${?CORE_FEDERATIONS_GOOGLE_ACCESS_TYPE}
|
|
324
296
|
}
|
|
325
297
|
|
|
326
298
|
# Any OpenID Connect provider (Okta, Entra ID, Auth0, Keycloak, your own
|
|
327
|
-
# tenant) — `@o3co/auth-provider-federation-oidc
|
|
299
|
+
# tenant) — `@o3co/auth-provider-federation-oidc`. Disabled by
|
|
328
300
|
# default; the env vars below switch this one instance on. The section
|
|
329
301
|
# name is the federation name: the browser starts at
|
|
330
302
|
# /session/oauth/federation/oidc and the identity handed to the Store is
|
|
@@ -333,19 +305,19 @@ federations {
|
|
|
333
305
|
# another name (`okta { type = "oidc" ... }`) with its own callbackURL.
|
|
334
306
|
oidc {
|
|
335
307
|
enabled = false
|
|
336
|
-
enabled = ${?
|
|
308
|
+
enabled = ${?CORE_FEDERATIONS_OIDC_ENABLED}
|
|
337
309
|
type = "oidc"
|
|
338
310
|
|
|
339
311
|
# Issuer identifier, exactly as the IdP writes it into `iss`. Discovery
|
|
340
312
|
# runs at boot from <issuer>/.well-known/openid-configuration and a
|
|
341
313
|
# failure refuses boot. https only (plain http on localhost for dev).
|
|
342
|
-
issuer = ${?
|
|
343
|
-
clientId = ${?
|
|
314
|
+
issuer = ${?CORE_FEDERATIONS_OIDC_ISSUER}
|
|
315
|
+
clientId = ${?CORE_FEDERATIONS_OIDC_CLIENT_ID}
|
|
344
316
|
# client_secret_basic. For private_key_jwt drop clientSecret and set
|
|
345
317
|
# privateKey (a PEM PKCS#8 key, or { pem, kid, alg }) instead.
|
|
346
|
-
clientSecret = ${?
|
|
318
|
+
clientSecret = ${?CORE_FEDERATIONS_OIDC_CLIENT_SECRET}
|
|
347
319
|
callbackURL = "http://localhost:3000/session/oauth/federation/oidc/callback"
|
|
348
|
-
callbackURL = ${?
|
|
320
|
+
callbackURL = ${?CORE_FEDERATIONS_OIDC_CALLBACK_URL}
|
|
349
321
|
scopes = ["openid", "profile", "email"]
|
|
350
322
|
|
|
351
323
|
# Same rules as the Google block above: exact URLs, https except on a
|
|
@@ -360,132 +332,15 @@ federations {
|
|
|
360
332
|
}
|
|
361
333
|
}
|
|
362
334
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
user {
|
|
373
|
-
type = "http"
|
|
374
|
-
type = ${?CLIENT_USER_TYPE}
|
|
375
|
-
yaml {
|
|
376
|
-
path = "./config/users.yaml"
|
|
377
|
-
}
|
|
378
|
-
http {
|
|
379
|
-
# Both URLs carry plaintext user credentials, so both must be absolute
|
|
380
|
-
# https URLs. http:// is accepted only for a loopback host (localhost,
|
|
381
|
-
# 127.0.0.0/8, [::1]); a private-range address or a container-network
|
|
382
|
-
# service name still needs https. Boot fails if either is not. Each must
|
|
383
|
-
# also be the endpoint that answers: no request follows a 3xx, so a URL
|
|
384
|
-
# that redirects fails every call.
|
|
385
|
-
authenticateUrl = ${?CLIENT_USER_AUTHENTICATE_URL}
|
|
386
|
-
authenticateByTokenUrl = ${?CLIENT_USER_AUTHENTICATE_BY_TOKEN_URL}
|
|
387
|
-
# #482: optional. When set, a signed-in user can link a second federated
|
|
388
|
-
# identity (GET /session/oauth/federation/<name>?link=1): the Store
|
|
389
|
-
# receives { userId, provider, sub, token, claims } and decides — never on
|
|
390
|
-
# an unverified or relay e-mail. Same https and no-redirect rule as the two URLs above.
|
|
391
|
-
# Also declared in core's reference.conf, so a composition that does not
|
|
392
|
-
# layer this file gets the same override.
|
|
393
|
-
# Without it, link=1 is refused with link_unsupported.
|
|
394
|
-
linkFederatedIdentityUrl = ${?CLIENT_USER_LINK_FEDERATED_IDENTITY_URL}
|
|
395
|
-
# #613: optional. The Store's identity lookup for federation grants —
|
|
396
|
-
# what lets this deployment keep FEDERATION_GRANTS_IDENTITY_LOOKUP=required
|
|
397
|
-
# with a connection configured. Same https and no-redirect rule. Declare what the Store
|
|
398
|
-
# covers, per registration, in a HOCON layer (README: Federation Grants):
|
|
399
|
-
# federatedIdentityLookupCoverage = [
|
|
400
|
-
# { provider = "entra-files", issuer = "...", clientId = "...", requiredClaims = ["tid", "oid"] }
|
|
401
|
-
# ]
|
|
402
|
-
findSubjectByFederatedIdentityUrl = ${?CLIENT_USER_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL}
|
|
403
|
-
# Optional. Sent on every Store request as `Authorization: Bearer <token>`
|
|
404
|
-
# so the Store can refuse any caller but this deployment; unset, none is
|
|
405
|
-
# sent. A bare token (no "Bearer " prefix), at least 32 bytes of key
|
|
406
|
-
# material -- `openssl rand -hex 32`. Boot fails on a weak, malformed or
|
|
407
|
-
# blank value. One token goes to every URL above: one trust domain. A
|
|
408
|
-
# Store that refuses it with 401 (or 403) + `WWW-Authenticate: Bearer`
|
|
409
|
-
# makes every Store call an outage rather than a wrong password: 503 and
|
|
410
|
-
# a logged error at login, federation callbacks and the jwt-bearer
|
|
411
|
-
# grant; a temporarily_unavailable redirect and
|
|
412
|
-
# federation_grant_callback_unavailable (err.name
|
|
413
|
-
# StoreCredentialRefusedError) at the federation-grants callback. Also
|
|
414
|
-
# declared in core's reference.conf.
|
|
415
|
-
bearerToken = ${?CLIENT_USER_BEARER_TOKEN}
|
|
416
|
-
# Positive integer, milliseconds, <= 2147483647. Boot fails otherwise —
|
|
417
|
-
# including on a blank env override, which HOCON substitutes as "".
|
|
418
|
-
timeout = 5000
|
|
419
|
-
timeout = ${?CLIENT_USER_TIMEOUT}
|
|
420
|
-
# Largest upstream response body accepted, in bytes. Positive integer.
|
|
421
|
-
maxResponseBytes = 1048576
|
|
422
|
-
maxResponseBytes = ${?CLIENT_USER_MAX_RESPONSE_BYTES}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
code {
|
|
426
|
-
type = "redis"
|
|
427
|
-
type = ${?CLIENT_CODE_TYPE}
|
|
428
|
-
memory {
|
|
429
|
-
defaultExpiresIn = 600
|
|
430
|
-
defaultExpiresIn = ${?CLIENT_CODE_DEFAULT_EXPIRES_IN}
|
|
431
|
-
}
|
|
432
|
-
redis {
|
|
433
|
-
defaultExpiresIn = 600
|
|
434
|
-
defaultExpiresIn = ${?CLIENT_CODE_DEFAULT_EXPIRES_IN}
|
|
435
|
-
endpointUri = ${?CLIENT_CODE_ENDPOINT_URI}
|
|
436
|
-
password = ${?CLIENT_CODE_PASSWORD}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
endpoints {
|
|
442
|
-
login {
|
|
443
|
-
url = "/login"
|
|
444
|
-
url = ${?ENDPOINTS_LOGIN_URL}
|
|
445
|
-
}
|
|
446
|
-
# #527: the consent page a client that is not first-party is routed
|
|
447
|
-
# through (`?challenge=<id>`). Deployment-owned, like the login page: it
|
|
448
|
-
# reads GET /oauth/consent and posts to POST /oauth/consent.
|
|
449
|
-
consent {
|
|
450
|
-
url = "/consent"
|
|
451
|
-
url = ${?ENDPOINTS_CONSENT_URL}
|
|
452
|
-
}
|
|
453
|
-
# IH-10: `client { url }` and `authCallback { url }` removed — no
|
|
454
|
-
# production consumer reads them.
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
# #500: browser origins allowed to read the token, userinfo, revocation and
|
|
458
|
-
# discovery/JWKS responses. Empty (the default) means CORS is off -- leave it
|
|
459
|
-
# alone unless a browser app is served from a different origin than this
|
|
460
|
-
# provider. See the library reference.conf for the full rules; the short
|
|
461
|
-
# version is that matching is EXACT, so each entry is a bare origin with no
|
|
462
|
-
# trailing slash, no path and no wildcard, and https is required off loopback.
|
|
463
|
-
#
|
|
464
|
-
# allowedOrigins = ["https://app.example.com", "http://localhost:5173"]
|
|
465
|
-
#
|
|
466
|
-
# The env-override line is repeated at this layer for the HOCON precedence
|
|
467
|
-
# reason the oauth.grants block above explains: application.conf wins over
|
|
468
|
-
# reference.conf, so without it CORS_ALLOWED_ORIGINS could not take effect.
|
|
469
|
-
# It takes a comma-separated list.
|
|
470
|
-
#
|
|
471
|
-
# Not to be confused with `session.csrf.trustedOrigins`, which answers the
|
|
472
|
-
# other question (#272).
|
|
473
|
-
cors {
|
|
474
|
-
allowedOrigins = []
|
|
475
|
-
allowedOrigins = ${?CORS_ALLOWED_ORIGINS}
|
|
335
|
+
# The variables that set keys of the template's own modules, bound here, last,
|
|
336
|
+
# so each wins over a value this file sets above it. Keep this block last.
|
|
337
|
+
# config/reference.conf holds the defaults and binds the same variables.
|
|
338
|
+
http {
|
|
339
|
+
port = ${?HTTP_PORT}
|
|
340
|
+
trustProxy = ${?HTTP_TRUST_PROXY}
|
|
341
|
+
cors.allowedOrigins = ${?HTTP_CORS_ALLOWED_ORIGINS}
|
|
476
342
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
# One ioredis socket per replica backs ALL Redis-typed clients (RT family
|
|
481
|
-
# + 4 user-session stores + rate limiter). Multi-replica deployments MUST
|
|
482
|
-
# point this at a shared Redis 7.2+ instance — without it, each replica
|
|
483
|
-
# stores RT families in its own ioredis connection against a local-only
|
|
484
|
-
# Redis, defeating the cross-replica persistence purpose.
|
|
485
|
-
refreshTokenFamilyStore {
|
|
486
|
-
redis {
|
|
487
|
-
url = "redis://localhost:6379"
|
|
488
|
-
url = ${?REFRESH_TOKEN_FAMILY_STORE_REDIS_URL}
|
|
489
|
-
password = ${?REFRESH_TOKEN_FAMILY_STORE_REDIS_PASSWORD}
|
|
490
|
-
}
|
|
343
|
+
redis-clients {
|
|
344
|
+
url = ${?REDIS_CLIENTS_URL}
|
|
345
|
+
password = ${?REDIS_CLIENTS_PASSWORD}
|
|
491
346
|
}
|