@o3co/create-auth-provider 0.14.0 → 0.16.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/README.ja.md +93 -47
- package/README.md +92 -46
- package/package.json +1 -1
- package/templates/standalone/.env.example +34 -0
- package/templates/standalone/Dockerfile +1 -1
- package/templates/standalone/README.ja.md +565 -130
- package/templates/standalone/README.md +356 -92
- package/templates/standalone/config/application.conf +77 -2
- package/templates/standalone/docker-compose.production.yml +7 -0
- package/templates/standalone/docker-compose.yml +7 -0
- package/templates/standalone/gitignore +3 -2
- package/templates/standalone/package.json +3 -2
- package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +49 -15
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +1103 -0
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +241 -0
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +1100 -0
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +40 -5
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +445 -0
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +446 -0
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +133 -0
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +85 -0
- package/templates/standalone/src/__tests__/google-session.fixture.mts +304 -0
- package/templates/standalone/src/__tests__/host-routes.test.mts +141 -0
- package/templates/standalone/src/__tests__/listen.test.mts +128 -0
- package/templates/standalone/src/__tests__/logger-projection.test.mts +77 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +20 -5
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +187 -0
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +139 -0
- package/templates/standalone/src/__tests__/replica-safety.test.mts +13 -3
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +149 -0
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +14 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +253 -22
- package/templates/standalone/src/__tests__/smoke.test.mts +39 -20
- package/templates/standalone/src/__tests__/store-credential.test.mts +276 -0
- package/templates/standalone/src/app.mts +30 -51
- package/templates/standalone/src/buildModules.mts +94 -12
- package/templates/standalone/src/listen.mts +66 -0
- package/templates/standalone/src/logger.mts +7 -4
- package/templates/standalone/src/modules.mts +118 -12
- package/templates/standalone/src/routes.mts +95 -0
- package/templates/standalone/src/shutdown.mts +90 -18
- package/templates/standalone/vitest.supertest-loopback.mts +22 -3
- package/templates/versions.json +15 -14
- package/templates/standalone/src/__tests__/terminal-error.test.mts +0 -96
- package/templates/standalone/src/terminalError.mts +0 -60
|
@@ -159,6 +159,44 @@ replaySeenSet {
|
|
|
159
159
|
adapter = ${?REPLAY_SEEN_SET_ADAPTER}
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
# #593: federation grants — a user's standing consent that a backend or an
|
|
163
|
+
# agent may obtain upstream IdP tokens with no session behind the call. Off by
|
|
164
|
+
# default; FEDERATION_GRANTS_ENABLED=true installs the routes, the two stores
|
|
165
|
+
# below and the subject-revocation service. Enabling it is a statement that
|
|
166
|
+
# the deployment has a consent page (FEDERATION_GRANTS_CONSENT_URL, no default),
|
|
167
|
+
# a `callbackURL` on every connection, and a user repository that covers each
|
|
168
|
+
# connection's registration (or FEDERATION_GRANTS_IDENTITY_LOOKUP=unsupported,
|
|
169
|
+
# the recorded decision not to make that check) — each refused at boot when
|
|
170
|
+
# missing. Connections and the encryption key ring have no environment form;
|
|
171
|
+
# write them in a deployment-owned HOCON layer (README: Federation Grants).
|
|
172
|
+
federationGrants {
|
|
173
|
+
enabled = false
|
|
174
|
+
enabled = ${?FEDERATION_GRANTS_ENABLED}
|
|
175
|
+
identityLookup = "required"
|
|
176
|
+
identityLookup = ${?FEDERATION_GRANTS_IDENTITY_LOOKUP}
|
|
177
|
+
consent {
|
|
178
|
+
url = ${?FEDERATION_GRANTS_CONSENT_URL}
|
|
179
|
+
}
|
|
180
|
+
encryptionMode = "required"
|
|
181
|
+
encryptionMode = ${?FEDERATION_GRANTS_ENCRYPTION_MODE}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# "redis" for both, as for the denylist above: a grant is an offline credential
|
|
185
|
+
# that must outlive a restart, and every store this file already selects shares
|
|
186
|
+
# the same socket. Redis grants need the subject boundary in Redis too
|
|
187
|
+
# (USER_SESSION_STORES_ADAPTER=redis, which both compose files set) — boot
|
|
188
|
+
# refuses the pairing with a memory boundary, because the grants would outlive
|
|
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
|
+
|
|
162
200
|
# #527: where consent to a client that is not first-party is recorded.
|
|
163
201
|
# "none" (the default) wires no store and such clients are refused at
|
|
164
202
|
# /authorize; "memory" is single-replica only (refused under
|
|
@@ -269,6 +307,20 @@ federations {
|
|
|
269
307
|
|
|
270
308
|
# Where the browser goes after a callback carrying no `redirect_to`.
|
|
271
309
|
# clientUrl = ${?FEDERATIONS_GOOGLE_CLIENT_URL}
|
|
310
|
+
|
|
311
|
+
# RFC 9207: Google always returns `iss` on the callback, so a callback
|
|
312
|
+
# without one is refused. `false` is the way out if Google ever stops
|
|
313
|
+
# sending it. It permits a missing `iss` only — one that is sent and is not
|
|
314
|
+
# Google's is refused either way.
|
|
315
|
+
# requireAuthorizationResponseIss = ${?FEDERATIONS_GOOGLE_REQUIRE_AUTHORIZATION_RESPONSE_ISS}
|
|
316
|
+
|
|
317
|
+
# Whether sign-in asks Google for a refresh token. Unset is "offline":
|
|
318
|
+
# every sign-in shows Google's consent screen, because Google issues a
|
|
319
|
+
# refresh token only there, and every session gets one that
|
|
320
|
+
# POST /oauth/federation/google/token can refresh with. "online" shows the
|
|
321
|
+
# consent screen on the first sign-in only and gets no refresh token at
|
|
322
|
+
# all — for a deployment that uses Google to sign in and nothing more.
|
|
323
|
+
accessType = ${?FEDERATIONS_GOOGLE_ACCESS_TYPE}
|
|
272
324
|
}
|
|
273
325
|
|
|
274
326
|
# Any OpenID Connect provider (Okta, Entra ID, Auth0, Keycloak, your own
|
|
@@ -327,17 +379,40 @@ repositories {
|
|
|
327
379
|
# Both URLs carry plaintext user credentials, so both must be absolute
|
|
328
380
|
# https URLs. http:// is accepted only for a loopback host (localhost,
|
|
329
381
|
# 127.0.0.0/8, [::1]); a private-range address or a container-network
|
|
330
|
-
# service name still needs https. Boot fails if either is not.
|
|
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.
|
|
331
385
|
authenticateUrl = ${?CLIENT_USER_AUTHENTICATE_URL}
|
|
332
386
|
authenticateByTokenUrl = ${?CLIENT_USER_AUTHENTICATE_BY_TOKEN_URL}
|
|
333
387
|
# #482: optional. When set, a signed-in user can link a second federated
|
|
334
388
|
# identity (GET /session/oauth/federation/<name>?link=1): the Store
|
|
335
389
|
# receives { userId, provider, sub, token, claims } and decides — never on
|
|
336
|
-
# an unverified or relay e-mail. Same https rule as the two URLs above.
|
|
390
|
+
# an unverified or relay e-mail. Same https and no-redirect rule as the two URLs above.
|
|
337
391
|
# Also declared in core's reference.conf, so a composition that does not
|
|
338
392
|
# layer this file gets the same override.
|
|
339
393
|
# Without it, link=1 is refused with link_unsupported.
|
|
340
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}
|
|
341
416
|
# Positive integer, milliseconds, <= 2147483647. Boot fails otherwise —
|
|
342
417
|
# including on a blank env override, which HOCON substitutes as "".
|
|
343
418
|
timeout = 5000
|
|
@@ -49,6 +49,13 @@ services:
|
|
|
49
49
|
# security group, a firewall rule, or the proxy's own network. Do not
|
|
50
50
|
# widen this line and call it done.
|
|
51
51
|
- "127.0.0.1:3000:3000"
|
|
52
|
+
# #593 slice 7: compose's default grace is ten seconds — the HTTP drain
|
|
53
|
+
# budget alone. With federation grants on, `src/app.mts` gives cleanup 45
|
|
54
|
+
# more for the drain of a rotated upstream credential's write, and SIGKILL
|
|
55
|
+
# has to arrive after that: drain (10) + cleanup (45) + an exit margin.
|
|
56
|
+
# The cleanup allowance grows by itself with `federationGrants.upstreamHardTimeoutMs`,
|
|
57
|
+
# `persistRetryBudgetMs` and `lockWaitMs`; raise this to match when you raise them.
|
|
58
|
+
stop_grace_period: 60s
|
|
52
59
|
depends_on:
|
|
53
60
|
redis:
|
|
54
61
|
condition: service_healthy
|
|
@@ -8,6 +8,13 @@ services:
|
|
|
8
8
|
volumes:
|
|
9
9
|
- ./src:/home/node/src
|
|
10
10
|
- ./config:/home/node/config
|
|
11
|
+
# #593 slice 7: compose's default grace is ten seconds — the HTTP drain
|
|
12
|
+
# budget alone. With federation grants on, `src/app.mts` gives cleanup 45
|
|
13
|
+
# more for the drain of a rotated upstream credential's write, and SIGKILL
|
|
14
|
+
# has to arrive after that: drain (10) + cleanup (45) + an exit margin.
|
|
15
|
+
# The cleanup allowance grows by itself with `federationGrants.upstreamHardTimeoutMs`,
|
|
16
|
+
# `persistRetryBudgetMs` and `lockWaitMs`; raise this to match when you raise them.
|
|
17
|
+
stop_grace_period: 60s
|
|
11
18
|
depends_on:
|
|
12
19
|
redis:
|
|
13
20
|
condition: service_healthy
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# #407: without this file, the first `git add .` in a scaffolded project
|
|
2
|
-
# commits the two things
|
|
3
|
-
#
|
|
2
|
+
# commits the two things a local setup creates right here — the signing key
|
|
3
|
+
# pair the README's Usage generates, and the `.env` (holding SESSION_SECRET)
|
|
4
|
+
# the compose files read.
|
|
4
5
|
|
|
5
6
|
# Secrets and per-machine configuration
|
|
6
7
|
.env
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o3co/auth-provider-standalone",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"start": "node dist/app.mjs",
|
|
14
14
|
"debug": "NODE_OPTIONS='--conditions=development' tsx watch src/app.mts",
|
|
15
|
-
"test": "vitest run --passWithNoTests"
|
|
15
|
+
"test": "tsc --noEmit && vitest run --passWithNoTests"
|
|
16
16
|
},
|
|
17
17
|
"imports": {
|
|
18
18
|
"#/*": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@o3co/auth-provider-core": "workspace:*",
|
|
25
25
|
"@o3co/auth-provider-federation-google": "workspace:*",
|
|
26
|
+
"@o3co/auth-provider-federation-grants": "workspace:*",
|
|
26
27
|
"@o3co/auth-provider-federation-oidc": "workspace:*",
|
|
27
28
|
"@o3co/auth-provider-foundation": "workspace:*",
|
|
28
29
|
"@o3co/auth-provider-oauth": "workspace:*",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { fileURLToPath } from "node:url";
|
|
30
|
-
import { type AppConfig, AppConfigSchema } from "@o3co/auth-provider-core";
|
|
30
|
+
import { type AppConfig, AppConfigSchema, type Logger } from "@o3co/auth-provider-core";
|
|
31
31
|
import { parseFile } from "@o3co/ts.hocon";
|
|
32
32
|
import { validate } from "@o3co/ts.hocon/zod";
|
|
33
33
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
@@ -54,15 +54,47 @@ function loadShipped(env: Record<string, string> = {}): AppConfig {
|
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
58
|
-
function
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
/** What `buildModules` hands the logger it is given, per level. */
|
|
58
|
+
function logged(config: AppConfig): Array<{ level: string; args: unknown[] }> {
|
|
59
|
+
const calls: Array<{ level: string; args: unknown[] }> = [];
|
|
60
|
+
const record =
|
|
61
|
+
(level: string) =>
|
|
62
|
+
(...args: unknown[]): void => {
|
|
63
|
+
calls.push({ level, args });
|
|
64
|
+
};
|
|
65
|
+
const logger: Logger = {
|
|
66
|
+
trace: record("trace"),
|
|
67
|
+
debug: record("debug"),
|
|
68
|
+
info: record("info"),
|
|
69
|
+
warn: record("warn"),
|
|
70
|
+
error: record("error"),
|
|
71
|
+
fatal: record("fatal"),
|
|
72
|
+
child: () => logger,
|
|
73
|
+
};
|
|
74
|
+
buildModules(config, { logger });
|
|
75
|
+
return calls;
|
|
64
76
|
}
|
|
65
77
|
|
|
78
|
+
/** The one line the alias is reported by: object-first, an event name, at warn. */
|
|
79
|
+
const ALIAS_WARNING = {
|
|
80
|
+
level: "warn",
|
|
81
|
+
args: [
|
|
82
|
+
{
|
|
83
|
+
key: "oauth.accessToken.expiresIn",
|
|
84
|
+
env: "OAUTH_ACCESS_TOKEN_EXPIRES_IN",
|
|
85
|
+
replacement: "oauth.accessToken.defaultExpiresIn",
|
|
86
|
+
replacementEnv: "OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN",
|
|
87
|
+
},
|
|
88
|
+
"config_key_deprecated",
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Every line `buildModules` writes about the deprecated key. */
|
|
93
|
+
const aliasWarnings = (config: AppConfig) =>
|
|
94
|
+
logged(config).filter(
|
|
95
|
+
({ args }) => (args[0] as { key?: unknown } | undefined)?.key === "oauth.accessToken.expiresIn",
|
|
96
|
+
);
|
|
97
|
+
|
|
66
98
|
describe("the deprecated access-token lifetime alias", () => {
|
|
67
99
|
afterEach(() => {
|
|
68
100
|
vi.restoreAllMocks();
|
|
@@ -81,12 +113,8 @@ describe("the deprecated access-token lifetime alias", () => {
|
|
|
81
113
|
);
|
|
82
114
|
});
|
|
83
115
|
|
|
84
|
-
it("warns once when OAUTH_ACCESS_TOKEN_EXPIRES_IN still decides the default", () => {
|
|
85
|
-
|
|
86
|
-
expect(warnings).toHaveLength(1);
|
|
87
|
-
expect(warnings[0]).toMatch(/deprecated/);
|
|
88
|
-
expect(warnings[0]).toMatch(/oauth\.accessToken\.defaultExpiresIn/);
|
|
89
|
-
expect(warnings[0]).toMatch(/OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN/);
|
|
116
|
+
it("warns once, object-first, when OAUTH_ACCESS_TOKEN_EXPIRES_IN still decides the default", () => {
|
|
117
|
+
expect(logged(loadShipped({ OAUTH_ACCESS_TOKEN_EXPIRES_IN: "900" }))).toEqual([ALIAS_WARNING]);
|
|
90
118
|
});
|
|
91
119
|
|
|
92
120
|
it("warns for a hand-built configuration that overrides only the old key", () => {
|
|
@@ -96,6 +124,12 @@ describe("the deprecated access-token lifetime alias", () => {
|
|
|
96
124
|
...config,
|
|
97
125
|
oauth: { ...config.oauth, accessToken: { expiresIn: 900 } },
|
|
98
126
|
}),
|
|
99
|
-
).
|
|
127
|
+
).toEqual([ALIAS_WARNING]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("writes the same line through consoleLogger when no logger is handed over", () => {
|
|
131
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
132
|
+
buildModules(loadShipped({ OAUTH_ACCESS_TOKEN_EXPIRES_IN: "900" }));
|
|
133
|
+
expect(warn.mock.calls).toEqual([ALIAS_WARNING.args]);
|
|
100
134
|
});
|
|
101
135
|
});
|