@o3co/create-auth-provider 0.17.0-rc.1 → 0.17.0-rc.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.
Files changed (70) hide show
  1. package/README.ja.md +3 -1
  2. package/README.md +5 -2
  3. package/package.json +1 -1
  4. package/templates/standalone/.env.example +7 -5
  5. package/templates/standalone/README.ja.md +53 -25
  6. package/templates/standalone/README.md +140 -58
  7. package/templates/standalone/config/application.conf +41 -36
  8. package/templates/standalone/config/clients.yaml.example +8 -0
  9. package/templates/standalone/config/development.conf +2 -2
  10. package/templates/standalone/config/mailpit.conf +10 -5
  11. package/templates/standalone/config/reference.conf +30 -5
  12. package/templates/standalone/docker-compose.mailpit.yml +3 -0
  13. package/templates/standalone/docker-compose.production.yml +5 -2
  14. package/templates/standalone/package.json +1 -1
  15. package/templates/standalone/src/__tests__/adapters.test.mts +92 -25
  16. package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +87 -0
  17. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +112 -98
  18. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +40 -11
  19. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +86 -51
  20. package/templates/standalone/src/__tests__/audit-sink.test.mts +10 -9
  21. package/templates/standalone/src/__tests__/config-notices.test.mts +157 -0
  22. package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +48 -0
  23. package/templates/standalone/src/__tests__/core-federations.test.mts +57 -31
  24. package/templates/standalone/src/__tests__/core-sections.test.mts +7 -5
  25. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +1 -1
  26. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +434 -136
  27. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +19 -21
  28. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +9 -15
  29. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +10 -4
  30. package/templates/standalone/src/__tests__/federation-types.test.mts +239 -0
  31. package/templates/standalone/src/__tests__/google-session.fixture.mts +60 -67
  32. package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +1 -1
  33. package/templates/standalone/src/__tests__/library-references.fixture.mts +51 -46
  34. package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +216 -0
  35. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +81 -16
  36. package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +122 -0
  37. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +8 -4
  38. package/templates/standalone/src/__tests__/mfa-switch.test.mts +149 -46
  39. package/templates/standalone/src/__tests__/own-modules.test.mts +78 -23
  40. package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +3 -3
  41. package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +214 -0
  42. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +8 -4
  43. package/templates/standalone/src/__tests__/replica-safety.test.mts +131 -19
  44. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +25 -23
  45. package/templates/standalone/src/__tests__/sections.test.mts +24 -0
  46. package/templates/standalone/src/__tests__/session-lifecycle.test.mts +215 -0
  47. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +3 -7
  48. package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +130 -0
  49. package/templates/standalone/src/__tests__/session-requirements.test.mts +114 -31
  50. package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +274 -0
  51. package/templates/standalone/src/__tests__/smoke.test.mts +19 -28
  52. package/templates/standalone/src/__tests__/startup-log-lines.test.mts +148 -0
  53. package/templates/standalone/src/__tests__/store-credential.test.mts +8 -13
  54. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +58 -38
  55. package/templates/standalone/src/__tests__/two-phase-config.test.mts +243 -76
  56. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +23 -31
  57. package/templates/standalone/src/adapters.mts +31 -18
  58. package/templates/standalone/src/app.mts +6 -5
  59. package/templates/standalone/src/bootRefusal.mts +117 -0
  60. package/templates/standalone/src/buildModules.mts +46 -96
  61. package/templates/standalone/src/configPath.mts +180 -153
  62. package/templates/standalone/src/mfaSwitch.mts +49 -27
  63. package/templates/standalone/src/modules.mts +37 -219
  64. package/templates/standalone/src/rootRenames.mts +38 -22
  65. package/templates/standalone/src/sections.mts +13 -2
  66. package/templates/standalone/tests/index.test.js +4 -3
  67. package/templates/versions.json +18 -18
  68. package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +0 -134
  69. package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +0 -202
  70. package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +0 -144
package/README.ja.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @o3co/create-auth-provider
2
2
 
3
- 最終更新: 2026-10-03
3
+ 最終更新: 2026-10-06
4
4
 
5
5
  auth.provider 用の CLI スキャフォルダーです。内包するテンプレートの 1 つから新しいサーバープロジェクトを生成します。
6
6
 
@@ -86,6 +86,8 @@ SMTP リレー)を設定するか、`MFA_MODE=off` で MFA を無効にする
86
86
  `mfaMode = ${?MFA_MODE}` をこの順に追記するので、後のテンプレートがデフォルトを
87
87
  変えてもプロジェクトは off のままで、`MFA_MODE` を設定すれば作り直さずに MFA を
88
88
  有効にできます。MFA パッケージは依存に残り、ほかには何も削除・変更しません。
89
+ プロジェクトに含まれるテスト(`pnpm run test`)はスイッチをプロジェクトの設定から
90
+ 読み、その値が意味する動作を確かめるので、`--no-mfa` の有無にかかわらず通ります。
89
91
 
90
92
  生成されるプロジェクトは pnpm のプロジェクトです。`Dockerfile` は
91
93
  `pnpm install --frozen-lockfile` でインストールし、ビルドの許可リストは
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @o3co/create-auth-provider
2
2
 
3
- Last updated: 2026-10-03
3
+ Last updated: 2026-10-06
4
4
 
5
5
  CLI scaffolder for auth.provider. Generates a new server project from one of the built-in templates.
6
6
 
@@ -85,7 +85,10 @@ switch installs and needs is in the template's README.
85
85
  it appends `mfaMode = "off"` and then `mfaMode = ${?MFA_MODE}` to
86
86
  `config/application.conf`, so the project stays off whatever default a later
87
87
  template ships, and `MFA_MODE` still turns MFA on without re-scaffolding. The
88
- MFA package stays a dependency; nothing else is removed or changed.
88
+ MFA package stays a dependency; nothing else is removed or changed. The
89
+ project's own suite (`pnpm run test`) reads the switch from the project's
90
+ configuration and asserts what it implies, so it passes with or without
91
+ `--no-mfa`.
89
92
 
90
93
  The generated project is a pnpm project: its `Dockerfile` installs with
91
94
  `pnpm install --frozen-lockfile`, and its build allowlist lives in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o3co/create-auth-provider",
3
- "version": "0.17.0-rc.1",
3
+ "version": "0.17.0-rc.3",
4
4
  "description": "Scaffold a new o3co/auth.provider project from one of its templates",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -78,6 +78,10 @@ SESSION_STORE_STORAGE_REDIS_URL=redis://redis:6379
78
78
  # defaults to redis://localhost:6379, which inside the container is the
79
79
  # container, so leaving it unset makes the compose boot dial nothing.
80
80
  REDIS_CLIENTS_URL=redis://redis:6379
81
+ # The Redis stores refuse to boot unless the server reports maxmemory-policy
82
+ # noeviction (README: Multi-replica deployments). Only for a server that does
83
+ # run noeviction but will not report it (INFO and CONFIG refused to this user):
84
+ # REDIS_CLIENTS_ASSUME_NO_EVICTION=true
81
85
 
82
86
  # --- Federation grants (#593, optional) --------------------------------------
83
87
  # Offline delegation of an upstream IdP's tokens to a backend or an agent, on a
@@ -126,8 +130,9 @@ REDIS_CLIENTS_URL=redis://redis:6379
126
130
  # Or run without MFA:
127
131
  # MFA_MODE=off
128
132
  # To read the mail in development, leave every STANDARD_SMTP_MAIL_SENDER_*
129
- # unset (USER and PASSWORD included) and run the Mailpit
130
- # overlay, which sets them (README: Mailpit for the SMTP sender):
133
+ # unset (USER and PASSWORD included), set MFA_ENCRYPTION_KEY to a key of your
134
+ # own (the run does not use the development sample key), and run the Mailpit
135
+ # overlay, which sets the sender's variables (README: Mailpit for the SMTP sender):
131
136
  # docker compose -f docker-compose.yml -f docker-compose.mailpit.yml up --build
132
137
 
133
138
  # Development (override for local HTTP).
@@ -142,9 +147,6 @@ SESSION_STORE_NAME=auth.sid
142
147
  # OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN=3600
143
148
  # The most a token-exchange `expires_in` can obtain; unset = the default.
144
149
  # OAUTH_ACCESS_TOKEN_MAX_EXPIRES_IN=3600
145
- # DEPRECATED alias of OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN, read only while
146
- # that one is unset.
147
- # OAUTH_ACCESS_TOKEN_EXPIRES_IN=3600
148
150
  # OAUTH_REFRESH_TOKEN_EXPIRES_IN=86400
149
151
  # SESSION_STORE_MAX_AGE=3600000
150
152
  # SESSION_STORE_SAME_SITE=lax # `none` additionally requires SESSION_STORE_SECURE=true
@@ -1,6 +1,6 @@
1
1
  # @o3co/auth-provider-standalone
2
2
 
3
- 最終更新: 2026-10-03
3
+ 最終更新: 2026-10-06
4
4
 
5
5
  auth.provider のデプロイ可能なサーバーテンプレート。これは composition root であり、設定を読み込み、モジュールをロードし、Express サーバーを起動する。`@o3co/create-auth-provider` で生成される。
6
6
 
@@ -11,7 +11,7 @@ auth.provider のデプロイ可能なサーバーテンプレート。これは
11
11
  **所有する**のは、1 つのデプロイに固有の選択である:
12
12
 
13
13
  - どのモジュールをどの順序で合成し、各ストアスロットをどのアダプターで埋めるか — [`src/buildModules.mts`](src/buildModules.mts)([モジュール合成順序](#モジュール合成順序) を参照)。その選択は自身のキー、`adapters` セクション([`src/adapters.mts`](src/adapters.mts))と MFA のスイッチ `mfaMode`([`src/mfaSwitch.mts`](src/mfaSwitch.mts))から読む
14
- - この scaffold だけが持つモジュール — `logging` と `http`(ホストプロセス自身の設定とその CORS のリスト、および core の `httpSettings` スロット)、署名鍵ストア、クライアントリポジトリとユーザーリポジトリ、監査 sink、共有される唯一の Redis 接続、in-memory のユーザーセッションストア・コードリポジトリ・フェデレーショントークンストア、そしてフェデレーションの config bridge — [`src/modules.mts`](src/modules.mts)。それらが所有するセクションのデフォルトは [`config/reference.conf`](config/reference.conf) にある。これらがパッケージではなく scaffold 側にあるのは、どれもこのテンプレートの設定セクションから自分のコンポーネントを組み立てるためである。別のソースを使いたいデプロイは、同じ形のモジュールを自前で配線する
14
+ - この scaffold だけが持つモジュール — `logging` と `http`(ホストプロセス自身の設定とその CORS のリスト、および core の `httpSettings` スロット)、署名鍵ストア、クライアントリポジトリとユーザーリポジトリ、監査 sink、共有される唯一の Redis 接続、in-memory のユーザーセッションストア・コードリポジトリ・フェデレーショントークンストア — [`src/modules.mts`](src/modules.mts)。それらが所有するセクションのデフォルトは [`config/reference.conf`](config/reference.conf) にある。これらがパッケージではなく scaffold 側にあるのは、どれもこのテンプレートの設定セクションから自分のコンポーネントを組み立てるためである。別のソースを使いたいデプロイは、同じ形のモジュールを自前で配線する
15
15
  - 設定をどこから読み、そのレイヤーをどう重ねるか — [`src/configPath.mts`](src/configPath.mts) と [`config/`](config/)
16
16
  - ホストプロセス: Express アプリ、そのセキュリティヘッダー、起動処理 — [`src/app.mts`](src/app.mts)。health / readiness / metrics の各ルート、合成されたルーター、その後ろの core の終端のエラーハンドラーを、マウントする順に — [`src/routes.mts`](src/routes.mts)。リスナーはソケットが bind されたときに `server_listening`(info、`port`)を 1 行ログに出し、bind できなければその bind のエラーで起動を失敗させ、その後のサーバーのエラーは `server_error`(error)としてログに出す — [`src/listen.mts`](src/listen.mts)。このコードがログに出すエラー — 処理されなかったリクエストのエラー、共有 Redis 接続の `error` イベント、失敗したシャットダウン — はすべて core の [`loggableError`](../../packages/core/README.ja.md#logger) による射影としてログに出し、エラーそのものは出さない。エラーは上流や Redis が言ったことを運びうるためである(サーバーが接続を拒否したとき、接続のエラーは `AUTH` のハンドシェイクをパスワードごと運ぶ)
17
17
  - 具体的な logger と監査ストリーム(pino) — [`src/logger.mts`](src/logger.mts) — およびメトリクス([`src/metrics.mts`](src/metrics.mts))
@@ -50,7 +50,7 @@ my-app:
50
50
 
51
51
  **これがしないこと。** ファーストパーティで*ある*クライアントについて、`/authorize` を強制遷移に対して安全にするわけではない — それはここで受け入れているモデルのままである。これが防ぐのは、確認なしのコード発行を任せるべきではなかったクライアントが、誤ってその立場に登録されることである。
52
52
 
53
- **マークの無い登録。** このフィールドの無い登録はファーストパーティではない: デフォルトのままでは、マークするまで `/authorize` で拒否され、警告付きで受け入れる猶予期間も無い。自分で運用するクライアントは、`/authorize` に向ける前にすべてマークすること。`oauth.authorize.allowUnmarkedClients` は設定項目ではない: これを設定した config や環境(`OAUTH_AUTHORIZE_ALLOW_UNMARKED_CLIENTS`、値は問わない)は、黙って無視されるのではなく、移行手順を示して起動時に失敗する。
53
+ **マークの無い登録。** このフィールドの無い登録はファーストパーティではない: デフォルトのままでは、マークするまで `/authorize` で拒否され、警告付きで受け入れる猶予期間も無い。自分で運用するクライアントは、`/authorize` に向ける前にすべてマークすること。`oauth.authorize.allowUnmarkedClients` は設定項目ではない: これを設定した config や環境(`OAUTH_AUTHORIZE_ALLOW_UNMARKED_CLIENTS`、値は問わない)は、黙って無視されるのではなく、起動時に失敗する: oauth モジュールがキーを削除されたキーとして(`config-path-relocated`)、変数を削除されたキーに束縛されていた変数として(`environment-variable-renamed`)拒否する。クライアントをマークしてから、キーを消し、変数を unset すること。
54
54
 
55
55
  ## マルチレプリカ構成
56
56
 
@@ -58,6 +58,8 @@ my-app:
58
58
 
59
59
  ロードバランサ配下で standalone サーバーを複数インスタンス動かすときは、`REDIS_CLIENTS_URL` を**共有**の Redis 7.2+ インスタンスに向けること。共有の Redis URL が無いと、各レプリカは refresh token family を自分の接続(と、ローカル専用の Redis)に保持するため、トークンを発行していないレプリカに届いた refresh リクエストは `invalid_grant` を返し、ラウンドロビンの LB では 1 回おきにそうなる。
60
60
 
61
+ **その Redis は `maxmemory-policy noeviction`(Redis のデフォルト)で動かすこと。** 期限まで残らなければならないキーを持つ Redis のストア — attempt counter、ユーザーセッションストアのライフサイクルストア、フェデレーショントークンストア、MFA のストア — は起動時にポリシーを読む(`INFO memory`、次に `CONFIG GET maxmemory-policy`)。ほかのポリシーでは起動を拒否し、サーバーがポリシーを答えないときも拒否する。接続のユーザーが `INFO` や `CONFIG` を実行できない(ACL で制限されたユーザー、それらを改名するマネージドサービス)が、サーバーが `noeviction` で動いていると分かっているなら、`REDIS_CLIENTS_ASSUME_NO_EVICTION=true`(`redis-clients.assumeNoEviction`、デフォルト `false`)を設定すること。これはポリシーを読めないときにだけ読まれる: サーバーが答えたポリシーが常に優先するので、`allkeys-*` や `volatile-*` のサーバーを通すことはない。
62
+
61
63
  **ロードバランサ配下では `HTTP_TRUST_PROXY` をプロキシのアドレスに設定すること。** デフォルトは `false` で、この場合 `req.ip` はクライアントではなく*ロードバランサの*アドレスになる。すると IP をキーとするすべての rate limit — OAuth エンドポイントの limiter と `POST /session/login` の総当たり対策ガード — が**全ユーザーで 1 つのバケットを共有する**: 誰からであれ最初の 20 回のログイン試行でウィンドウを使い切り、以降のユーザーは全員 `429` を受け取る。この障害は設定ミスではなく攻撃のように見えるため、深夜 3 時の診断が高くつく。このサービスの前段で何かが TLS を終端したりプロキシしたりしているなら必ず設定し、`X-Forwarded-For` を設定しているのがそのホップであることを確認すること。`ip` のない監査イベントは、リクエストのアドレスがアドレスでなかった — `req.ip` がアドレス以外を入れた `X-Forwarded-For` から来た — ことを意味するので、`HTTP_TRUST_PROXY` がそのヘッダーを設定するホップだけを信頼しているか確認すること。
62
64
 
63
65
  取りうる形は 4 つで、いずれも Express 自身のものである:
@@ -73,7 +75,11 @@ my-app:
73
75
 
74
76
  allowlist はネットワーク上の制御であって、暗号学的な制御ではない。エッジは受信した `X-Forwarded-*` ヘッダーに追記するのではなく**除去**しなければならず、エッジとこのプロセスの間のホップは、送信元アドレスを偽装できる者から到達できてはならない。
75
77
 
76
- **`adapters.rateLimiter` を Redis に向けること。** デフォルトは `"memory"` でプロセスごとである: N レプリカでは設定したすべての limit が実質 N 倍になり、デプロイのたびにリセットされる。memory アダプターは**バケット枯渇によって回避可能**でもある: バケット数の上限を 10,000 とし、上限に達すると、新しいキーを受け入れる際にリセットが最も近いバケットを追い出す — そのため多数の送信元 IP を提示できる攻撃者(`HTTP_TRUST_PROXY` が実際のホップより広ければ、`req.ip` はクライアントの影響を受ける)は、標的のカウンターが追い出されてやり直しになるまでテーブルをかき回せる。これは開発用の 1 プロセスなら許容できるが、本番の rate limit ではない。ログインガードも同じ共有コンポーネントの上で動くため、1 つの設定で OAuth エンドポイントと `/session/login` の両方がカバーされる。ログインのウィンドウと上限は引き続き `session.rateLimit.login` で設定する。session モジュールがそれを両アダプターが読む `login` の予算として寄与するため、改めて書き直すものは無い。
78
+ **`adapters.rateLimiter` を Redis に向けること。** デフォルトは `"memory"` でプロセスごとである: N レプリカでは設定したすべての limit が実質 N 倍になり、デプロイのたびにリセットされる。memory アダプターは**バケット枯渇によって回避可能**でもある: バケット数の上限を 10,000 とし、上限に達すると、新しいキーを受け入れる際にリセットが最も近いバケットを追い出す — そのため多数の送信元 IP を提示できる攻撃者(`HTTP_TRUST_PROXY` が実際のホップより広ければ、`req.ip` はクライアントの影響を受ける)は、標的のカウンターが追い出されてやり直しになるまでテーブルをかき回せる。これは開発用の 1 プロセスなら許容できるが、本番の rate limit ではない。`/session/login` はこのコンポーネントの上では動かず、device grant を加えたときの `POST /oauth/device/verification` も同じである: それぞれ自身の試行上限(`session.rateLimit.login`、`device-grant.rateLimit`)は試行カウンターの上で数えられ、リミッターの設定はこれを変えない。
79
+
80
+ **MFA ルートの予算はリミッターのものである。** MFA パッケージは自身のルートの予算を持たない: `/session/mfa` への POST はすべて、配線されたリミッターだけによって、クライアントアドレスごとにプレフィックス `mfa` の下で制限される。`config/reference.conf` は両方のリミッターに `limits.mfa { limit = 60, windowSeconds = 300 }`(`core-rate-limiter-memory.limits.mfa`、`redis-rate-limiter.limits.mfa`)を与えるので、`adapters.rateLimiter` を切り替えてもこの予算は保たれる。そこでは既定値なので、構成が配線しないリミッターがこれを持っていても、boot はそのセクションの名前を出さない。変えるなら、配線したリミッターについて `config/application.conf` か自分のレイヤーで変えること。エントリがなければリミッターの `defaultLimit`(60 秒あたり 60)が適用される。MFA のロック — トランザクションごとの試行回数、バックオフ、週ごとの失敗回数 — は MFA パッケージ自身のもので、リミッターの設定はこれを変えない。
81
+
82
+ **`adapters.attemptCounter` を Redis に向けること。** デフォルトは `"memory"` でカウンターを配線しない: ログイン(device grant を加えたときはデバイス検証も)は試行をプロセスごとに数えるので、N レプリカでは `session.rateLimit.login`(と `device-grant.rateLimit`)が上限の N 倍を許し、デプロイのたびにリセットされる。`CORE_DEPLOYMENT_MODE=multi` は起動を拒否する。`"redis"`(`ADAPTERS_ATTEMPT_COUNTER=redis`)は共有の Redis ソケットの上に `redisAttemptCounterModule` を入れる。その Redis は `maxmemory-policy noeviction`(Redis のデフォルト)で動かすこと: 追い出された窓は数え直しになるからである。モジュールはほかのポリシーでは起動を拒否する。サーバーがポリシーを答えないときも、`REDIS_CLIENTS_ASSUME_NO_EVICTION=true`(上記)でない限り拒否する。どちらも閉じる側に倒れる: カウンターが答えられない間、`/session/login` と `POST /oauth/device/verification` は `redis-rate-limiter.failMode` が何であっても `503` を返す。
77
83
 
78
84
  **BFF の背後では、必要になる前に `limits.token` を上げておくこと。** OAuth エンドポイントの rate limit は `req.ip` をキーにする(`packages/core/src/ratelimit/guard.mts` はバケットキーを `<endpoint>:ip:<req.ip>` として組み立てる)。クライアントがブラウザやネイティブアプリで、このプロバイダーと直接通信しているなら、これは正しい identity である。しかし backend-for-frontend 構成 — サーバー側アプリがセッションを保持し、ユーザーに代わってコード交換と refresh を行う構成 — では誤った identity になる: すべての `/oauth/token` と `/oauth/introspect` の呼び出しが BFF の単一アドレスから届き、デプロイ全体で 1 つのバケットを共有する。デフォルトの 60 秒あたり 60 リクエストでは、**全ユーザー合計で毎分およそ 60 回の session グラント交換**が上限になる — しかもそれは rate limit として表に出てこない。BFF は想定していない `429` を受け取り、それを自分の呼び出し元への `502` に変え、ユーザーが報告する症状は「サインインがときどき壊れる」になる。それは起き始めるトラフィック量に達した時点で現れ、それより前には現れない。
79
85
 
@@ -94,9 +100,9 @@ redis-rate-limiter {
94
100
 
95
101
  デフォルトは意図的に変えていない: 送信元 IP ごとに 60 秒あたり 60 回というのは、クライアントが本当に別々の IP であるデプロイにとって妥当な総当たり対策の上限であり、一方の構成に合わせて全体的に引き上げれば、もう一方の構成での防御が弱まる。BFF *自身の*ユーザーを守るスロットリングは、ユーザーごとの identity がまだ存在する BFF の前段に置くこと。
96
102
 
97
- **2 つ以上のレプリカを動かすようになったら `CORE_DEPLOYMENT_MODE=multi` を設定すること。** すると、共有が必要な in-memory ストアがまだ配線されていれば起動が*失敗*し、該当するものすべてと、それぞれの代償が名指しされる — ユーザーセッションの分岐(back-channel logout が 1 つのレプリカにしか届かず、ログアウトしたセッションが他のレプリカでは有効なまま)、rate limit カウンターの倍増、アクセストークン失効の未伝播、一度きりのクライアントアサーションや WebAuthn チャレンジがレプリカごとに 1 回ずつ再利用できてしまうこと。このチェックはライブラリのモジュール名のリストではなく、インストールされた各モジュールが自身の manifest に持つ宣言を読むため、このテンプレート独自の in-memory モジュール — ユーザーセッションストア(`ADAPTERS_USER_SESSION_STORES=memory`)、認可コードリポジトリ(`ADAPTERS_CODE_REPOSITORY=memory`)、フェデレーショントークンストア(`ADAPTERS_FEDERATION_TOKEN_STORE=memory`、デフォルト) — も名指しで拒否される。`SESSION_STORE_STORAGE_TYPE=memory` のときの express-session 自身のストア(#474)と、デフォルトの memory の rate limiter(`core-rate-limiter-memory`、`ADAPTERS_RATE_LIMITER=memory`)も同様である。モードが未設定なら何も拒否されない: これらはすべて、起動時の 1 件の `replica_unsafe_adapters` 警告に列挙される。(login と WebAuthn-options のルートは、それぞれ個別に警告するプロセス単位のフォールバック limiter を持つが、それが働くのは `rateLimiter` をまったく配線しない構成だけで、このテンプレートは常に配線する。オペレーター runbook を参照。)`CORE_DEPLOYMENT_MODE=single` ではチェックは何も言わない。レプリカは 1 つだと宣言したからである。このテンプレートは DPoP をインストールしない。DPoP を加えた構成では、受け入れた DPoP proof はすべて `private_key_jwt` と同じ replay seen-set(`ADAPTERS_REPLAY_SEEN_SET`)に記録されるため、同じ扱いを受ける — `memory` は `CORE_DEPLOYMENT_MODE=multi` のもとで拒否され、モード未設定なら警告に列挙される。同梱の `redis` なら DPoP の記録もレプリカ間で共有される。dpop パッケージの [operator requirements](../../packages/dpop/README.md#operator-requirements) を参照。
103
+ **2 つ以上のレプリカを動かすようになったら `CORE_DEPLOYMENT_MODE=multi` を設定すること。** すると、共有が必要な in-memory ストアがまだ配線されていれば起動が*失敗*し、該当するものすべてと、それぞれの代償が名指しされる — ユーザーセッションの分岐(back-channel logout が 1 つのレプリカにしか届かず、ログアウトしたセッションが他のレプリカでは有効なまま)、rate limit カウンターの倍増、アクセストークン失効の未伝播、一度きりのクライアントアサーションや WebAuthn チャレンジがレプリカごとに 1 回ずつ再利用できてしまうこと。このチェックはライブラリのモジュール名のリストではなく、インストールされた各モジュールが自身の manifest に持つ宣言を読むため、このテンプレート独自の in-memory モジュール — ユーザーセッションストア(`ADAPTERS_USER_SESSION_STORES=memory`)、認可コードリポジトリ(`ADAPTERS_CODE_REPOSITORY=memory`)、フェデレーショントークンストア(`ADAPTERS_FEDERATION_TOKEN_STORE=memory`、デフォルト) — も名指しで拒否される。`SESSION_STORE_STORAGE_TYPE=memory` のときの express-session 自身のストア(#474)と、デフォルトの memory の rate limiter(`core-rate-limiter-memory`、`ADAPTERS_RATE_LIMITER=memory`)も同様である。モードが未設定なら何も拒否されない: これらはすべて、起動時の 1 件の `replica_unsafe_adapters` 警告に列挙される。(WebAuthn-options のルートは個別に警告するプロセス単位のフォールバック limiter を持つが、それが働くのは `rateLimiter` をまったく配線しない構成だけで、このテンプレートは常に配線する。ログインは `ADAPTERS_ATTEMPT_COUNTER=memory` のときプロセスごとに数える: `multi` ではログインを名指しして起動が拒否され、モード未設定なら `attempt_counter_not_shared` を警告する。オペレーター runbook を参照。)`CORE_DEPLOYMENT_MODE=single` ではチェックは何も言わない。レプリカは 1 つだと宣言したからである。このテンプレートは DPoP をインストールしない。DPoP を加えた構成では、受け入れた DPoP proof はすべて `private_key_jwt` と同じ replay seen-set(`ADAPTERS_REPLAY_SEEN_SET`)に記録されるため、同じ扱いを受ける — `memory` は `CORE_DEPLOYMENT_MODE=multi` のもとで拒否され、モード未設定なら警告に列挙される。同梱の `redis` なら DPoP の記録もレプリカ間で共有される。dpop パッケージの [operator requirements](../../packages/dpop/README.md#operator-requirements) を参照。
98
104
 
99
- この変数は `core.deployment.mode` を設定する。旧名の `DEPLOYMENT_MODE` は、単独で、または `CORE_DEPLOYMENT_MODE` と異なる値で設定されているとブートを拒否し、同じ値で並べて設定されていればブートする。
105
+ この変数は `core.deployment.mode` を設定する。旧名の `DEPLOYMENT_MODE` は、設定されていれば、単独でも `CORE_DEPLOYMENT_MODE` と並べても、値が何であれブートを拒否する。外すこと。
100
106
 
101
107
  このチェックに*できない*ことも把握しておくこと: `CORE_DEPLOYMENT_MODE` を一度も設定しないまま N レプリカにスケールしても、何も失敗しない。すべての状態を自分のメモリに持つプロセスには、他のピアに気づくための共有媒体が無い — その状態は、まさにそれが真であるときに内側からは検出できない。この変数はスケールの一環として設定し、何かが壊れてからにしないこと。
102
108
 
@@ -204,9 +210,9 @@ pnpm run start
204
210
 
205
211
  1. **`config/{ENV}.conf`** — 現在の環境の overlay。`ENV = CONFIG_ENV || NODE_ENV || "development"` で決まる。
206
212
  2. **`config/application.conf`** — このデプロイの設定。
207
- 3. **読み込むモジュールの各パッケージの `reference.conf`**、その次に **`@o3co/auth-provider-core` のもの** — デフォルト。インストール済みパッケージから解決される: 各モジュールが自分のパッケージのファイルを宣言し、core の `moduleReferences(modules)` がそれを core のものを最後にして列挙する。テンプレート自身のモジュールは `config/reference.conf` を宣言し、そこには自身が所有するセクション — `logging`、`http`(その CORS のリスト `http.cors` を含む)、`key-store`、共有 Redis 接続の `redis-clients`、`repositories`、in-process のコードリポジトリの `standalone-in-memory-code-repository`、`audit-sink` — と、composition root 自身の `adapters` と `mfaMode`(下記)のデフォルトがある。デプロイ固有の値はそこではなく上の 2 ファイルに書く。`config/reference.conf` はこれらのキーの変数をデフォルトの横で束縛するので、上の 2 ファイルのどちらかが設定する値は変数に勝つ。ただし `HTTP_PORT`、`HTTP_TRUST_PROXY`、`HTTP_CORS_ALLOWED_ORIGINS`、`REDIS_CLIENTS_URL` / `REDIS_CLIENTS_PASSWORD` は `application.conf` が末尾の行でもう一度束縛するので、それぞれ、そのファイルがそれより上で設定する値に勝つ。上の 2 ファイルのどちらも設定していないキーは、ここから値を得る。
213
+ 3. **読み込むモジュールの各パッケージの `reference.conf`**、その次に **`@o3co/auth-provider-core` のもの** — デフォルト。インストール済みパッケージから解決される: 各モジュールが自分のパッケージのファイルを宣言し、core の `moduleReferences(modules)` がそれを core のものを最後にして列挙する。テンプレート自身のモジュールは `config/reference.conf` を宣言し、そこには自身が所有するセクション — `logging`、`http`(その CORS のリスト `http.cors` を含む)、`key-store`、共有 Redis 接続の `redis-clients`、`repositories`、in-process のコードリポジトリの `standalone-in-memory-code-repository`、`audit-sink` — と、composition root 自身の `adapters` と `mfaMode`(下記)のデフォルト、そして両方のレートリミッターのセクションにある MFA ルートの予算(`core-rate-limiter-memory.limits.mfa`、`redis-rate-limiter.limits.mfa`。[マルチレプリカ構成](#マルチレプリカ構成) を参照)がある。デプロイ固有の値はそこではなく上の 2 ファイルに書く。`config/reference.conf` はこれらのキーの変数をデフォルトの横で束縛するので、上の 2 ファイルのどちらかが設定する値は変数に勝つ。ただし `HTTP_PORT`、`HTTP_TRUST_PROXY`、`HTTP_CORS_ALLOWED_ORIGINS`、`REDIS_CLIENTS_URL` / `REDIS_CLIENTS_PASSWORD` は `application.conf` が末尾の行でもう一度束縛するので、それぞれ、そのファイルがそれより上で設定する値に勝つ。上の 2 ファイルのどちらも設定していないキーは、ここから値を得る。
208
214
 
209
- 上の 2 ファイルは、環境変数の一つのスナップショットのもとで一度だけ読み(`readOwnLayers`)、その一度の読み込みから二つの段階を組み立てる — 起動中にファイルが置き換えられても、変数が変わっても、boot がモジュールを選んだものと違うものをパースすることはない。読み込みは二段階で行う([#728](https://github.com/o3co/auth.provider/issues/728)、[`src/configPath.mts`](src/configPath.mts))。まず、モジュールを知る前に、`buildModules` がモジュールを選ぶスイッチ — フェデレーション、機能 — と、期待するセッション要件を導く元の `sessionRequirements` を、上の 2 ファイルを core の `reference.conf` だけの上に重ねて読む(`readSwitches`、core の transitional reader で読む)。パースするのはそれらのパス(`SWITCHES`)だけである。`adapters` — 各スロットをどのアダプターで埋めるか、composition root 自身のセクション — は、上の 2 ファイルをテンプレートの `config/reference.conf` の上に重ね、テンプレート自身のスキーマで読む(`readAdapters`): 移動する前のパスにまだ書かれた選択や、それとともに改名された変数は、どのモジュールを選ぶよりも前にここで拒否され、新しいパスと変数を名指しする。composition root 自身のキーである MFA のスイッチ `mfaMode`(`MFA_MODE`)も同じように、テンプレート自身のスキーマで読み(`readMfaSwitch`)、それと並んで、Store を使う MFA 要素ストアの元になるユーザーリポジトリの HTTP 設定をパースせずに読む。boot には `adapters` も `mfaMode` も渡さない。パッケージの `reference.conf` だけが設定するものはこの段階では見えない — まだどれも重ねていない — ので、組み立て時に設定を読むモジュールを `buildModules` に加えるなら、そのモジュールが読むパスを `SWITCHES` に加える。ログレベルも boot の前に読むが、それは `logging` モジュールのセクションとして、そのモジュールのスキーマで、テンプレートの `reference.conf` の上に読む(`readLogging`): テンプレートは設定を読みモジュールを選ぶ間もログを出すので、logger は boot の前に存在する。次に、読み込むすべてのモジュールの `reference.conf` の上に解決した設定を、第一段階で導いたセッション要件を書き込み、`mfa` セクションは MFA のスイッチが決めるとおりにして([多要素認証](#多要素認証) を参照)、パースせずに `createApp` に渡す(`resolveForBoot`): boot はそれを、読み込まれたすべてのモジュールのスキーマで一度だけパースする。boot の後にテンプレートが読むもの — 信頼するホップ、ポート、readiness の期限 — は、`http` モジュールから読む: core の `httpSettings` スロットと、テンプレートの `httpHostSettings` である。読み込まれたどのモジュールも所有しないトップレベルのセクションは残され、boot 時に `config_sections_ignored` として一度だけログに出る: セクション名の綴り間違いはここに現れる。
215
+ 上の 2 ファイルは、環境変数の一つのスナップショットのもとで一度だけ読み(`readOwnLayers`)、その一度の読み込みから二つの段階を組み立てる — 起動中にファイルが置き換えられても、変数が変わっても、boot がモジュールを選んだものと違うものをパースすることはない。読み込みは二段階で行う([#728](https://github.com/o3co/auth.provider/issues/728)、[`src/configPath.mts`](src/configPath.mts))。まず、モジュールを知る前に、`buildModules` がモジュールを選ぶもの — フェデレーションのエントリは決して読まない — を、上の 2 ファイルをテンプレートの `config/reference.conf` だけの上に重ねて読む(`readSwitches`)。`adapters` — 各スロットをどのアダプターで埋めるか、composition root 自身のセクション — は、テンプレート自身のスキーマで読む(`readAdapters`): 移動する前のパスにまだ書かれた選択や、それとともに改名された変数は、どのモジュールを選ぶよりも前にここで拒否され、新しいパスと変数を名指しする。composition root 自身のキーである MFA のスイッチ `mfaMode`(`MFA_MODE`)も同じように、テンプレート自身のスキーマで読み(`readMfaSwitch`)、それと並んで、Store を使う MFA 要素ストアの元になるユーザーリポジトリの HTTP 設定をパースせずに読む。フェデレーショングラントを組み込むかどうかは `federation-grants.enabled`(`FEDERATION_GRANTS_ENABLED`。このために `application.conf` がバインドする)から、core の `coerceBooleanFromEnv` で読む: `false` と読めない値はフェデレーショングラントのモジュールを組み込むので、不正な値でも機能がオフと読まれることはなく、boot が `federation-grants.enabled` で拒否する。セクションの古いパス `federationGrants` に書かれた設定も同じく組み込み、boot が新しいパスを名指しして拒否する。boot には `adapters` も `mfaMode` も渡さない。第一段階はこれ以外のキーを読まず、パッケージの `reference.conf` だけが設定するものも見えない — まだどれも重ねていない。`buildModules` に加えたモジュールは、自分のセクションを boot 時に読む(`deps.section`)のであって、組み立て時には読まない。ログレベルも boot の前に読むが、それは `logging` モジュールのセクションとして、そのモジュールのスキーマで、テンプレートの `reference.conf` の上に読む(`readLogging`): テンプレートは設定を読みモジュールを選ぶ間もログを出すので、logger は boot の前に存在する。テンプレートが boot の前に行う設定の拒否 — 第一段階での `adapters`・`mfaMode`・`logging`、および下で boot に渡すものを組み立てるとき(`resolveForBoot`)— は、boot が同じ場合に使う reason(`config-path-relocated`、`environment-variable-renamed`、`config-validation-failed`、`module-section-path-invalid`)を持つ `BootError` として投げる([`src/bootRefusal.mts`](src/bootRefusal.mts))ので、boot エラーの reason に対するアラートはこれも捉える。起動の失敗がすべて reason を持つわけではない: `config/` の外のファイルを指す `CONFIG_ENV` は素の `Error`、存在しない `{ENV}.conf` や HOCON がパースできないファイルは HOCON ライブラリのエラー、boot の後の失敗(リスナーがバインドできないポートなど)はそれ自身のエラーである。次に、読み込むすべてのモジュールの `reference.conf` の上に解決した設定を、MFA のスイッチが期待するとおりの `core.sessionRequirements` — スイッチが MFA を組み込むときは設定のリストに `mfa` を加えたもの。名前のリストでない値は書かれたまま渡し、boot が拒否する — を書き込み、`mfa` セクションは MFA のスイッチが決めるとおりにして([多要素認証](#多要素認証) を参照)、パースせずに `createApp` に渡す(`resolveForBoot`): boot はそれを、読み込まれたすべてのモジュールのスキーマで一度だけパースする。boot の後にテンプレートが読むもの — 信頼するホップ、ポート、readiness の期限 — は、`http` モジュールから読む: core の `httpSettings` スロットと、テンプレートの `httpHostSettings` である。それと並んで、テンプレートは設定の既定値 — 同じ `reference.conf` 群を、上の 2 ファイルも環境変数もなしで解決したもの(`configDefaultsFor`)— を boot に渡す。読み込まれたどのモジュールも所有しないトップレベルのセクションは残され、boot はそれをこの既定値で区別して、一度だけ warn で名前を出す: 読み込まれたパッケージの `reference.conf` が、構成が読み込まないモジュールのために設定するセクション(たとえば Redis 上のストアがないときの `redis-clients`)は、そのファイルが設定するとおりのままなら名前が出ず、ファイルか変数が変えると `config_sections_not_loaded` — その設定はどこにも届かない — になる。読み込まれたどのパッケージも設定しないセクションは `config_sections_ignored` で、セクション名の綴り間違いはここに現れる。読み込まれたパッケージの `reference.conf` が捕捉する改名済みの変数のうち、読み込まれたどのモジュールも宣言しないものは `environment_variables_not_applied` として名前が出る。それぞれの行は [オペレーター runbook](../../docs/operator-runbook.md) にある。
210
216
 
211
217
  overlay の値は `application.conf` より優先される。scaffold には `development.conf` と `production.conf`、そして `mailpit.conf` が同梱されている。`mailpit.conf` は、[Docker](#docker) の Mailpit オーバーレイが選ぶ名前のもとでの development の overlay である。別の環境(例: `staging`)を追加するときは `config/staging.conf` を作成し、`CONFIG_ENV=staging` を設定する。`{ENV}.conf` が存在しない場合は起動時エラーになる — タイポは黙ってデフォルトにフォールバックせず、fail-fast する。
212
218
 
@@ -217,6 +223,7 @@ overlay の値は `application.conf` より優先される。scaffold には `de
217
223
  | 変数 | デフォルト | 説明 |
218
224
  |---|---|---|
219
225
  | `ADAPTERS_RATE_LIMITER` | `memory` | `adapters.rateLimiter`: `memory` または `redis`。[マルチレプリカ構成](#マルチレプリカ構成) を参照 |
226
+ | `ADAPTERS_ATTEMPT_COUNTER` | `memory` | `adapters.attemptCounter`: ログインの試行上限(device grant を加えたときはデバイス検証のものも)を数えるカウンター。`memory`(配線しない: プロセスごと)または `redis`。[マルチレプリカ構成](#マルチレプリカ構成) を参照 |
220
227
  | `ADAPTERS_USER_SESSION_STORES` | `memory` | `adapters.userSessionStores`: ユーザーセッションストア、`memory` または `redis`。`SESSION_STORE_STORAGE_TYPE` と揃える([Docker](#docker) を参照) |
221
228
  | `ADAPTERS_ACCESS_TOKEN_DENYLIST` | `redis` | `adapters.accessTokenDenylist`: `memory` または `redis` |
222
229
  | `ADAPTERS_REPLAY_SEEN_SET` | `redis` | `adapters.replaySeenSet`: `private_key_jwt` の背後の replay seen-set、`memory` または `redis` |
@@ -231,7 +238,7 @@ overlay の値は `application.conf` より優先される。scaffold には `de
231
238
  | `ADAPTERS_USER_REPOSITORY` | `http` | `adapters.userRepository`: `http`、`yaml` または `static`(core の `yaml` の別名)。[ユーザーリポジトリ](#ユーザーリポジトリ) を参照 |
232
239
  | `ADAPTERS_AUDIT_SINK` | `logger` | `adapters.auditSink`: セキュリティイベントの送り先、`logger`(stdout への pino エンベロープ付き NDJSON)または `console`(イベントの JSON そのまま)。`none` は存在しない。[監査ログ](#監査ログ) を参照 |
233
240
 
234
- 移動する前の場所 — `rateLimiter.adapter`、`oauth.code.adapter`、`audit.sink.type`、`repositories.user.type` など — にまだ書かれた選択や、それとともに改名された変数(`RATE_LIMITER_ADAPTER`、`CLIENT_TYPE` など)が単独で、または別の値で設定されていると、新しいパスと変数を名指しして boot の前に失敗する。古い変数が新しい変数と同じ値で並んでいるなら受け入れる。
241
+ 移動する前の場所 — `rateLimiter.adapter`、`oauth.code.adapter`、`audit.sink.type`、`repositories.user.type` など — にまだ書かれた選択や、それとともに改名された変数(`RATE_LIMITER_ADAPTER`、`CLIENT_TYPE` など)が設定されていれば、単独でも新しい変数と並べても、値が何であれ、新しいパスと変数を名指しして boot の前に失敗する。古い変数は外すこと。
235
242
 
236
243
  ### HTTP
237
244
 
@@ -290,13 +297,14 @@ openssl pkey -in jwt-private.pem -pubout -out jwt-public.pem
290
297
  |---|---|---|
291
298
  | `OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN` | `3600` | リクエストが有効期間を指定しないときに全グラントが発行するアクセストークンの有効期間(秒)。正の整数、上限は 1 年(`31536000`)。 |
292
299
  | `OAUTH_ACCESS_TOKEN_MAX_EXPIRES_IN` | デフォルトと同じ | token exchange リクエストの `expires_in` パラメータで得られる上限。超えるリクエストはこの値に切り詰められる。未設定ならデフォルトと同じで、これを設定しない限りどのトークンも延長されない。デフォルトがこれを超えると、両キーを名指しして起動に失敗する。オフラインで検証するリソースサーバーに対し、交換で発行されたトークンが失効後も通用し得る期間の上限でもある。 |
293
- | `OAUTH_ACCESS_TOKEN_EXPIRES_IN` | — | `OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN` の**非推奨(deprecated)**エイリアス(config キーでは `oauth.accessToken.expiresIn` が `oauth.accessToken.defaultExpiresIn` のエイリアス)。新しい変数が未設定の間だけ読まれる。これがまだデフォルトを決めている間は、起動時に `config_key_deprecated`(warn)がログに出る。値は新しい変数へ移すこと。 |
294
300
  | `OAUTH_REFRESH_TOKEN_EXPIRES_IN` | `86400` | リフレッシュトークンの有効期間(秒)。正の整数、上限は 1 年(`31536000`)。 |
295
301
 
296
302
  どれも 10 進数字だけを読む。空文字で export すると、フォールバックではなく起動失敗になる(HOCON は `FOO=` を `""` に解決する)。16 進(`0x10`)、指数(`1e3`)、符号(`+5`)、小数(`5.0`)も同じく起動失敗になる。
297
303
 
298
304
  `expires_in` リクエストパラメータを読むのは token exchange(RFC 8693)だけで、他のグラントはそれを無視してデフォルトを発行する。
299
305
 
306
+ デフォルトの旧名 `OAUTH_ACCESS_TOKEN_EXPIRES_IN`(config キーでは `oauth.accessToken.expiresIn`)は、設定されていれば、単独でも `OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN` と並べても、値が何であれブートを拒否する。旧キーは値が何であれブートを拒否する。値は消さずに新しい名前へ移すこと: 消すとデフォルトは `3600` になる。
307
+
300
308
  ### グラントタイプ
301
309
 
302
310
  | 変数 | デフォルト | 説明 |
@@ -307,7 +315,7 @@ openssl pkey -in jwt-private.pem -pubout -out jwt-public.pem
307
315
  | `OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED` | `false` | client credentials グラントタイプを有効化 |
308
316
  | `OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED` | `false` | jwt-bearer グラントタイプ(RFC 7523)を有効化 |
309
317
 
310
- スイッチはそれぞれモジュールのキー — `oauth-session.enabled`、ほかは `oauth-authorization.grants.<grant>.enabled` — で、テンプレートはモジュールを選ぶために boot の前に自分のファイルと環境変数からこれを読む。旧名の `OAUTH_GRANTS_<GRANT>_ENABLED` は、単独で、または新名と違う値で設定されていると boot を拒否し、同じ値なら boot する。
318
+ スイッチはそれぞれモジュールのキー — `oauth-session.enabled`、ほかは `oauth-authorization.grants.<grant>.enabled` — である。どちらのモジュールも常に読み込まれ、自分のスイッチを boot がパースする設定から boot で読む。テンプレートはそのどれも boot の前には読まない。旧名の `OAUTH_GRANTS_<GRANT>_ENABLED` は、設定されていれば、単独でも新名と並べても、値が何であれ boot を拒否する。
311
319
 
312
320
  ### Session
313
321
 
@@ -324,7 +332,7 @@ openssl pkey -in jwt-private.pem -pubout -out jwt-public.pem
324
332
  | `SESSION_STORE_STORAGE_REDIS_URL` | `redis://localhost:6379` | セッションストア用 Redis 接続 URL |
325
333
  | `SESSION_STORE_STORAGE_REDIS_PASSWORD` | — | セッションストア用 Redis パスワード |
326
334
 
327
- cookie とそのストアはセッションストアのセクション `session-store` のもので、CSRF トークンの寿命、ログインページ、ログインの予算は session モジュールのセクション `session` のもの。`SESSION_STORE_*` の旧名 `SESSION_<KEY>` は、単独で、または新名と違う値で設定されていると boot を拒否し、同じ値なら boot する。
335
+ cookie とそのストアはセッションストアのセクション `session-store` のもので、CSRF トークンの寿命、ログインページ、ログインの予算は session モジュールのセクション `session` のもの。`SESSION_STORE_*` の旧名 `SESSION_<KEY>` は、設定されていれば、単独でも新名と並べても、値が何であれ boot を拒否する。
328
336
 
329
337
  ローカルの HTTP 開発のために `SESSION_STORE_SECURE=false` を設定する場合や、ドメインを共有する Cookie のために `SESSION_STORE_DOMAIN` を設定する場合は、`SESSION_STORE_NAME` も `auth.sid` のような `__Host-` でない値にすること — `SESSION_STORE_SECURE=false` なら接頭辞の無い値にする: `__Secure-` の名前も `SESSION_STORE_SECURE=true` を要する。`__Host-` や `__Secure-`(大文字小文字は問わない)の Cookie 名が、ブラウザがその prefix に対して拒否する属性と組み合わされると、`SESSION_STORE_NAME` が Cookie の名前(RFC 6265 のトークン: 空白、`;` などの区切り文字を含まない)でないと、また `SESSION_STORE_DOMAIN` がホスト名でない(スキーム、ポート、パスを含む)と、サーバーは fail-fast する。
330
338
 
@@ -373,7 +381,25 @@ http.cors {
373
381
 
374
382
  ### Google フェデレーション
375
383
 
376
- フェデレーションは core のものである: `core.federations`、各フェデレーションに到達する名前(`/session/oauth/federation/<name>`)をキーとする 1 つのマップ。各キーは、そのパスから名付けた変数 `CORE_FEDERATIONS_<NAME>_<KEY>` に束縛される。トップレベルに書いたマップ(`federations { ... }`)は、各キーの `core.federations` の下のパスを名指しして起動を拒否する。`FEDERATIONS_GOOGLE_*` や `FEDERATIONS_OIDC_*` の変数が単独で、または新しい名前と異なる値で設定されていると、どのモジュールを選ぶよりも前に拒否される。
384
+ フェデレーションは core のものである: `core.federations`、各フェデレーションに到達する名前(`/session/oauth/federation/<name>`)をキーとする 1 つのマップ。各キーは、そのパスから名付けた変数 `CORE_FEDERATIONS_<NAME>_<KEY>` に束縛される。トップレベルに書いたマップ(`federations { ... }`)は、各キーの `core.federations` の下のパスを名指しして起動を拒否する。`FEDERATIONS_GOOGLE_*` や `FEDERATIONS_OIDC_*` の変数が設定されていれば、単独でも新しい名前と並べても、値が何であれ、どのモジュールを選ぶよりも前に拒否される。
385
+
386
+ すべてのエントリは自分の `type` を書き、boot は有効化された各エントリを、その type を扱うモジュールにエントリの名前で渡す。テンプレートは同梱する 2 つの type — `google`(`@o3co/auth-provider-federation-google`)と `oidc`(`@o3co/auth-provider-federation-oidc`)— を、マップの内容にかかわらず常に読み込み、エントリを自分では読まない: エントリが持つキーは core と type のスキーマが読み、不正なものはそのパス(`core.federations.<name>.<key>`)を名指しして拒否される。`type` の無いエントリは、有効かどうかにかかわらず `core.federations.<name>.type` で拒否される(`config-validation-failed`)。読み込んだどのモジュールも扱わない type の有効なエントリは、そのエントリを名指しして起動を拒否する(`federation-type-unhandled`)。GitHub と Apple は同梱しない: 使いたいデプロイは、そのパッケージとモジュールを `buildModules` に加える — GitHub は type モジュール `githubFederationTypeModule()` と `type = "github"` のエントリで、Apple はその [README](../../packages/federation-apple/README.md) に従う。
387
+
388
+ `core.federations.google` は `type = "google"` を書いて同梱されている。2 つ目の Google クライアントは、同じ type の 2 つ目のエントリを、独自の名前と独自のコールバックで書く:
389
+
390
+ ```hocon
391
+ core.federations {
392
+ google-work {
393
+ enabled = true
394
+ type = "google"
395
+ clientId = ${GOOGLE_WORK_CLIENT_ID}
396
+ clientSecret = ${GOOGLE_WORK_CLIENT_SECRET}
397
+ callbackURL = "https://auth.example.com/session/oauth/federation/google-work/callback"
398
+ }
399
+ }
400
+ ```
401
+
402
+ `google` のエントリが下の変数のほかに受け付けるキー — `redirectAllowlist`、`sessionDomain`、`authCallbackUrl`、`clientUrl`、`requireAuthorizationResponseIss`、`endSessionEndpoint` — の完全な一覧は [パッケージの README](../../packages/federation-google/README.md) にある。`config/application.conf` は `endSessionEndpoint` 以外のすべてを示す。type が挙げていないキーはエントリを拒否させる。
377
403
 
378
404
  | 変数 | デフォルト | 説明 |
379
405
  |---|---|---|
@@ -411,7 +437,7 @@ core.federations {
411
437
  }
412
438
  ```
413
439
 
414
- エントリの `type` が実装を指定する。`type` の無い `core.federations.google` は組み込みの Google フェデレーションである。`type = "oidc"` を付けると `google` という名前の汎用 OIDC インスタンスになり、組み込みモジュールは合成されない。
440
+ 実装を指定するのはエントリの `type` であり、名前ではない: `type = "oidc"` を書いた `google` という名前のエントリは、`google` という名前の汎用 OIDC フェデレーションである。
415
441
 
416
442
  パッケージが受け付けるすべてのフィールド — `scopes`、`discovery` / `endpoints`、`privateKey`、`userInfo`、`idTokenSignedResponseAlg`、`clockToleranceSeconds` — は [パッケージの README](../../packages/federation-oidc/README.md) に記載されている。
417
443
 
@@ -455,7 +481,7 @@ core.federations {
455
481
  | `REDIS_CODE_REPOSITORY_KEY_PREFIX` | `oauth:code:` | `redis-code-repository.keyPrefix`: Redis のリポジトリのキー名前空間([Redis の名前空間](#redis-の名前空間) を参照) |
456
482
  | `STANDALONE_IN_MEMORY_CODE_REPOSITORY_DEFAULT_EXPIRES_IN` | `600` | `standalone-in-memory-code-repository.defaultExpiresIn`: in-process のリポジトリのもの。正の整数(秒) |
457
483
 
458
- `CLIENT_CODE_DEFAULT_EXPIRES_IN` と `CLIENT_CODE_KEY_PREFIX` はこれらとともに改名され、`CLIENT_CODE_ENDPOINT_URI` と `CLIENT_CODE_PASSWORD` は削除された。改名されたものが単独で、または新しい名前と異なる値で設定されていると、そして削除されたものが設定されていると、代わりに設定すべきものを名指しして起動に失敗する。
484
+ `CLIENT_CODE_DEFAULT_EXPIRES_IN` と `CLIENT_CODE_KEY_PREFIX` はこれらとともに改名され、`CLIENT_CODE_ENDPOINT_URI` と `CLIENT_CODE_PASSWORD` は削除された。改名されたものが、単独でも新しい名前と並べても、値が何であれ設定されていると、そして削除されたものが設定されていると、代わりに設定すべきものを名指しして起動に失敗する。
459
485
 
460
486
  ### フェデレーショントークンストア
461
487
 
@@ -486,7 +512,7 @@ core.federations {
486
512
  | 変数 | デフォルト | 説明 |
487
513
  |---|---|---|
488
514
  | `MFA_MODE` | `required` | `mfaMode`: `required`、`optional` または `off` |
489
- | `MFA_ENCRYPTION_KEY` | — | MFA の鍵リングの最初の鍵で、すべての要素のデータを封じる: 32 バイトの canonical な base64(`openssl rand -base64 32`)。MFA が有効なら必須。`CONFIG_ENV=development` では `config/development.conf` が代わりに MFA パッケージの公開サンプル鍵を置く: 自分の鍵はそこに書く。そのリングの横でこの変数を設定すると boot は拒否される。サンプル鍵は `CONFIG_ENV` か `NODE_ENV` が `production` か `staging` のとき、また `CORE_DEPLOYMENT_MODE=multi` のもとでは拒否される |
515
+ | `MFA_ENCRYPTION_KEY` | — | MFA の鍵リングの最初の鍵で、すべての要素のデータを封じる: 32 バイトの canonical な base64(`openssl rand -base64 32`)。MFA が有効なら必須。`CONFIG_ENV=development` では `config/development.conf` が代わりに MFA パッケージの公開サンプル鍵を置く: 自分の鍵はそこに書く。そのリングの横でこの変数を設定すると boot は拒否される。サンプル鍵が受け入れられるのは、`CONFIG_ENV` と `NODE_ENV` のうち設定されているものがすべて `development` か `test` を示すときだけで、`CORE_DEPLOYMENT_MODE=multi` のもとでは拒否される |
490
516
  | `MFA_PAGE_URL` | `/mfa` | デプロイの MFA ページ。ログインの第二要素とステップアップはここから始まる。テンプレートはページを同梱しない: ページの契約は [MFA パッケージのもの](../../packages/mfa/README.md#the-routes) |
491
517
  | `MFA_STORE_TIMEOUT_MS` | `5000` | `mfa.storeTimeoutMs`、Store の呼び出し 1 回の時間: 各ストアの呼び出しごとのタイムアウト以上でなければならない。Store を呼ぶ構成(`ADAPTERS_USER_REPOSITORY=http`、または要素を Store に置く)で `REPOSITORIES_USER_HTTP_TIMEOUT` を下回ると boot を拒否するので、二つは一緒に上げる。37500 ms を超えても拒否する |
492
518
  | `STANDARD_SMTP_MAIL_SENDER_HOST` | — | development 以外での SMTP リレー。MFA はここにアカウントのメールの証明とメール要素のコードを送る。MFA が有効なら、boot にはこれと `STANDARD_SMTP_MAIL_SENDER_FROM` が要る |
@@ -585,7 +611,7 @@ worker:
585
611
 
586
612
  | 変数 | デフォルト | 説明 |
587
613
  |---|---|---|
588
- | `REDIS_SESSION_STORES_KEY_PREFIX` | `ss:` | ユーザーセッション、RP レジストリ、session-family インデックス、session-federation インデックスの外側の prefix。 |
614
+ | `REDIS_SESSION_STORES_KEY_PREFIX` | `ss:` | ユーザーセッション、subject のセッションインデックス、subject の失効境界、セッションライフサイクルのレコードの外側の prefix。 |
589
615
  | `REDIS_REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX` | `rtfam:` | refresh token family レコードの prefix。 |
590
616
  | `REDIS_CODE_REPOSITORY_KEY_PREFIX` | `oauth:code:` | OAuth 認可コードの prefix。 |
591
617
  | `REDIS_FEDERATION_TOKEN_STORE_KEY_PREFIX` | `ft:` | フェデレーショントークンのレコード、そのセッションごとのインデックス、およびそのロックキーの prefix。 |
@@ -606,15 +632,15 @@ worker:
606
632
 
607
633
  どのモジュールをどの順序で合成するかについては、[`src/buildModules.mts`](src/buildModules.mts) が唯一の信頼できる情報源である — そのコピーではなく、それ自体を読むこと。boot planner は `requires` / `provides` をトポロジカルに解決するが、ルートとミドルウェアは、モジュールが `before` / `after` を宣言しない限りリストの順にマウントする — したがって、何かをマウントするモジュールでは位置が意味を持つ。リストを編集するときに守るべきルールは次のとおり:
608
634
 
609
- 1. **`sessionStoreModuleFor(config)` は先頭のままにする。** これは `express-session` の middleware をマウントし、`before` / `after` を宣言しないため、`req.session` を読むすべてのルートより前に来るのは、リスト内の位置のおかげである。`config` から組み立てるのは、`session-store.storage.type = "memory"` が自らを replica-unsafe と宣言するようにするためである。
610
- 2. **`/oauth` の下では順序は関係しない。** `federationGrantsModules`(フェデレーショングラントが有効な間)、`oauthModule`、独自のモジュールは、いずれも `/oauth` の下にルートをマウントしうる。`oauthModule` のルーターが body をパースするのは自身のルートだけなので、各モジュールへのリクエストは、リストの順に関係なくそのモジュール自身の parser に届く — ただし、`/oauth` 配下のどのモジュールも自分の body を自分でパースし、その parser を自分のパスちょうどに限定している場合に限る(同梱のモジュールはそうしている)。限定はルートとして行う(`router.all(path, parser)` か、ルート自身のハンドラ列)。`router.use(path, parser)` は `path` の下のすべてのパスにもマッチする。フェデレーショングラントのブラウザ側の半分は、自身の `after` によってセッション middleware の後ろに自らを並べる。
635
+ 1. **`sessionStoreModule` は先頭のままにする。** これは `express-session` の middleware をマウントし、`before` / `after` を宣言しないため、`req.session` を読むすべてのルートより前に来るのは、リスト内の位置のおかげである。replica safety は自分のセクションから宣言するので、`session-store.storage.type = "memory"` は replica-unsafe となり、`core.deployment.mode = "multi"` で拒否される。
636
+ 2. **`/oauth` の下では順序は関係しない。** `federationGrantsModules`(フェデレーショングラントが有効な間)、`oauthEndpointsModule`、独自のモジュールは、いずれも `/oauth` の下にルートをマウントしうる。`oauthEndpointsModule` のルーターが body をパースするのは自身のルートだけなので、各モジュールへのリクエストは、リストの順に関係なくそのモジュール自身の parser に届く — ただし、`/oauth` 配下のどのモジュールも自分の body を自分でパースし、その parser を自分のパスちょうどに限定している場合に限る(同梱のモジュールはそうしている)。限定はルートとして行う(`router.all(path, parser)` か、ルート自身のハンドラ列)。`router.use(path, parser)` は `path` の下のすべてのパスにもマッチする。フェデレーショングラントのブラウザ側の半分は、自身の `after` によってセッション middleware の後ろに自らを並べる。
611
637
  3. **ストアスロット 1 つにつきモジュール 1 つ。** 各アダプタースイッチ — `adapters.federationTokenStore`、`adapters.userSessionStores`、`adapters.rateLimiter`、`adapters.codeRepository`、`adapters.accessTokenDenylist`、`adapters.replaySeenSet`、`adapters.consentStore`、フェデレーショングラントの 2 つのストアスイッチ、MFA の 2 つのストアスイッチ — は、memory / Redis の組(MFA の要素については Store も)から 1 つを選ぶ。両者は同じスロットを提供するため、両方を配線すると起動時のスロット衝突になる。`adapters.consentStore = "none"` はどちらも配線せず、フェデレーショングラントのストアは機能が有効な間だけ、MFA のストアは `MFA_MODE` が MFA を組み込む間だけ配線される。
612
638
  4. **共有 Redis 接続は、最初の Redis バックエンドのモジュールとともに加わる。** `standaloneRedisClientsModule` は、ここにあるすべての Redis アダプターが使う 1 本の ioredis 接続を自身のセクション(`redis-clients`)から開き、合成されたモジュールがそれを必要とするときには必ず追加される。同梱の合成では refresh token family ストアが Redis 上にあるため、デプロイには常にこれがある。in-memory の family ストアはテスト用の override(`overrides.refreshTokenFamilyModules`)である。
613
639
  5. **テンプレート自身の設定モジュールは常に合成される。** `loggingModule` と `httpModule` は `logging {}` と `http {}` を所有し、CORS のリストは `http` のキーの一つ(`http.cors.allowedOrigins`)である。`httpModule` は core の `httpSettings` を提供する。これは authoritative なので、モジュールが読み込まれている間は `overrideComponents` のエントリで置き換えられない。`httpModule` はさらに、`app.mts` が boot の後にポートと readiness の期限を読むテンプレートの `httpHostSettings` を提供する。logger はモジュールではなく、boot の前に `logging` セクションから作られ(`readLogging`)、`logger` コンポーネントとして boot に渡される。
614
- 6. **フェデレーションアダプターは、その config bridge とともに加わる。** `googleFederationModule` には `googleFederationConfigModule` が伴い — これは有効化され、かつ `type` が `google` である `core.federations.google` エントリに対してのみで、そのため `google` という名前の `type = "oidc"` セクションが二重に合成されることはない — 有効化された `type = "oidc"` のセクションごとに 1 つずつの `oidcFederationModule(name)` には、それらが共有する 1 つの `oidcFederationConfigModule` が伴う。bridge の provider は対応するセクションが無いと throw するため、この組は内部でゲートされるのではなく、合成時に含めるかどうかが決まる。
615
- 7. **メール送信者は環境に従う。** 設定が `development` として選ばれたところでは `@o3co/auth-provider-standard` の開発用送信者で、これは各コードをログに書く。そのモジュールは、その名前と、設定されていれば `CONFIG_ENV` と `NODE_ENV` のそれぞれが `development` か `test` であるところでだけ入り、それ以外のところ、または `core.deployment.mode` が `multi` のところでは起動を拒否する。それ以外の名前では SMTP 送信者のモジュールで、そのセクションは `standard-smtp-mail-sender`([パッケージの README](../../packages/standard/README.md))。その送信者は `mailSender` スロットを何かが読むところでだけ作られ、そこでは起動に `STANDARD_SMTP_MAIL_SENDER_HOST` と `STANDARD_SMTP_MAIL_SENDER_FROM` が要る。`MFA_MODE` が `off` の間はスロットを読むものが無いので、テンプレートはそれらが無くても起動する。MFA が有効なら、MFA モジュールとオペレーターによるリセットがそれを読む。独自の送信者はこの選択を置き換える: [`src/app.mts`](src/app.mts) が `buildModules` を呼ぶところで、そのモジュールを `buildModules(switches, { environment: env, logger, mailSenderModules: [mySenderModule] })` のように渡す。`mailSenderModules` を渡すと、どの環境名でもそれが入り、同梱の送信者は入らないので、両方がスロットを提供することは無い。空のリストを渡すと送信者は入らず、送信者を必要とするモジュール(MFA のメール要素、または `requireEmailProof = "always"` の MFA モジュール)は起動を拒否する。
640
+ 6. **フェデレーションの type は常に合成される。** `googleFederationTypeModule()` と `oidcFederationTypeModule()` は `core.federations` の内容にかかわらずリストに入る: それぞれが自分の type を提供し、boot はその type の有効なエントリごとに 1 つのフェデレーションを、エントリの名前で組み立てる。したがってフェデレーションの追加と削除は設定だけで済む。リストの中でエントリを読むものは無い。type で振り分けられるエントリと同じ名前のフェデレーションを直接提供するモジュールは、起動時の `duplicate-contribute` になる: 別の種類のフェデレーションは type モジュールとして加え、そのエントリにその type を書く。
641
+ 7. **メール送信者は環境に従う。** 設定が `development` として選ばれたところでは `@o3co/auth-provider-standard` の開発用送信者で、これは各コードをログに書く。そのモジュールは、その名前と、設定されていれば `CONFIG_ENV` と `NODE_ENV` のそれぞれが `development` か `test` であるところでだけ入り、それ以外のところ、または `core.deployment.mode` が `multi` のところでは起動を拒否する。それ以外の名前では SMTP 送信者のモジュールで、そのセクションは `standard-smtp-mail-sender`([パッケージの README](../../packages/standard/README.md))。その送信者は `mailSender` スロットを何かが読むところでだけ作られ、そこでは起動に `STANDARD_SMTP_MAIL_SENDER_HOST` と `STANDARD_SMTP_MAIL_SENDER_FROM` が要る。`MFA_MODE` が `off` の間はスロットを読むものが無いので、テンプレートはそれらが無くても起動する。MFA が有効なら、MFA モジュールとオペレーターによるリセットがそれを読む。独自の送信者はこの選択を置き換える: [`src/app.mts`](src/app.mts) が `buildModules` を呼ぶところで、そのモジュールを `buildModules(switches, { environment: env, mailSenderModules: [mySenderModule] })` のように渡す。`mailSenderModules` を渡すと、どの環境名でもそれが入り、同梱の送信者は入らないので、両方がスロットを提供することは無い。空のリストを渡すと送信者は入らず、送信者を必要とするモジュール(MFA のメール要素、または `requireEmailProof = "always"` の MFA モジュール)は起動を拒否する。
616
642
 
617
- `jwksModule`(core 由来)は常に合成される: トークンに署名するプロバイダーは、issuer が設定されているかどうかにかかわらず検証鍵を公開する。各ルートモジュールが何をマウントするかは、それぞれのパッケージの README にある。合成時に知っておくべき振る舞いが 1 つある: `sessionModule` の `POST /session/logout` は `UserSession` レコード(これにより `/oauth/introspect` と `/oauth/userinfo` はそのセッションから発行されたトークンを受け付けなくなる)、subject インデックス、フェデレーションのエントリを削除する — しかし refresh token family は失効させ**ない**。完全なカスケードを実行するエンドポイントは `POST /oauth/logout` である。[どのログアウトエンドポイントが何を無効化するか](../../docs/operator-runbook.md#which-logout-endpoint-invalidates-what) を参照。
643
+ `jwksModule`(core 由来)は常に合成される: トークンに署名するプロバイダーは、issuer が設定されているかどうかにかかわらず検証鍵を公開する。各ルートモジュールが何をマウントするかは、それぞれのパッケージの README にある。テンプレートは core の `sessionLifecycleModule` を合成する: ログインはそのセッションのレコードを開き、コード交換がそれに参加し、`POST /session/logout` と `POST /oauth/logout` はどちらもライフサイクルを通してセッションを終了する。終了はセッションの refresh token family を失効させ、relying party に伝える。[どのログアウトエンドポイントが何を無効化するか](../../docs/operator-runbook.md#which-logout-endpoint-invalidates-what) を参照。
618
644
 
619
645
  ## 組み込みルート
620
646
 
@@ -694,11 +720,13 @@ docker run -e HTTP_PORT=8080 -p 8080:8080 my-auth-provider
694
720
  docker compose -f docker-compose.yml -f docker-compose.mailpit.yml up --build
695
721
  ```
696
722
 
723
+ 最初に実行する前に、自分の鍵を `.env` に `MFA_ENCRYPTION_KEY` として書く(`openssl rand -base64 32`)。この実行は development のサンプル鍵を使わず、鍵が無ければ boot は拒否される。
724
+
697
725
  メールは <http://localhost:8025> で読む。Mailpit はメールを保持し、先へは何も配送しない。素の `docker compose up`(`make dev`)は Mailpit を起動せず、本番用のファイルにも Mailpit は無い。開発専用である: デプロイでは `STANDARD_SMTP_MAIL_SENDER_*` の変数で実際のリレーを指定する([多要素認証](#多要素認証))。
698
726
 
699
727
  オーバーレイが app サービスに設定するもの:
700
728
 
701
- - `CONFIG_ENV=mailpit`。`development` ではテンプレートは各コードをログに出す送信者を入れる([モジュール合成順序](#モジュール合成順序) のルール 7)。[`config/mailpit.conf`](config/mailpit.conf) は `development.conf` を include するので、それ以外は MFA のサンプル鍵も含めて development の実行と同じである。
729
+ - `CONFIG_ENV=mailpit`。`development` ではテンプレートは各コードをログに出す送信者を入れる([モジュール合成順序](#モジュール合成順序) のルール 7)。[`config/mailpit.conf`](config/mailpit.conf) は `development.conf` を include しない。`development.conf` の唯一の設定は MFA のサンプル鍵で、MFA パッケージがそれを受け入れるのは、設定されている環境名がすべて `development` か `test` を示すときだけだからである。鍵リングの最初の鍵は、MFA パッケージの `reference.conf` が束ねるとおり `MFA_ENCRYPTION_KEY` である。
702
730
  - `MFA_MODE` は設定しない: MFA はテンプレートの出荷どおり、または `.env` が設定するとおりに有効である。MFA が off ならメールを送るものが無い。
703
731
  - `ADAPTERS_MFA_FACTOR_STORE=redis` と `ADAPTERS_MFA_TRANSACTION_STORE=redis`(compose の Redis 上、`docker-compose.yml` と同じ): `development` と `test` 以外の名前では、MFA のストアをメモリに置けない。
704
732
  - `STANDARD_SMTP_MAIL_SENDER_HOST=localhost`、`STANDARD_SMTP_MAIL_SENDER_PORT=1025`、`STANDARD_SMTP_MAIL_SENDER_SECURE=none`、`STANDARD_SMTP_MAIL_SENDER_FROM=auth@example.com`。SMTP 送信者が平文で送るのはループバックのホストにだけなので、Mailpit は app コンテナのネットワーク名前空間で動き(`network_mode: service:app`)、その `localhost` で待ち受ける。そのため Mailpit の Web UI のポートは、Mailpit ではなく app サービスがループバックに公開する。
@@ -749,14 +777,14 @@ probe は接続を開いた builder が登録するため、リストはこの
749
777
 
750
778
  ## カスタムモジュールの追加
751
779
 
752
- カスタムモジュールを追加するには、`src/buildModules.mts` でインポートし、`buildModules` が返す配列に追加する。boot planner は依存関係をモジュールが `require` / `provide` するものによって解決するが、ルートとミドルウェアは、`before` / `after` の指定が無い限りリストの順にマウントする。したがって、他のルートが依存する middleware では位置が意味を持つ — セッションストアモジュール、[モジュール合成順序](#モジュール合成順序) のルール 1 である。`/oauth` のように別のモジュールも使うプレフィックスの下のルートは、自分の body を自分でパースし、その parser を自分のパスちょうどに限定する限り、特定の位置を必要としない(ルール 2): `oauthModule` のルーターがパースするのは自身のルートの body だけで、プレフィックス配下のすべてのリクエストに走る parser は、他のモジュールの body を代わりに読んでしまう。前者のようなものをマウントしないモジュールは末尾に置けばよい:
780
+ カスタムモジュールを追加するには、`src/buildModules.mts` でインポートし、`buildModules` が返す配列に追加する。boot planner は依存関係をモジュールが `require` / `provide` するものによって解決するが、ルートとミドルウェアは、`before` / `after` の指定が無い限りリストの順にマウントする。したがって、他のルートが依存する middleware では位置が意味を持つ — セッションストアモジュール、[モジュール合成順序](#モジュール合成順序) のルール 1 である。`/oauth` のように別のモジュールも使うプレフィックスの下のルートは、自分の body を自分でパースし、その parser を自分のパスちょうどに限定する限り、特定の位置を必要としない(ルール 2): `oauthEndpointsModule` のルーターがパースするのは自身のルートの body だけで、プレフィックス配下のすべてのリクエストに走る parser は、他のモジュールの body を代わりに読んでしまう。前者のようなものをマウントしないモジュールは末尾に置けばよい:
753
781
 
754
782
  ```diff
755
783
  // src/buildModules.mts
756
784
  +import { myCustomModule } from "./my-custom-module.mjs";
757
785
  …
758
786
  return [
759
- sessionStoreModuleFor(config),
787
+ sessionStoreModule,
760
788
  …
761
789
  ...(federationGrantsEnabled || mfaInstalled ? [subjectRevocationServiceModule] : []),
762
790
  + myCustomModule,