@o3co/create-auth-provider 0.17.0-rc.1 → 0.17.0-rc.2

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 (58) hide show
  1. package/package.json +1 -1
  2. package/templates/standalone/.env.example +0 -3
  3. package/templates/standalone/README.ja.md +42 -18
  4. package/templates/standalone/README.md +120 -44
  5. package/templates/standalone/config/application.conf +40 -36
  6. package/templates/standalone/config/reference.conf +21 -2
  7. package/templates/standalone/docker-compose.production.yml +5 -2
  8. package/templates/standalone/package.json +1 -1
  9. package/templates/standalone/src/__tests__/adapters.test.mts +80 -19
  10. package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +87 -0
  11. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +112 -98
  12. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +27 -6
  13. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +86 -51
  14. package/templates/standalone/src/__tests__/audit-sink.test.mts +10 -9
  15. package/templates/standalone/src/__tests__/config-notices.test.mts +154 -0
  16. package/templates/standalone/src/__tests__/core-federations.test.mts +48 -25
  17. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +1 -1
  18. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +349 -54
  19. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +15 -21
  20. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +9 -15
  21. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +10 -4
  22. package/templates/standalone/src/__tests__/federation-types.test.mts +239 -0
  23. package/templates/standalone/src/__tests__/google-session.fixture.mts +60 -67
  24. package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +1 -1
  25. package/templates/standalone/src/__tests__/library-references.fixture.mts +51 -46
  26. package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +216 -0
  27. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +0 -2
  28. package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +122 -0
  29. package/templates/standalone/src/__tests__/mfa-switch.test.mts +70 -25
  30. package/templates/standalone/src/__tests__/own-modules.test.mts +40 -11
  31. package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +3 -3
  32. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +8 -4
  33. package/templates/standalone/src/__tests__/replica-safety.test.mts +127 -19
  34. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +15 -20
  35. package/templates/standalone/src/__tests__/session-lifecycle.test.mts +215 -0
  36. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +3 -7
  37. package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +130 -0
  38. package/templates/standalone/src/__tests__/session-requirements.test.mts +114 -31
  39. package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +270 -0
  40. package/templates/standalone/src/__tests__/smoke.test.mts +19 -28
  41. package/templates/standalone/src/__tests__/store-credential.test.mts +8 -13
  42. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +50 -34
  43. package/templates/standalone/src/__tests__/two-phase-config.test.mts +242 -76
  44. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +23 -31
  45. package/templates/standalone/src/adapters.mts +31 -18
  46. package/templates/standalone/src/app.mts +6 -5
  47. package/templates/standalone/src/bootRefusal.mts +117 -0
  48. package/templates/standalone/src/buildModules.mts +46 -96
  49. package/templates/standalone/src/configPath.mts +180 -153
  50. package/templates/standalone/src/mfaSwitch.mts +43 -20
  51. package/templates/standalone/src/modules.mts +25 -213
  52. package/templates/standalone/src/rootRenames.mts +35 -18
  53. package/templates/standalone/src/sections.mts +1 -0
  54. package/templates/standalone/tests/index.test.js +4 -3
  55. package/templates/versions.json +18 -18
  56. package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +0 -134
  57. package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +0 -202
  58. package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +0 -144
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.2",
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",
@@ -142,9 +142,6 @@ SESSION_STORE_NAME=auth.sid
142
142
  # OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN=3600
143
143
  # The most a token-exchange `expires_in` can obtain; unset = the default.
144
144
  # 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
145
  # OAUTH_REFRESH_TOKEN_EXPIRES_IN=86400
149
146
  # SESSION_STORE_MAX_AGE=3600000
150
147
  # 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
 
@@ -73,7 +73,11 @@ my-app:
73
73
 
74
74
  allowlist はネットワーク上の制御であって、暗号学的な制御ではない。エッジは受信した `X-Forwarded-*` ヘッダーに追記するのではなく**除去**しなければならず、エッジとこのプロセスの間のホップは、送信元アドレスを偽装できる者から到達できてはならない。
75
75
 
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` の予算として寄与するため、改めて書き直すものは無い。
76
+ **`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`)は試行カウンターの上で数えられ、リミッターの設定はこれを変えない。
77
+
78
+ **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 パッケージ自身のもので、リミッターの設定はこれを変えない。
79
+
80
+ **`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 のデフォルト)で動かすこと: 追い出された窓は数え直しになるからである。モジュールは読み取れたほかのポリシーでは起動を拒否するが、サーバーが答えない(`INFO` と `CONFIG` が拒否・改名されている)ときは `attempt_counter_durability_unchecked` を警告して起動するので、ポリシーはサーバーの設定側で確かめること。どちらも閉じる側に倒れる: カウンターが答えられない間、`/session/login` と `POST /oauth/device/verification` は `redis-rate-limiter.failMode` が何であっても `503` を返す。
77
81
 
78
82
  **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
83
 
@@ -94,7 +98,7 @@ redis-rate-limiter {
94
98
 
95
99
  デフォルトは意図的に変えていない: 送信元 IP ごとに 60 秒あたり 60 回というのは、クライアントが本当に別々の IP であるデプロイにとって妥当な総当たり対策の上限であり、一方の構成に合わせて全体的に引き上げれば、もう一方の構成での防御が弱まる。BFF *自身の*ユーザーを守るスロットリングは、ユーザーごとの identity がまだ存在する BFF の前段に置くこと。
96
100
 
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) を参照。
101
+ **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
102
 
99
103
  この変数は `core.deployment.mode` を設定する。旧名の `DEPLOYMENT_MODE` は、単独で、または `CORE_DEPLOYMENT_MODE` と異なる値で設定されているとブートを拒否し、同じ値で並べて設定されていればブートする。
100
104
 
@@ -204,9 +208,9 @@ pnpm run start
204
208
 
205
209
  1. **`config/{ENV}.conf`** — 現在の環境の overlay。`ENV = CONFIG_ENV || NODE_ENV || "development"` で決まる。
206
210
  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 ファイルのどちらも設定していないキーは、ここから値を得る。
211
+ 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
212
 
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` として一度だけログに出る: セクション名の綴り間違いはここに現れる。
213
+ 上の 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
214
 
211
215
  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
216
 
@@ -217,6 +221,7 @@ overlay の値は `application.conf` より優先される。scaffold には `de
217
221
  | 変数 | デフォルト | 説明 |
218
222
  |---|---|---|
219
223
  | `ADAPTERS_RATE_LIMITER` | `memory` | `adapters.rateLimiter`: `memory` または `redis`。[マルチレプリカ構成](#マルチレプリカ構成) を参照 |
224
+ | `ADAPTERS_ATTEMPT_COUNTER` | `memory` | `adapters.attemptCounter`: ログインの試行上限(device grant を加えたときはデバイス検証のものも)を数えるカウンター。`memory`(配線しない: プロセスごと)または `redis`。[マルチレプリカ構成](#マルチレプリカ構成) を参照 |
220
225
  | `ADAPTERS_USER_SESSION_STORES` | `memory` | `adapters.userSessionStores`: ユーザーセッションストア、`memory` または `redis`。`SESSION_STORE_STORAGE_TYPE` と揃える([Docker](#docker) を参照) |
221
226
  | `ADAPTERS_ACCESS_TOKEN_DENYLIST` | `redis` | `adapters.accessTokenDenylist`: `memory` または `redis` |
222
227
  | `ADAPTERS_REPLAY_SEEN_SET` | `redis` | `adapters.replaySeenSet`: `private_key_jwt` の背後の replay seen-set、`memory` または `redis` |
@@ -290,13 +295,14 @@ openssl pkey -in jwt-private.pem -pubout -out jwt-public.pem
290
295
  |---|---|---|
291
296
  | `OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN` | `3600` | リクエストが有効期間を指定しないときに全グラントが発行するアクセストークンの有効期間(秒)。正の整数、上限は 1 年(`31536000`)。 |
292
297
  | `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
298
  | `OAUTH_REFRESH_TOKEN_EXPIRES_IN` | `86400` | リフレッシュトークンの有効期間(秒)。正の整数、上限は 1 年(`31536000`)。 |
295
299
 
296
300
  どれも 10 進数字だけを読む。空文字で export すると、フォールバックではなく起動失敗になる(HOCON は `FOO=` を `""` に解決する)。16 進(`0x10`)、指数(`1e3`)、符号(`+5`)、小数(`5.0`)も同じく起動失敗になる。
297
301
 
298
302
  `expires_in` リクエストパラメータを読むのは token exchange(RFC 8693)だけで、他のグラントはそれを無視してデフォルトを発行する。
299
303
 
304
+ デフォルトの旧名 `OAUTH_ACCESS_TOKEN_EXPIRES_IN`(config キーでは `oauth.accessToken.expiresIn`)は、単独で、または `OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN` と異なる値で設定されているとブートを拒否し、同じ値で並べて設定されていればブートする。旧キーは値が何であれブートを拒否する。値は消さずに新しい名前へ移すこと: 消すとデフォルトは `3600` になる。
305
+
300
306
  ### グラントタイプ
301
307
 
302
308
  | 変数 | デフォルト | 説明 |
@@ -307,7 +313,7 @@ openssl pkey -in jwt-private.pem -pubout -out jwt-public.pem
307
313
  | `OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED` | `false` | client credentials グラントタイプを有効化 |
308
314
  | `OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED` | `false` | jwt-bearer グラントタイプ(RFC 7523)を有効化 |
309
315
 
310
- スイッチはそれぞれモジュールのキー — `oauth-session.enabled`、ほかは `oauth-authorization.grants.<grant>.enabled` — で、テンプレートはモジュールを選ぶために boot の前に自分のファイルと環境変数からこれを読む。旧名の `OAUTH_GRANTS_<GRANT>_ENABLED` は、単独で、または新名と違う値で設定されていると boot を拒否し、同じ値なら boot する。
316
+ スイッチはそれぞれモジュールのキー — `oauth-session.enabled`、ほかは `oauth-authorization.grants.<grant>.enabled` — である。どちらのモジュールも常に読み込まれ、自分のスイッチを boot がパースする設定から boot で読む。テンプレートはそのどれも boot の前には読まない。旧名の `OAUTH_GRANTS_<GRANT>_ENABLED` は、単独で、または新名と違う値で設定されていると boot を拒否し、同じ値なら boot する。
311
317
 
312
318
  ### Session
313
319
 
@@ -375,6 +381,24 @@ http.cors {
375
381
 
376
382
  フェデレーションは core のものである: `core.federations`、各フェデレーションに到達する名前(`/session/oauth/federation/<name>`)をキーとする 1 つのマップ。各キーは、そのパスから名付けた変数 `CORE_FEDERATIONS_<NAME>_<KEY>` に束縛される。トップレベルに書いたマップ(`federations { ... }`)は、各キーの `core.federations` の下のパスを名指しして起動を拒否する。`FEDERATIONS_GOOGLE_*` や `FEDERATIONS_OIDC_*` の変数が単独で、または新しい名前と異なる値で設定されていると、どのモジュールを選ぶよりも前に拒否される。
377
383
 
384
+ すべてのエントリは自分の `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) に従う。
385
+
386
+ `core.federations.google` は `type = "google"` を書いて同梱されている。2 つ目の Google クライアントは、同じ type の 2 つ目のエントリを、独自の名前と独自のコールバックで書く:
387
+
388
+ ```hocon
389
+ core.federations {
390
+ google-work {
391
+ enabled = true
392
+ type = "google"
393
+ clientId = ${GOOGLE_WORK_CLIENT_ID}
394
+ clientSecret = ${GOOGLE_WORK_CLIENT_SECRET}
395
+ callbackURL = "https://auth.example.com/session/oauth/federation/google-work/callback"
396
+ }
397
+ }
398
+ ```
399
+
400
+ `google` のエントリが下の変数のほかに受け付けるキー — `redirectAllowlist`、`sessionDomain`、`authCallbackUrl`、`clientUrl`、`requireAuthorizationResponseIss`、`endSessionEndpoint` — の完全な一覧は [パッケージの README](../../packages/federation-google/README.md) にある。`config/application.conf` は `endSessionEndpoint` 以外のすべてを示す。type が挙げていないキーはエントリを拒否させる。
401
+
378
402
  | 変数 | デフォルト | 説明 |
379
403
  |---|---|---|
380
404
  | `CORE_FEDERATIONS_GOOGLE_ENABLED` | `false` | Google OAuth フェデレーションを有効化 |
@@ -411,7 +435,7 @@ core.federations {
411
435
  }
412
436
  ```
413
437
 
414
- エントリの `type` が実装を指定する。`type` の無い `core.federations.google` は組み込みの Google フェデレーションである。`type = "oidc"` を付けると `google` という名前の汎用 OIDC インスタンスになり、組み込みモジュールは合成されない。
438
+ 実装を指定するのはエントリの `type` であり、名前ではない: `type = "oidc"` を書いた `google` という名前のエントリは、`google` という名前の汎用 OIDC フェデレーションである。
415
439
 
416
440
  パッケージが受け付けるすべてのフィールド — `scopes`、`discovery` / `endpoints`、`privateKey`、`userInfo`、`idTokenSignedResponseAlg`、`clockToleranceSeconds` — は [パッケージの README](../../packages/federation-oidc/README.md) に記載されている。
417
441
 
@@ -585,7 +609,7 @@ worker:
585
609
 
586
610
  | 変数 | デフォルト | 説明 |
587
611
  |---|---|---|
588
- | `REDIS_SESSION_STORES_KEY_PREFIX` | `ss:` | ユーザーセッション、RP レジストリ、session-family インデックス、session-federation インデックスの外側の prefix。 |
612
+ | `REDIS_SESSION_STORES_KEY_PREFIX` | `ss:` | ユーザーセッション、subject のセッションインデックス、subject の失効境界、セッションライフサイクルのレコードの外側の prefix。 |
589
613
  | `REDIS_REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX` | `rtfam:` | refresh token family レコードの prefix。 |
590
614
  | `REDIS_CODE_REPOSITORY_KEY_PREFIX` | `oauth:code:` | OAuth 認可コードの prefix。 |
591
615
  | `REDIS_FEDERATION_TOKEN_STORE_KEY_PREFIX` | `ft:` | フェデレーショントークンのレコード、そのセッションごとのインデックス、およびそのロックキーの prefix。 |
@@ -606,15 +630,15 @@ worker:
606
630
 
607
631
  どのモジュールをどの順序で合成するかについては、[`src/buildModules.mts`](src/buildModules.mts) が唯一の信頼できる情報源である — そのコピーではなく、それ自体を読むこと。boot planner は `requires` / `provides` をトポロジカルに解決するが、ルートとミドルウェアは、モジュールが `before` / `after` を宣言しない限りリストの順にマウントする — したがって、何かをマウントするモジュールでは位置が意味を持つ。リストを編集するときに守るべきルールは次のとおり:
608
632
 
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 の後ろに自らを並べる。
633
+ 1. **`sessionStoreModule` は先頭のままにする。** これは `express-session` の middleware をマウントし、`before` / `after` を宣言しないため、`req.session` を読むすべてのルートより前に来るのは、リスト内の位置のおかげである。replica safety は自分のセクションから宣言するので、`session-store.storage.type = "memory"` は replica-unsafe となり、`core.deployment.mode = "multi"` で拒否される。
634
+ 2. **`/oauth` の下では順序は関係しない。** `federationGrantsModules`(フェデレーショングラントが有効な間)、`oauthEndpointsModule`、独自のモジュールは、いずれも `/oauth` の下にルートをマウントしうる。`oauthEndpointsModule` のルーターが body をパースするのは自身のルートだけなので、各モジュールへのリクエストは、リストの順に関係なくそのモジュール自身の parser に届く — ただし、`/oauth` 配下のどのモジュールも自分の body を自分でパースし、その parser を自分のパスちょうどに限定している場合に限る(同梱のモジュールはそうしている)。限定はルートとして行う(`router.all(path, parser)` か、ルート自身のハンドラ列)。`router.use(path, parser)` は `path` の下のすべてのパスにもマッチする。フェデレーショングラントのブラウザ側の半分は、自身の `after` によってセッション middleware の後ろに自らを並べる。
611
635
  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
636
  4. **共有 Redis 接続は、最初の Redis バックエンドのモジュールとともに加わる。** `standaloneRedisClientsModule` は、ここにあるすべての Redis アダプターが使う 1 本の ioredis 接続を自身のセクション(`redis-clients`)から開き、合成されたモジュールがそれを必要とするときには必ず追加される。同梱の合成では refresh token family ストアが Redis 上にあるため、デプロイには常にこれがある。in-memory の family ストアはテスト用の override(`overrides.refreshTokenFamilyModules`)である。
613
637
  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 モジュール)は起動を拒否する。
638
+ 6. **フェデレーションの type は常に合成される。** `googleFederationTypeModule()` と `oidcFederationTypeModule()` は `core.federations` の内容にかかわらずリストに入る: それぞれが自分の type を提供し、boot はその type の有効なエントリごとに 1 つのフェデレーションを、エントリの名前で組み立てる。したがってフェデレーションの追加と削除は設定だけで済む。リストの中でエントリを読むものは無い。type で振り分けられるエントリと同じ名前のフェデレーションを直接提供するモジュールは、起動時の `duplicate-contribute` になる: 別の種類のフェデレーションは type モジュールとして加え、そのエントリにその type を書く。
639
+ 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
640
 
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) を参照。
641
+ `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
642
 
619
643
  ## 組み込みルート
620
644
 
@@ -749,14 +773,14 @@ probe は接続を開いた builder が登録するため、リストはこの
749
773
 
750
774
  ## カスタムモジュールの追加
751
775
 
752
- カスタムモジュールを追加するには、`src/buildModules.mts` でインポートし、`buildModules` が返す配列に追加する。boot planner は依存関係をモジュールが `require` / `provide` するものによって解決するが、ルートとミドルウェアは、`before` / `after` の指定が無い限りリストの順にマウントする。したがって、他のルートが依存する middleware では位置が意味を持つ — セッションストアモジュール、[モジュール合成順序](#モジュール合成順序) のルール 1 である。`/oauth` のように別のモジュールも使うプレフィックスの下のルートは、自分の body を自分でパースし、その parser を自分のパスちょうどに限定する限り、特定の位置を必要としない(ルール 2): `oauthModule` のルーターがパースするのは自身のルートの body だけで、プレフィックス配下のすべてのリクエストに走る parser は、他のモジュールの body を代わりに読んでしまう。前者のようなものをマウントしないモジュールは末尾に置けばよい:
776
+ カスタムモジュールを追加するには、`src/buildModules.mts` でインポートし、`buildModules` が返す配列に追加する。boot planner は依存関係をモジュールが `require` / `provide` するものによって解決するが、ルートとミドルウェアは、`before` / `after` の指定が無い限りリストの順にマウントする。したがって、他のルートが依存する middleware では位置が意味を持つ — セッションストアモジュール、[モジュール合成順序](#モジュール合成順序) のルール 1 である。`/oauth` のように別のモジュールも使うプレフィックスの下のルートは、自分の body を自分でパースし、その parser を自分のパスちょうどに限定する限り、特定の位置を必要としない(ルール 2): `oauthEndpointsModule` のルーターがパースするのは自身のルートの body だけで、プレフィックス配下のすべてのリクエストに走る parser は、他のモジュールの body を代わりに読んでしまう。前者のようなものをマウントしないモジュールは末尾に置けばよい:
753
777
 
754
778
  ```diff
755
779
  // src/buildModules.mts
756
780
  +import { myCustomModule } from "./my-custom-module.mjs";
757
781
  …
758
782
  return [
759
- sessionStoreModuleFor(config),
783
+ sessionStoreModule,
760
784
  …
761
785
  ...(federationGrantsEnabled || mfaInstalled ? [subjectRevocationServiceModule] : []),
762
786
  + myCustomModule,