@o3co/create-auth-provider 0.5.2 → 0.5.3
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/Dockerfile +16 -3
- package/templates/standalone/README.md +32 -0
- package/templates/standalone/config/application.conf +34 -0
- package/templates/standalone/package.json +1 -1
- package/templates/standalone/src/__tests__/smoke.test.mts +35 -0
- package/templates/versions.json +8 -8
package/package.json
CHANGED
|
@@ -13,11 +13,17 @@ FROM node-base AS deps
|
|
|
13
13
|
|
|
14
14
|
COPY package.json ./
|
|
15
15
|
|
|
16
|
+
USER node
|
|
17
|
+
|
|
16
18
|
RUN pnpm install
|
|
17
19
|
|
|
18
20
|
#############################################
|
|
19
21
|
FROM deps AS builder
|
|
20
22
|
|
|
23
|
+
# USER node inherited from deps stage; repeated here to make the builder
|
|
24
|
+
# hardening invariant obvious in Dockerfile reviews.
|
|
25
|
+
USER node
|
|
26
|
+
|
|
21
27
|
COPY tsconfig.json ./
|
|
22
28
|
COPY config/ ./config/
|
|
23
29
|
COPY src/ ./src/
|
|
@@ -31,13 +37,22 @@ ENV NODE_ENV=production
|
|
|
31
37
|
|
|
32
38
|
COPY --from=deps /home/node/package.json /home/node/pnpm-lock.yaml ./
|
|
33
39
|
|
|
40
|
+
USER node
|
|
41
|
+
|
|
34
42
|
RUN pnpm install --prod \
|
|
35
43
|
&& pnpm store prune
|
|
36
44
|
|
|
37
45
|
COPY --from=builder /home/node/dist/ ./dist/
|
|
38
46
|
COPY --from=builder /home/node/config/ ./config/
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
ENV HTTP_PORT=3000
|
|
49
|
+
EXPOSE ${HTTP_PORT}
|
|
50
|
+
|
|
51
|
+
# Use the shell form so ${HTTP_PORT} expands at probe time. The HOCON
|
|
52
|
+
# config reads `${?HTTP_PORT}` for `http.port`, so app.listen() and the
|
|
53
|
+
# healthcheck stay in sync regardless of the operator's port choice.
|
|
54
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
|
55
|
+
CMD wget -q -O /dev/null "http://localhost:${HTTP_PORT}/_healthcheck" || exit 1
|
|
41
56
|
|
|
42
57
|
ENTRYPOINT ["tini", "--"]
|
|
43
58
|
CMD ["node", "dist/app.mjs"]
|
|
@@ -53,6 +68,4 @@ CMD ["pnpm", "run", "test", "--", "--passWithNoTests"]
|
|
|
53
68
|
#############################################
|
|
54
69
|
FROM builder AS develop
|
|
55
70
|
|
|
56
|
-
USER node
|
|
57
|
-
|
|
58
71
|
CMD ["pnpm", "run", "debug"]
|
|
@@ -94,6 +94,7 @@ fail fast rather than silently falling back to defaults.
|
|
|
94
94
|
| Variable | Default | Description |
|
|
95
95
|
|---|---|---|
|
|
96
96
|
| `SESSION_SECRET` | — | **Required.** Session signing secret |
|
|
97
|
+
| `SESSION_NAME` | `__Host-auth.session` | Session cookie name. The default uses the `__Host-` prefix and therefore requires `SESSION_SECURE=true` and no `SESSION_DOMAIN`. |
|
|
97
98
|
| `SESSION_MAX_AGE` | `3600000` | Session cookie max age in milliseconds |
|
|
98
99
|
| `SESSION_SECURE` | `true` | Set `Secure` flag on session cookie |
|
|
99
100
|
| `SESSION_SAME_SITE` | `lax` | `SameSite` attribute (`lax`, `strict`, `none`) |
|
|
@@ -102,6 +103,11 @@ fail fast rather than silently falling back to defaults.
|
|
|
102
103
|
| `SESSION_STORAGE_REDIS_URL` | `redis://localhost:6379` | Redis connection URL for session storage |
|
|
103
104
|
| `SESSION_STORAGE_REDIS_PASSWORD` | — | Redis password for session storage |
|
|
104
105
|
|
|
106
|
+
If you set `SESSION_SECURE=false` for local HTTP development or set `SESSION_DOMAIN`
|
|
107
|
+
for shared-domain cookies, also set `SESSION_NAME` to a non-`__Host-` value such as
|
|
108
|
+
`auth.sid`. The server fails fast when a `__Host-` cookie name is combined with
|
|
109
|
+
attributes that browsers reject for that prefix.
|
|
110
|
+
|
|
105
111
|
### Google Federation
|
|
106
112
|
|
|
107
113
|
| Variable | Default | Description |
|
|
@@ -136,6 +142,20 @@ fail fast rather than silently falling back to defaults.
|
|
|
136
142
|
| `CLIENT_CODE_PASSWORD` | — | Redis password for code storage |
|
|
137
143
|
| `CLIENT_CODE_DEFAULT_EXPIRES_IN` | `600` | Default authorization code lifetime in seconds |
|
|
138
144
|
|
|
145
|
+
### Redis Namespacing
|
|
146
|
+
|
|
147
|
+
For multi-tenant Redis clusters, set deployment-specific key prefixes so two
|
|
148
|
+
auth.provider instances cannot collide in the same database:
|
|
149
|
+
|
|
150
|
+
| Variable | Default | Description |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `REDIS_SESSION_STORES_KEY_PREFIX` | `ss:` | Outer prefix for user sessions, RP registry, session-family index, and session-federation index. |
|
|
153
|
+
| `REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX` | `rtfam:` | Prefix for refresh-token family records. |
|
|
154
|
+
| `CLIENT_CODE_KEY_PREFIX` | `oauth:code:` | Prefix for OAuth authorization codes. |
|
|
155
|
+
|
|
156
|
+
Use values that include the deployment name, for example `tenant-a:ss:`,
|
|
157
|
+
`tenant-a:rtfam:`, and `tenant-a:code:`.
|
|
158
|
+
|
|
139
159
|
### Endpoints
|
|
140
160
|
|
|
141
161
|
| Variable | Default | Description |
|
|
@@ -185,6 +205,18 @@ make test
|
|
|
185
205
|
|
|
186
206
|
The `docker-compose.yml` starts the auth server together with a Redis container. Configure environment variables in `.env`.
|
|
187
207
|
|
|
208
|
+
The production image sets `ENV HTTP_PORT=3000` and uses it for both `EXPOSE`
|
|
209
|
+
and the Docker-native healthcheck against `/_healthcheck`. The HOCON config
|
|
210
|
+
also reads `${?HTTP_PORT}` for `http.port`, so `app.listen()` and the
|
|
211
|
+
healthcheck cannot drift apart. To run on a different port:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
docker run -e HTTP_PORT=8080 -p 8080:8080 my-auth-provider
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`EXPOSE` is image metadata and does not publish ports by itself, so the
|
|
218
|
+
explicit `-p` mapping is still required.
|
|
219
|
+
|
|
188
220
|
## Adding Custom Modules
|
|
189
221
|
|
|
190
222
|
To add a custom module, import it in `src/buildModules.mts` and include it in the `modules` array passed to `createApp`:
|
|
@@ -91,6 +91,11 @@ oauth {
|
|
|
91
91
|
}
|
|
92
92
|
refresh_token { enabled = true, enabled = ${?OAUTH_GRANTS_REFRESH_TOKEN_ENABLED} }
|
|
93
93
|
}
|
|
94
|
+
# IH-6: when oauth.jwt.issuer is configured, require `openid` in
|
|
95
|
+
# /authorize scope by default. Set OAUTH_OIDC_MODE=dual only if this OP
|
|
96
|
+
# intentionally also accepts OAuth-only authorization requests.
|
|
97
|
+
oidcMode = "oidc-required"
|
|
98
|
+
oidcMode = ${?OAUTH_OIDC_MODE}
|
|
94
99
|
# OR-9: adapter switch for the OAuth authorization-code repository. When
|
|
95
100
|
# set to "redis", the standalone composition root wires
|
|
96
101
|
# `redisCodeRepositoryModule` against the shared ioredis socket
|
|
@@ -102,10 +107,22 @@ oauth {
|
|
|
102
107
|
adapter = "redis"
|
|
103
108
|
adapter = ${?OAUTH_CODE_ADAPTER}
|
|
104
109
|
}
|
|
110
|
+
# F10: maximum existing RFC 8693 actor chain depth accepted before adding
|
|
111
|
+
# the current actor. Depth 3 allows one current actor plus two prior nested
|
|
112
|
+
# actors; lower for stricter delegation environments.
|
|
113
|
+
tokenExchange {
|
|
114
|
+
maxActorChainDepth = 3
|
|
115
|
+
maxActorChainDepth = ${?OAUTH_TOKEN_EXCHANGE_MAX_ACTOR_CHAIN_DEPTH}
|
|
116
|
+
}
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
session {
|
|
108
120
|
secret = ${?SESSION_SECRET}
|
|
121
|
+
# MIN-2: __Host- prefix requires Secure, Path=/, and no Domain attribute.
|
|
122
|
+
# If SESSION_SECURE=false or SESSION_DOMAIN is set, also override
|
|
123
|
+
# SESSION_NAME to a non-__Host- value.
|
|
124
|
+
name = "__Host-auth.session"
|
|
125
|
+
name = ${?SESSION_NAME}
|
|
109
126
|
maxAge = 3600000
|
|
110
127
|
maxAge = ${?SESSION_MAX_AGE}
|
|
111
128
|
secure = true
|
|
@@ -222,11 +239,28 @@ rateLimiter {
|
|
|
222
239
|
adapter = ${?RATE_LIMITER_ADAPTER}
|
|
223
240
|
}
|
|
224
241
|
|
|
242
|
+
memoryRateLimiter {
|
|
243
|
+
# SF-10: bound the in-memory OAuth endpoint rate-limiter bucket map.
|
|
244
|
+
# Multi-replica production should use `rateLimiter.adapter = "redis"`;
|
|
245
|
+
# this cap keeps the single-process fallback from growing without limit.
|
|
246
|
+
maxBuckets = 10000
|
|
247
|
+
maxBuckets = ${?MEMORY_RATE_LIMITER_MAX_BUCKETS}
|
|
248
|
+
}
|
|
249
|
+
|
|
225
250
|
userSessionStores {
|
|
226
251
|
adapter = "memory"
|
|
227
252
|
adapter = ${?USER_SESSION_STORES_ADAPTER}
|
|
228
253
|
}
|
|
229
254
|
|
|
255
|
+
# MIN-3: outer namespace for the four bundled Redis user-session stores.
|
|
256
|
+
# The module appends fixed sub-prefixes (`us:`, `rp:`, `fi:`, `fed:`).
|
|
257
|
+
# Multi-tenant Redis deployments should set a deployment-specific value,
|
|
258
|
+
# e.g. `tenant-a:ss:`.
|
|
259
|
+
redisSessionStores {
|
|
260
|
+
keyPrefix = "ss:"
|
|
261
|
+
keyPrefix = ${?REDIS_SESSION_STORES_KEY_PREFIX}
|
|
262
|
+
}
|
|
263
|
+
|
|
230
264
|
# D-2 v2 + Wave 5d: ioredis connection-config consumed by the shared
|
|
231
265
|
# `standaloneRedisClientsModule` in `templates/standalone/src/modules.mts`.
|
|
232
266
|
# One ioredis socket per replica backs ALL Redis-typed clients (RT family
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
17
18
|
import {
|
|
18
19
|
type AppConfig,
|
|
19
20
|
createApp,
|
|
@@ -30,6 +31,8 @@ import request from "supertest";
|
|
|
30
31
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
31
32
|
import { buildModules } from "../buildModules.mjs";
|
|
32
33
|
|
|
34
|
+
const dockerfile = readFileSync(new URL("../../Dockerfile", import.meta.url), "utf8");
|
|
35
|
+
|
|
33
36
|
const config: AppConfig = {
|
|
34
37
|
http: { port: 0, trustProxy: false },
|
|
35
38
|
oauth: {
|
|
@@ -51,6 +54,7 @@ const config: AppConfig = {
|
|
|
51
54
|
legacyRtPolicy: "reject" as const,
|
|
52
55
|
},
|
|
53
56
|
grants: {},
|
|
57
|
+
oidcMode: "oidc-required",
|
|
54
58
|
// OR-9: explicit memory adapter for the smoke test. The legacy
|
|
55
59
|
// `repositories.code.type` fallback would also pick memory here,
|
|
56
60
|
// but the explicit setting documents the intent and avoids
|
|
@@ -59,6 +63,7 @@ const config: AppConfig = {
|
|
|
59
63
|
},
|
|
60
64
|
session: {
|
|
61
65
|
secret: "test-session-secret",
|
|
66
|
+
name: "auth.sid",
|
|
62
67
|
maxAge: 3600000,
|
|
63
68
|
secure: false,
|
|
64
69
|
sameSite: "lax",
|
|
@@ -180,6 +185,36 @@ describe("standalone smoke test", () => {
|
|
|
180
185
|
handleRef = undefined;
|
|
181
186
|
});
|
|
182
187
|
|
|
188
|
+
it("Dockerfile declares runtime metadata and runs install/build steps as node", () => {
|
|
189
|
+
// Default port is 3000 but the HEALTHCHECK and EXPOSE must read it
|
|
190
|
+
// from ${HTTP_PORT} so an operator overriding the env var keeps the
|
|
191
|
+
// app, EXPOSE metadata, and healthcheck in sync. Assert the ENV /
|
|
192
|
+
// EXPOSE / HEALTHCHECK structure rather than the literal port so the
|
|
193
|
+
// test is not brittle when operators or downstream forks change the
|
|
194
|
+
// default.
|
|
195
|
+
// The literal `${HTTP_PORT}` token from the Dockerfile is split across
|
|
196
|
+
// concatenations to sidestep biome's `noTemplateCurlyInString` rule
|
|
197
|
+
// (it cannot statically tell that this string is intentionally NOT
|
|
198
|
+
// a JS template placeholder — it is a Dockerfile env-substitution).
|
|
199
|
+
const HTTP_PORT_VAR = `$${"{"}HTTP_PORT}`;
|
|
200
|
+
expect(dockerfile).toContain("ENV HTTP_PORT=3000");
|
|
201
|
+
expect(dockerfile).toContain(`EXPOSE ${HTTP_PORT_VAR}`);
|
|
202
|
+
expect(dockerfile).toMatch(
|
|
203
|
+
/HEALTHCHECK\s+--interval=30s\s+--timeout=3s\s+--start-period=10s\s+--retries=3/,
|
|
204
|
+
);
|
|
205
|
+
// Match the literal `${HTTP_PORT}` token inside the HEALTHCHECK CMD.
|
|
206
|
+
// `[$]` (character class) sidesteps biome's `noTemplateCurlyInString`
|
|
207
|
+
// false-positive without changing what we actually accept.
|
|
208
|
+
expect(dockerfile).toMatch(
|
|
209
|
+
/CMD\s+wget\s+-q\s+-O\s+\/dev\/null\s+"http:\/\/localhost:[$]\{HTTP_PORT\}\/_healthcheck"\s+\|\|\s+exit\s+1/,
|
|
210
|
+
);
|
|
211
|
+
expect(dockerfile).toMatch(/FROM node-base AS deps[\s\S]*USER node[\s\S]*RUN pnpm install/);
|
|
212
|
+
expect(dockerfile).toMatch(/FROM deps AS builder[\s\S]*USER node[\s\S]*RUN pnpm run build/);
|
|
213
|
+
expect(dockerfile).toMatch(
|
|
214
|
+
/FROM node-base AS runtime[\s\S]*USER node[\s\S]*RUN pnpm install --prod/,
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
183
218
|
it("GET /_healthcheck returns 200", async () => {
|
|
184
219
|
const { app, handle } = await buildApp();
|
|
185
220
|
handleRef = handle;
|
package/templates/versions.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@o3co/auth-provider-core": "0.5.
|
|
3
|
-
"@o3co/auth-provider-federation-google": "0.5.
|
|
4
|
-
"@o3co/auth-provider-federation-github": "0.5.
|
|
5
|
-
"@o3co/auth-provider-oauth": "0.5.
|
|
6
|
-
"@o3co/auth-provider-oauth-token-exchange": "0.5.
|
|
7
|
-
"@o3co/auth-provider-session": "0.5.
|
|
8
|
-
"@o3co/auth-provider-foundation": "0.5.
|
|
9
|
-
"@o3co/auth-provider-redis": "0.5.
|
|
2
|
+
"@o3co/auth-provider-core": "0.5.3",
|
|
3
|
+
"@o3co/auth-provider-federation-google": "0.5.3",
|
|
4
|
+
"@o3co/auth-provider-federation-github": "0.5.3",
|
|
5
|
+
"@o3co/auth-provider-oauth": "0.5.3",
|
|
6
|
+
"@o3co/auth-provider-oauth-token-exchange": "0.5.3",
|
|
7
|
+
"@o3co/auth-provider-session": "0.5.3",
|
|
8
|
+
"@o3co/auth-provider-foundation": "0.5.3",
|
|
9
|
+
"@o3co/auth-provider-redis": "0.5.3"
|
|
10
10
|
}
|