@o3co/create-auth-provider 0.9.0 → 0.11.0
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/dist/index.d.mts +20 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +110 -7
- package/package.json +4 -4
- package/templates/standalone/.env.example +49 -5
- package/templates/standalone/Dockerfile +59 -16
- package/templates/standalone/README.ja.md +92 -13
- package/templates/standalone/README.md +358 -18
- package/templates/standalone/config/application.conf +132 -4
- package/templates/standalone/config/clients.yaml.example +56 -2
- package/templates/standalone/config/production.conf +9 -3
- package/templates/standalone/docker-compose.production.yml +132 -0
- package/templates/standalone/docker-compose.yml +7 -0
- package/templates/standalone/gitignore +21 -0
- package/templates/standalone/package.json +15 -20
- package/templates/standalone/src/__tests__/audit-sink.test.mts +343 -0
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +423 -0
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +114 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +231 -0
- package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +76 -1
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +117 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +244 -0
- package/templates/standalone/src/__tests__/smoke.test.mts +213 -11
- package/templates/standalone/src/__tests__/terminal-error.test.mts +96 -0
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +117 -7
- package/templates/standalone/src/app.mts +78 -10
- package/templates/standalone/src/buildModules.mts +43 -1
- package/templates/standalone/src/configPath.mts +2 -2
- package/templates/standalone/src/logger.mts +90 -2
- package/templates/standalone/src/metrics.mts +190 -0
- package/templates/standalone/src/modules.mts +319 -33
- package/templates/standalone/src/shutdown.mts +162 -0
- package/templates/standalone/src/terminalError.mts +60 -0
- package/templates/standalone/tests/index.test.js +69 -5
- package/templates/standalone/tsconfig.json +9 -1
- package/templates/standalone/vitest.config.mts +8 -0
- package/templates/versions.json +12 -11
|
@@ -9,10 +9,46 @@
|
|
|
9
9
|
http {
|
|
10
10
|
port = 3000
|
|
11
11
|
port = ${?HTTP_PORT}
|
|
12
|
+
# false | true | <hop count> | <address list>. Prefer naming the proxy:
|
|
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.
|
|
12
16
|
trustProxy = false
|
|
13
17
|
trustProxy = ${?HTTP_TRUST_PROXY}
|
|
14
18
|
}
|
|
15
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.
|
|
26
|
+
#
|
|
27
|
+
# "logger" rather than reference.conf's "console": both write newline-delimited
|
|
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.
|
|
34
|
+
#
|
|
35
|
+
# The audit stream's level is FIXED at info and `logging.level` does not gate
|
|
36
|
+
# it. LOG_LEVEL=warn is an ordinary production setting and LOG_LEVEL=silent a
|
|
37
|
+
# legitimate one; either silencing the audit trail would be #287 again, reached
|
|
38
|
+
# from the operator's side. Where events go is chosen here, and this selector
|
|
39
|
+
# has no "none" -- an unknown type fails boot naming the sinks that exist,
|
|
40
|
+
# rather than producing a deployment with no audit trail (#304).
|
|
41
|
+
#
|
|
42
|
+
# To point this at a real sink (SIEM, log pipeline, message bus): register the
|
|
43
|
+
# builder in `auditSinkModule` (src/modules.mts), name it here, and put its
|
|
44
|
+
# options in this block -- `sink { type = "splunk-hec", splunk-hec { ... } }`.
|
|
45
|
+
audit {
|
|
46
|
+
sink {
|
|
47
|
+
type = "logger"
|
|
48
|
+
type = ${?AUDIT_SINK_TYPE}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
16
52
|
oauth {
|
|
17
53
|
grants {
|
|
18
54
|
# Template is a typical web-app OP — enable the universal user-auth path.
|
|
@@ -30,13 +66,18 @@ oauth {
|
|
|
30
66
|
enabled = true
|
|
31
67
|
enabled = ${?OAUTH_GRANTS_SESSION_ENABLED}
|
|
32
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.
|
|
33
78
|
authorization_code {
|
|
34
79
|
enabled = true
|
|
35
80
|
enabled = ${?OAUTH_GRANTS_AUTHORIZATION_CODE_ENABLED}
|
|
36
|
-
pkce {
|
|
37
|
-
requireS256 = false
|
|
38
|
-
requireS256 = ${?OAUTH_GRANTS_AUTHORIZATION_CODE_PKCE_REQUIRE_S256}
|
|
39
|
-
}
|
|
40
81
|
}
|
|
41
82
|
refresh_token {
|
|
42
83
|
enabled = true
|
|
@@ -53,10 +94,44 @@ oauth {
|
|
|
53
94
|
# restart and across replicas — multi-replica production MUST use "redis".
|
|
54
95
|
# Supersedes the legacy `repositories.code.type` switch (kept below as
|
|
55
96
|
# deprecated for backward compat — see CHANGELOG for the removal version).
|
|
97
|
+
# #267: /authorize refuses any client not marked `firstParty: true` in
|
|
98
|
+
# clients.yaml — the template's example clients are marked. There is no
|
|
99
|
+
# opt-out: the one-time `allowUnmarkedClients` migration flag was removed
|
|
100
|
+
# (#330), and a config or environment still setting it fails at boot with
|
|
101
|
+
# migration instructions (via the tombstone in core's reference.conf).
|
|
56
102
|
code {
|
|
57
103
|
adapter = "redis"
|
|
58
104
|
adapter = ${?OAUTH_CODE_ADAPTER}
|
|
59
105
|
}
|
|
106
|
+
# #277: POST /oauth/revoke revokes access tokens by writing their `jti` to the
|
|
107
|
+
# denylist wired below, and boot FAILS if that denylist is missing — an
|
|
108
|
+
# RFC 7009 endpoint must answer 200, so an unwired denylist would tell every
|
|
109
|
+
# operator their token is revoked while it kept working until expiry.
|
|
110
|
+
#
|
|
111
|
+
# Deployments that genuinely do not revoke access tokens (short-lived tokens
|
|
112
|
+
# plus refresh-token revocation only) set "unsupported" instead, and the
|
|
113
|
+
# endpoint then answers `unsupported_token_type` for
|
|
114
|
+
# `token_type_hint = access_token`. Refresh-token revocation works in both
|
|
115
|
+
# modes and needs no denylist.
|
|
116
|
+
revocation {
|
|
117
|
+
accessToken = "denylist"
|
|
118
|
+
accessToken = ${?OAUTH_REVOCATION_ACCESS_TOKEN}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# #277: backend for the access-token denylist.
|
|
123
|
+
#
|
|
124
|
+
# "redis" by default, not "memory": the memory adapter keeps revocations in one
|
|
125
|
+
# process, so a token revoked on one replica stays valid on every other — and
|
|
126
|
+
# `deployment.mode = "multi"` refuses it by name for exactly that reason. It
|
|
127
|
+
# shares the ioredis socket configured under `refreshTokenFamilyStore.redis`
|
|
128
|
+
# below, so switching it on costs no extra connection.
|
|
129
|
+
#
|
|
130
|
+
# Single-instance local development can set ACCESS_TOKEN_DENYLIST_ADAPTER=memory
|
|
131
|
+
# to run without Redis.
|
|
132
|
+
accessTokenDenylist {
|
|
133
|
+
adapter = "redis"
|
|
134
|
+
adapter = ${?ACCESS_TOKEN_DENYLIST_ADAPTER}
|
|
60
135
|
}
|
|
61
136
|
|
|
62
137
|
session {
|
|
@@ -74,6 +149,17 @@ session {
|
|
|
74
149
|
sameSite = ${?SESSION_SAME_SITE}
|
|
75
150
|
domain = null
|
|
76
151
|
domain = ${?SESSION_DOMAIN}
|
|
152
|
+
# #405: every URL `POST /session/login` may accept as `redirect_to`, matched
|
|
153
|
+
# EXACTLY -- scheme, host, port, path and query all significant. Empty (the
|
|
154
|
+
# default) refuses every `redirect_to`, so leave it alone unless your login
|
|
155
|
+
# page actually posts one.
|
|
156
|
+
#
|
|
157
|
+
# redirectAllowlist = ["https://app.example.com/welcome"]
|
|
158
|
+
#
|
|
159
|
+
# `http://` is accepted for loopback hosts only. With `domain` set above, an
|
|
160
|
+
# entry outside it fails boot rather than being silently ignored. The
|
|
161
|
+
# federation flow has a separate list per provider (see `federations` below).
|
|
162
|
+
redirectAllowlist = []
|
|
77
163
|
storage {
|
|
78
164
|
type = "redis"
|
|
79
165
|
type = ${?SESSION_STORAGE_TYPE}
|
|
@@ -113,6 +199,39 @@ federations {
|
|
|
113
199
|
clientSecret = ${?FEDERATIONS_GOOGLE_CLIENT_SECRET}
|
|
114
200
|
callbackURL = "http://localhost:3000/session/oauth/federation/google/callback"
|
|
115
201
|
callbackURL = ${?FEDERATIONS_GOOGLE_CALLBACK_URL}
|
|
202
|
+
|
|
203
|
+
# Every URL a `redirect_to` query parameter may name on
|
|
204
|
+
# /session/oauth/federation/google. Matching is EXACT — scheme, host, port,
|
|
205
|
+
# path, query and fragment all count — so list each landing page in full.
|
|
206
|
+
# There is no wildcard and no subdomain matching.
|
|
207
|
+
#
|
|
208
|
+
# An empty list refuses every `redirect_to`, which is the right setting for
|
|
209
|
+
# a deployment that does not use the parameter. It is NOT a way to allow
|
|
210
|
+
# everything: before #278 an unset allowlist accepted any http(s) URL, which
|
|
211
|
+
# made this an open redirect. Nothing falls back to that any more.
|
|
212
|
+
#
|
|
213
|
+
# https is required, except for a loopback host (localhost, 127.0.0.0/8,
|
|
214
|
+
# [::1]) — that carve-out is what lets a local dev front-end and a native
|
|
215
|
+
# client on an ephemeral loopback listener work without a certificate. The
|
|
216
|
+
# port is still part of the match, so list the port the client binds.
|
|
217
|
+
#
|
|
218
|
+
# redirectAllowlist = [
|
|
219
|
+
# "https://app.example.com/welcome"
|
|
220
|
+
# "http://localhost:5173/welcome"
|
|
221
|
+
# ]
|
|
222
|
+
redirectAllowlist = []
|
|
223
|
+
|
|
224
|
+
# Cookie / session domain. When set, every non-loopback redirectAllowlist
|
|
225
|
+
# entry must be inside it, checked at boot — an entry outside it fails
|
|
226
|
+
# startup rather than sitting here looking effective.
|
|
227
|
+
# sessionDomain = ${?FEDERATIONS_GOOGLE_SESSION_DOMAIN}
|
|
228
|
+
|
|
229
|
+
# Post-callback bridge page. Required only if `redirect_to` is used at all:
|
|
230
|
+
# the browser lands here as `<authCallbackUrl>?redirect_to=<encoded>`.
|
|
231
|
+
# authCallbackUrl = ${?FEDERATIONS_GOOGLE_AUTH_CALLBACK_URL}
|
|
232
|
+
|
|
233
|
+
# Where the browser goes after a callback carrying no `redirect_to`.
|
|
234
|
+
# clientUrl = ${?FEDERATIONS_GOOGLE_CLIENT_URL}
|
|
116
235
|
}
|
|
117
236
|
}
|
|
118
237
|
|
|
@@ -132,10 +251,19 @@ repositories {
|
|
|
132
251
|
path = "./config/users.yaml"
|
|
133
252
|
}
|
|
134
253
|
http {
|
|
254
|
+
# Both URLs carry plaintext user credentials, so both must be absolute
|
|
255
|
+
# https URLs. http:// is accepted only for a loopback host (localhost,
|
|
256
|
+
# 127.0.0.0/8, [::1]); a private-range address or a container-network
|
|
257
|
+
# service name still needs https. Boot fails if either is not.
|
|
135
258
|
authenticateUrl = ${?CLIENT_USER_AUTHENTICATE_URL}
|
|
136
259
|
authenticateByTokenUrl = ${?CLIENT_USER_AUTHENTICATE_BY_TOKEN_URL}
|
|
260
|
+
# Positive integer, milliseconds, <= 2147483647. Boot fails otherwise —
|
|
261
|
+
# including on a blank env override, which HOCON substitutes as "".
|
|
137
262
|
timeout = 5000
|
|
138
263
|
timeout = ${?CLIENT_USER_TIMEOUT}
|
|
264
|
+
# Largest upstream response body accepted, in bytes. Positive integer.
|
|
265
|
+
maxResponseBytes = 1048576
|
|
266
|
+
maxResponseBytes = ${?CLIENT_USER_MAX_RESPONSE_BYTES}
|
|
139
267
|
}
|
|
140
268
|
}
|
|
141
269
|
code {
|
|
@@ -7,24 +7,78 @@
|
|
|
7
7
|
# - "none": public client — no clientSecret; PKCE/S256 mandatory at /authorize
|
|
8
8
|
#
|
|
9
9
|
# Secret format (only for confidential clients — basic / post):
|
|
10
|
-
# -
|
|
11
|
-
# -
|
|
10
|
+
# - Bcrypt hash: "$2b$10$..." <-- use this
|
|
11
|
+
# - Plain text: "my-secret" <-- accepted, for local development only
|
|
12
|
+
#
|
|
13
|
+
# Generate a real one:
|
|
14
|
+
# SECRET=$(openssl rand -base64 32)
|
|
15
|
+
# node -e "require('bcrypt').hash(process.argv[1],10).then(console.log)" "$SECRET"
|
|
16
|
+
# Give $SECRET to the client; put the hash in this file.
|
|
17
|
+
#
|
|
18
|
+
# #407: the schema accepts any non-empty string here, unlike `session.secret`
|
|
19
|
+
# and the JWT signing secret, which have carried a 256-bit floor since #282.
|
|
20
|
+
# That is deliberate — this field also holds a bcrypt hash, whose shape those
|
|
21
|
+
# floors would reject — but it means NOTHING stops you from shipping
|
|
22
|
+
# `clientSecret: "changeme"`. A client secret authenticates a confidential
|
|
23
|
+
# client at the token endpoint; treat it as you would any other credential
|
|
24
|
+
# and give it 256 bits of randomness.
|
|
25
|
+
#
|
|
26
|
+
# `firstParty` (#267): REQUIRED to be `true` for a client to use `/authorize`.
|
|
27
|
+
# `/authorize` mints a code as soon as the session is authenticated, with no
|
|
28
|
+
# consent step, so a forced top-level navigation from an attacker's page
|
|
29
|
+
# makes a logged-in user's browser mint one for whatever client is named.
|
|
30
|
+
# That is defensible for clients you operate and only for those. Mark a
|
|
31
|
+
# client `firstParty: true` only if you would be comfortable with it
|
|
32
|
+
# receiving the user's identity without the user being asked.
|
|
33
|
+
#
|
|
34
|
+
# A client without it is refused at `/authorize`, as is one carrying an
|
|
35
|
+
# explicit `firstParty: false`. There is no opt-out: mark every client you
|
|
36
|
+
# operate before pointing it at `/authorize`.
|
|
37
|
+
#
|
|
38
|
+
# `allowPlainPkce` (#273): PKCE is mandatory for EVERY client — confidential
|
|
39
|
+
# ones included — and `S256` is the only method accepted. There is no
|
|
40
|
+
# server-wide setting that changes either half. This per-client flag is the
|
|
41
|
+
# one exception hatch, and only for the method: `allowPlainPkce: true` lets
|
|
42
|
+
# that one client use the RFC 7636 `plain` challenge, which stores the
|
|
43
|
+
# verifier itself as the challenge and so proves nothing against anyone who
|
|
44
|
+
# can read the authorization request. Set it only for a legacy client that
|
|
45
|
+
# genuinely cannot compute SHA-256, and treat it as a migration deadline.
|
|
12
46
|
|
|
13
47
|
my-app:
|
|
14
48
|
tokenEndpointAuthMethod: "client_secret_basic"
|
|
49
|
+
firstParty: true
|
|
15
50
|
clientSecret: "$2b$10$REPLACE_WITH_BCRYPT_HASH"
|
|
16
51
|
allowedRedirectUris:
|
|
17
52
|
- "http://localhost:3000/callback"
|
|
18
53
|
allowedScopes:
|
|
19
54
|
- "read"
|
|
20
55
|
- "write"
|
|
56
|
+
# What an omitted `scope` parameter grants (#396). Must be a subset of
|
|
57
|
+
# allowedScopes (checked at boot). Without this, a request that omits
|
|
58
|
+
# `scope` is refused with invalid_scope — the old behavior granted the
|
|
59
|
+
# ENTIRE allowlist, which made "forgot to send scope" the maximum grant.
|
|
60
|
+
# Declare what an unscoped request should mean, not the ceiling.
|
|
61
|
+
defaultScopes:
|
|
62
|
+
- "read"
|
|
21
63
|
|
|
22
64
|
# Public client example (e.g., a single-page app or mobile app):
|
|
23
65
|
# my-spa:
|
|
24
66
|
# tokenEndpointAuthMethod: "none"
|
|
67
|
+
# firstParty: true
|
|
25
68
|
# # clientSecret MUST be absent for "none" clients.
|
|
26
69
|
# allowedRedirectUris:
|
|
27
70
|
# - "https://app.example/callback"
|
|
28
71
|
# allowedScopes:
|
|
29
72
|
# - "openid"
|
|
30
73
|
# - "profile"
|
|
74
|
+
|
|
75
|
+
# Legacy client that cannot compute SHA-256 (#273). Prefer fixing the client.
|
|
76
|
+
# my-legacy-device:
|
|
77
|
+
# tokenEndpointAuthMethod: "client_secret_basic"
|
|
78
|
+
# firstParty: true
|
|
79
|
+
# clientSecret: "$2b$10$REPLACE_WITH_BCRYPT_HASH"
|
|
80
|
+
# allowPlainPkce: true
|
|
81
|
+
# allowedRedirectUris:
|
|
82
|
+
# - "https://legacy.example/callback"
|
|
83
|
+
# allowedScopes:
|
|
84
|
+
# - "openid"
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
# Loaded when CONFIG_ENV (or NODE_ENV) is "production".
|
|
3
3
|
# Values here override application.conf.
|
|
4
4
|
#
|
|
5
|
-
# Most production-sensitive values (
|
|
6
|
-
# federation client secrets, etc.) should be supplied via environment
|
|
7
|
-
# see the ${?VAR} substitutions in application.conf.
|
|
5
|
+
# Most production-sensitive values (SESSION_SECRET, the JWT signing key, REDIS
|
|
6
|
+
# URLs, federation client secrets, etc.) should be supplied via environment
|
|
7
|
+
# variables — see the ${?VAR} substitutions in application.conf.
|
|
8
|
+
#
|
|
9
|
+
# #407: this used to name OAUTH_JWT_SECRET as the typical example. That is the
|
|
10
|
+
# HMAC secret, and it stopped being the default when EdDSA became one (#282) —
|
|
11
|
+
# the shipped path is now a key pair delivered as files
|
|
12
|
+
# (OAUTH_JWT_PRIVATE_KEY_PATH / OAUTH_JWT_PUBLIC_KEY_PATH), which
|
|
13
|
+
# docker-compose.production.yml mounts as compose secrets.
|
|
8
14
|
#
|
|
9
15
|
# Override entries here only when a value must be fixed per-environment rather
|
|
10
16
|
# than per-deployment (e.g. feature flags turned on only in production).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Production compose example (#293 item 16).
|
|
2
|
+
#
|
|
3
|
+
# `docker-compose.yml` is a DEVELOPMENT file: it builds the `develop` target
|
|
4
|
+
# and bind-mounts ./src and ./config into the container, so copy-paste
|
|
5
|
+
# deploying it ships a hot-reload server running your working tree. This file
|
|
6
|
+
# is the deployable shape: the `runtime` target (compiled JS only, tini,
|
|
7
|
+
# healthcheck), no source mounts, restart policies, and a Redis that is
|
|
8
|
+
# reachable from the app network only and persists to a volume.
|
|
9
|
+
#
|
|
10
|
+
# openssl genpkey -algorithm ed25519 -out jwt-private.pem
|
|
11
|
+
# openssl pkey -in jwt-private.pem -pubout -out jwt-public.pem
|
|
12
|
+
# chmod 600 jwt-private.pem
|
|
13
|
+
# docker compose -f docker-compose.production.yml up -d --build
|
|
14
|
+
#
|
|
15
|
+
# The pem pair is required: EdDSA is the default and there is no key-material
|
|
16
|
+
# default, so a deployment that generates none fails at boot naming the keys to
|
|
17
|
+
# set. It reaches the container as a compose secret rather than through
|
|
18
|
+
# `config/`, which the image COPYs — see the `secrets:` block below.
|
|
19
|
+
#
|
|
20
|
+
# What this file deliberately does NOT do:
|
|
21
|
+
#
|
|
22
|
+
# - **TLS.** Terminate it in front (reverse proxy / load balancer), and set
|
|
23
|
+
# HTTP_TRUST_PROXY to that hop's address so `req.ip` — and every IP-keyed
|
|
24
|
+
# rate limit — means the client, not the proxy. See README
|
|
25
|
+
# "Multi-replica deployments".
|
|
26
|
+
# - **Scale out.** This is one replica (DEPLOYMENT_MODE=single). Before
|
|
27
|
+
# `--scale app=N`, set DEPLOYMENT_MODE=multi and read that same README
|
|
28
|
+
# section — boot will then refuse any in-memory store that must be shared.
|
|
29
|
+
# - **Invent secrets.** SESSION_SECRET (and any client secrets referenced by
|
|
30
|
+
# config/clients.yaml) come from `.env`, which is required here — chmod 600
|
|
31
|
+
# it, keep it out of git (`git local-files` or your secret manager of
|
|
32
|
+
# choice), and never bake it into the image.
|
|
33
|
+
|
|
34
|
+
services:
|
|
35
|
+
app:
|
|
36
|
+
build:
|
|
37
|
+
context: .
|
|
38
|
+
target: runtime
|
|
39
|
+
restart: unless-stopped
|
|
40
|
+
ports:
|
|
41
|
+
# Loopback-bound on purpose (#407). This file assumes TLS is terminated
|
|
42
|
+
# in front, and the README says to keep `/metrics` off the public
|
|
43
|
+
# listener — a bare "3000:3000" publishes on every interface and
|
|
44
|
+
# contradicts both. A reverse proxy on the same host reaches it here.
|
|
45
|
+
#
|
|
46
|
+
# If your load balancer is on another host, publish it instead
|
|
47
|
+
# (`- "3000:3000"`) and put the access control somewhere real: a
|
|
48
|
+
# security group, a firewall rule, or the proxy's own network. Do not
|
|
49
|
+
# widen this line and call it done.
|
|
50
|
+
- "127.0.0.1:3000:3000"
|
|
51
|
+
depends_on:
|
|
52
|
+
redis:
|
|
53
|
+
condition: service_healthy
|
|
54
|
+
env_file:
|
|
55
|
+
# Required, unlike the dev file: a production boot without
|
|
56
|
+
# SESSION_SECRET (or with the sample one) must fail loudly here, not
|
|
57
|
+
# serve quietly with a guessable secret.
|
|
58
|
+
- path: .env
|
|
59
|
+
required: true
|
|
60
|
+
secrets:
|
|
61
|
+
# The signing keys. EdDSA is the default algorithm and there is no key
|
|
62
|
+
# material default, so the process needs the pem pair at boot — and the
|
|
63
|
+
# `runtime` image deliberately does not contain it: `Dockerfile` COPYs
|
|
64
|
+
# `config/`, so a key parked there is baked into a layer that travels
|
|
65
|
+
# with every image push. Compose secrets mount read-only under
|
|
66
|
+
# /run/secrets and are the reason the env below overrides the
|
|
67
|
+
# `./jwt-*.pem` paths in `.env.example`.
|
|
68
|
+
#
|
|
69
|
+
# Generate them next to this file before the first `up`:
|
|
70
|
+
# openssl genpkey -algorithm ed25519 -out jwt-private.pem
|
|
71
|
+
# openssl pkey -in jwt-private.pem -pubout -out jwt-public.pem
|
|
72
|
+
# chmod 600 jwt-private.pem
|
|
73
|
+
- jwt_private_key
|
|
74
|
+
- jwt_public_key
|
|
75
|
+
environment:
|
|
76
|
+
NODE_ENV: production
|
|
77
|
+
DEPLOYMENT_MODE: single
|
|
78
|
+
# Point the process at the mounted secrets rather than the working-copy
|
|
79
|
+
# paths `.env` carries for local runs.
|
|
80
|
+
OAUTH_JWT_PRIVATE_KEY_PATH: /run/secrets/jwt_private_key
|
|
81
|
+
OAUTH_JWT_PUBLIC_KEY_PATH: /run/secrets/jwt_public_key
|
|
82
|
+
# `environment` wins over `env_file`, which is the point here:
|
|
83
|
+
# `.env.example` ships SESSION_SECURE=false / SESSION_NAME=auth.sid so a
|
|
84
|
+
# plain-HTTP local run works, and this file requires that same `.env`.
|
|
85
|
+
# Behind the TLS this compose assumes, a non-Secure session cookie is the
|
|
86
|
+
# session handed to anyone who can read one plaintext hop. The default
|
|
87
|
+
# `__Host-` cookie name is restored with it — the prefix browsers only
|
|
88
|
+
# honour on a Secure cookie.
|
|
89
|
+
SESSION_SECURE: "true"
|
|
90
|
+
SESSION_NAME: __Host-auth.session
|
|
91
|
+
# Every Redis-backed store shares the compose-network Redis below.
|
|
92
|
+
REFRESH_TOKEN_FAMILY_STORE_REDIS_URL: redis://redis:6379
|
|
93
|
+
SESSION_STORAGE_TYPE: redis
|
|
94
|
+
SESSION_STORAGE_REDIS_URL: redis://redis:6379
|
|
95
|
+
# The memory rate limiter is per-process and evadable under bucket
|
|
96
|
+
# exhaustion (see README) — production points it at Redis.
|
|
97
|
+
RATE_LIMITER_ADAPTER: redis
|
|
98
|
+
|
|
99
|
+
redis:
|
|
100
|
+
# Tag-pinned, not digest-pinned. The Dockerfile argues for digests (#289)
|
|
101
|
+
# and that argument holds here too — a tag is mutable, so two `up`s a month
|
|
102
|
+
# apart can run different Redis builds. Pin the digest for a production
|
|
103
|
+
# deployment you need to reproduce:
|
|
104
|
+
# image: redis:7.2-alpine@sha256:<digest>
|
|
105
|
+
# Left as a tag in the template because a digest goes stale the week it is
|
|
106
|
+
# written and a scaffold that ships a stale one teaches nothing (#407).
|
|
107
|
+
image: redis:7.2-alpine
|
|
108
|
+
restart: unless-stopped
|
|
109
|
+
# No `ports:`, deliberately — the app reaches Redis over the compose
|
|
110
|
+
# network, and nothing else should. The dev file publishes 6379 to
|
|
111
|
+
# loopback for redis-cli convenience; a published production Redis with
|
|
112
|
+
# no AUTH is an incident.
|
|
113
|
+
command: ["redis-server", "--appendonly", "yes"]
|
|
114
|
+
volumes:
|
|
115
|
+
- redis-data:/data
|
|
116
|
+
healthcheck:
|
|
117
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
118
|
+
interval: 5s
|
|
119
|
+
timeout: 3s
|
|
120
|
+
retries: 5
|
|
121
|
+
|
|
122
|
+
volumes:
|
|
123
|
+
redis-data:
|
|
124
|
+
|
|
125
|
+
secrets:
|
|
126
|
+
# File-backed, so the pem pair stays on the host and reaches the container
|
|
127
|
+
# read-only at /run/secrets. Keep both out of git alongside `.env`; a key
|
|
128
|
+
# committed or COPYed into the image is a key you have to rotate.
|
|
129
|
+
jwt_private_key:
|
|
130
|
+
file: ./jwt-private.pem
|
|
131
|
+
jwt_public_key:
|
|
132
|
+
file: ./jwt-public.pem
|
|
@@ -16,6 +16,13 @@ services:
|
|
|
16
16
|
required: false
|
|
17
17
|
|
|
18
18
|
redis:
|
|
19
|
+
# Tag-pinned, not digest-pinned. The Dockerfile argues for digests (#289)
|
|
20
|
+
# and that argument holds here too — a tag is mutable, so two `up`s a month
|
|
21
|
+
# apart can run different Redis builds. Pin the digest for a production
|
|
22
|
+
# deployment you need to reproduce:
|
|
23
|
+
# image: redis:7.2-alpine@sha256:<digest>
|
|
24
|
+
# Left as a tag in the template because a digest goes stale the week it is
|
|
25
|
+
# written and a scaffold that ships a stale one teaches nothing (#407).
|
|
19
26
|
image: redis:7.2-alpine
|
|
20
27
|
ports:
|
|
21
28
|
- "127.0.0.1:6379:6379"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# #407: without this file, the first `git add .` in a scaffolded project
|
|
2
|
+
# commits the two things the README's own setup steps tell you to create right
|
|
3
|
+
# here — the `.env` holding SESSION_SECRET, and the signing key pair.
|
|
4
|
+
|
|
5
|
+
# Secrets and per-machine configuration
|
|
6
|
+
.env
|
|
7
|
+
.env.*
|
|
8
|
+
!.env.example
|
|
9
|
+
*.pem
|
|
10
|
+
*.key
|
|
11
|
+
config/clients.yaml
|
|
12
|
+
config/*.local.conf
|
|
13
|
+
|
|
14
|
+
# Build output and dependencies
|
|
15
|
+
node_modules/
|
|
16
|
+
dist/
|
|
17
|
+
coverage/
|
|
18
|
+
|
|
19
|
+
# Editor / OS noise
|
|
20
|
+
.DS_Store
|
|
21
|
+
*.log
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o3co/auth-provider-standalone",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
7
|
-
"node": ">=
|
|
7
|
+
"node": ">=22.0.0"
|
|
8
8
|
},
|
|
9
9
|
"packageManager": "pnpm@10.29.3",
|
|
10
10
|
"type": "module",
|
|
@@ -23,33 +23,28 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@o3co/auth-provider-core": "workspace:*",
|
|
25
25
|
"@o3co/auth-provider-federation-google": "workspace:*",
|
|
26
|
-
"@o3co/auth-provider-oauth": "workspace:*",
|
|
27
|
-
"@o3co/auth-provider-session": "workspace:*",
|
|
28
26
|
"@o3co/auth-provider-foundation": "workspace:*",
|
|
27
|
+
"@o3co/auth-provider-oauth": "workspace:*",
|
|
29
28
|
"@o3co/auth-provider-redis": "workspace:*",
|
|
30
|
-
"@o3co/auth
|
|
31
|
-
"@o3co/ts.hocon": "^1.
|
|
32
|
-
"connect-redis": "^
|
|
29
|
+
"@o3co/auth-provider-session": "workspace:*",
|
|
30
|
+
"@o3co/ts.hocon": "^1.13.0",
|
|
31
|
+
"connect-redis": "^10.0.0",
|
|
33
32
|
"express": "^5.2.1",
|
|
34
33
|
"express-session": "^1.19.0",
|
|
35
|
-
"helmet": "^8.
|
|
36
|
-
"ioredis": "^
|
|
34
|
+
"helmet": "^8.3.0",
|
|
35
|
+
"ioredis": "^6.0.0",
|
|
37
36
|
"pino": "^10.3.1",
|
|
38
|
-
"
|
|
37
|
+
"prom-client": "^15.1.3",
|
|
38
|
+
"redis": "^6.2.1"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/express": "^5.0.6",
|
|
42
42
|
"@types/express-session": "^1",
|
|
43
|
-
"@types/node": "^
|
|
44
|
-
"@types/supertest": "^7.2.
|
|
43
|
+
"@types/node": "^26.2.0",
|
|
44
|
+
"@types/supertest": "^7.2.1",
|
|
45
45
|
"supertest": "^7.1.0",
|
|
46
|
-
"tsx": "^4.
|
|
47
|
-
"typescript": "^
|
|
48
|
-
"vitest": "^4.1.
|
|
49
|
-
},
|
|
50
|
-
"pnpm": {
|
|
51
|
-
"onlyBuiltDependencies": [
|
|
52
|
-
"bcrypt"
|
|
53
|
-
]
|
|
46
|
+
"tsx": "^4.23.10",
|
|
47
|
+
"typescript": "^6.0.3",
|
|
48
|
+
"vitest": "^4.1.11"
|
|
54
49
|
}
|
|
55
50
|
}
|