Skip to content

Latest commit

 

History

History
971 lines (746 loc) · 54.6 KB

File metadata and controls

971 lines (746 loc) · 54.6 KB

TESTING.md — E2E テストの実行と判定

対象: root/programs/Tests 配置: root

本書は、どう実行し、どう合否を判定するかを扱う。 テストそのものの設計方針と個々のテストの説明は programs/Tests/README.md が一次情報。

一次情報は本書ではない。 迷ったら次を見ること。

内容 一次情報
各テストが何を確かめるのか programs/Tests/TESTCASES.md
テストの方針・構成・未修正項目 programs/Tests/README.md
適合上の穴の一覧 programs/ANALYSIS-IdP.md
ビルド BUILDING.md
設定と起動 URL CONFIGURATION.md

1. 使い方

cd root
.\2_RunAllTests.ps1 -Launch     # 2 つのサイトを起動 → テスト → 停止
.\2_RunAllTests.ps1             # 起動済みのサイトを叩く
.\2_RunAllTests.ps1 -Launch -Filter "FullyQualifiedName~RequestObjectTests"

1 件だけ試すときは、下の層を直接叩いてもよい。

cd root\programs\Tests
.\test.ps1 -Launch -Filter "FullyQualifiedName~Issue186"
引数 意味
-Launch net10.0 版と net48 版の両方を起動してからテストし、終わったら停止する
-Url net10.0 版(Kestrel)の待ち受け URL。既定 https://localhost:44300
-NetFxUrl net48 版(IIS Express)の待ち受け URL。既定 https://localhost:44302
-NoNetFx net48 版を起動しない。その分は Skip される
-Filter dotnet test の --filter
-Configuration Debug(既定)/ Release
-OutputDir TRX とログの保存先。既定は programs\Tests\E2ETests\Result(.gitignore 済み)
-UpdateTestCases テストケースの原本(programs\Tests\TESTCASES.md)を作り直す
-UserStoreType サイトが使うストア(mem 既定 / sql / ora / npg)。mem 以外は下の「ストアを切り替える」を読む(#207)
-ConnectionString mem 以外のときの接続文字列。省略時は環境変数から読む

ストアを切り替える(#207)

既定は mem。 これは変えない。sql / ora / npg は、対応する DBMS が 動いていることが前提になるため、既定にすると DBMS の無い環境でテストが回らなくなる。

# 接続文字列は環境変数で渡す(キー名ではなく、専用の名前を使う)
$env:MPAS_CONNSTR_SQL = '...'
.\2_RunAllTests.ps1 -Launch -UserStoreType sql

E2E 用の DBMS は store/ で立てる(#250 の段階 1)

このリポジトリの store/ が、3 方言をまとめて立てる(SQL Server / Oracle / PostgreSQL)。

cd store
.\1_DockerComposeUp.bat      # DDL を流し込んでから起動する
.\2_DockerComposeDown.bat    # -v 付き。作り直せるように残さない

ポートは +1 にしてある。

store/(E2E) 既定のポート
SQL Server 1434 1433
Oracle 1522 1521
PostgreSQL 5433 5432

なぜ +1 か。 手動確認は LocalServicesOnDocker を使い続ける(RP アプリなどもそちらに繋ぐ)。 既定ポートを空けておくことで、E2E 用と同時に起動できる。

コミット済みの ConnectionString_* はポートを書いていないので、 既定ポート = LocalServicesOnDocker を指す。手動確認はそのまま。

E2E に渡す接続文字列。

$env:MPAS_CONNSTR_SQL = 'Data Source=localhost,1434;Initial Catalog=UserStore;User ID=sa;Password=<pw>;Encrypt=false;'
$env:MPAS_CONNSTR_ODP = 'User Id=SCOTT;Password=<pw>;Data Source=localhost:1522/FREEPDB1;'
$env:MPAS_CONNSTR_NPS = 'HOST=localhost;PORT=5433;DATABASE=UserStore;USER ID=postgres;PASSWORD=<pw>;'
  • DDL は 0_CopyInitSql.ps1 が repo から流し込む(1_DockerComposeUp.bat が先に呼ぶ)。 コピー先は生成物で .gitignore 済み。原本は root/files/resource/.../Sql/ だけ
  • store/ の DB は使い捨てにできる。 そのため、下の 「古いデータベースを使い回すと、列が足りない」は E2E 側では起きない (手動側=LocalServicesOnDocker では引き続き起こりうる)
  • Oracle の初回起動は数分かかる(docker compose ps が healthy になるまで待つ)
ストア 接続文字列の環境変数 上書きされる設定キー
sql MPAS_CONNSTR_SQL ConnectionString_SQL
ora MPAS_CONNSTR_ODP ConnectionString_ODP
npg MPAS_CONNSTR_NPS ConnectionString_NPS

スクリプトに接続文字列の既定値は持たせていない。 持たせると、それが事実上の資格情報になる。 渡していなければ、その場で止まる(どの環境変数を設定すればよいかを表示する)。

DBMS は LocalServicesOnDocker で、まとめてコンテナとして起動できる (SQL Server / Oracle / PostgreSQL。#208 の実測はこれで行った)。 Oracle は gvenzl/oracle-free:23-slim で、接続先の PDB は FREEPDB1(XEPDB1 ではない)。

切り替える前に、対象の DBMS で次が済んでいること。

  1. 空のデータベース(スキーマ)を作る
  2. files/resource/MultiPurposeAuthSite/Sql/<dbms>/Create_UserStore.sql を流す

ロール・管理者・テスト ユーザは、サイトが初回の /Account/Login で作る (CreateData が Roles の件数で初期化済みかを判定する)。手で入れる必要はない。

net48 版は npg を選べない。 Npgsql の参照が #if NETCORE で囲まれているため、 -UserStoreType npg のときは net48 版を起動しない(その分は Skip)。

mem と違い、net48 版と net10.0 版が同じデータベースを共有する。 mem は各サイトが別の入れ物なので、ストアを変えると初めて出る失敗がある。

実測(#242 の作業。sql) : 通しで RT-230.1(core)が 1 件落ちた (UnstructuredData に書いた値が /userinfo に出ない)。 UserClaimsTests だけを回すと 8/8 通るので、通し実行のときだけ起きる。 原因は未特定。 「両ターゲットの取り合い」は確かめたが説明になっていない (この値を書くのは UserClaimsTests だけで、同じクラスのケースは並列に走らない。 利用者の行が重複しているわけでもない)。落ちたら、まず 1 クラスだけで回して切り分ける。

実測(#250 の段階 1。store/ の 3 方言) : 同じ型が別のテストでも出た。

ストア 落ちたもの 1 クラスだけで回すと
sql RT-233.2(core) 22/22 通る
ora RT-230.3(core) 8/8 通る
npg 無し(210 成功 / 失敗 0) -

分かっていること。 core だけ/DB ストアのときだけ/通しのときだけ/毎回 1 件だけ。 落ちるテストは毎回違う(RT-230.1 / RT-230.3 / RT-233.2)。 mem では出ない(各サイトが別の入れ物のため)。原因は依然として未特定。

上流の IdP も store/ で立てる(#250 の段階 2〜3)

ハイブリッド ID フェデレーションの「上流側」を、コンテナで 1 つ建てる。 下流(連携する側)はホストで動かす(Visual Studio / test.ps1 -Launch)。

cd store
.\3_PublishUpstream.ps1      # publish と証明書(1_DockerComposeUp.bat が先に呼ぶ)
docker compose up -d upstream
値 なぜ
URL https://localhost:44301 雛形が上流として書いている番号
パス root(/authorize) UsePathBase を呼んでいない
ストア mem 雛形のテスト利用者が自動で作られる。上流に DB は要らない
証明書 ホストの dotnet dev-certs を書き出したもの 既に信頼済み。ブラウザが警告を出さない
ログ store/logs/(ACCESS / OPERATION / SQLTRACE) ホストから読める

net48 版はコンテナ化しない。 上流は 1 つあればよく、net10.0 版で足りる。

パスの形が VS と違う。 雛形の IdFederation{Authorize,Token,UserInfo}Endpoint は https://localhost:44301/MultiPurposeAuthSite/... を指している。これは IIS Express の仮想ディレクトリの形で、Kestrel(コンテナ)は root で配信する。 下流はこの 3 つを /MultiPurposeAuthSite 抜きに向ける必要がある (E2E は appSettings__... の環境変数で渡せる。4 節と同じ形)。

UsePathBase を足して VS に合わせることはしなかった。 E2E の net10.0 版(https://localhost:44300)も既に root で配信しているので、 root がこのアプリの Kestrel での通常の形である。

リソースはイメージに入れず、ホストの C:\root\files\resource をマウントする(読み取り専用)。 署名鍵(X509 の pfx、JwkSet.json)を含むため、イメージに焼くべきではない。 中身は Readme.ja.md の手順で用意されているものを、そのまま使う。

雛形の設定は 15 箇所が C:/root/files/resource/... である(Windows 前提)。 Linux ではドライブ文字が効かないので、docker-compose.yml が 15 個すべてをマウント先(/resource)に振り替えている。 1 つでも漏らすと、その設定を使った瞬間に落ちるので、 appsettings.json を "C:/root/files で grep した数と突き合わせること。

log4net だけは中身(出力先)も Windows のパスなので、 差し替えた構成(store/app/LogConf.xml)をイメージに入れてある。

上流コンテナの自己テスト(#250)

自己テストは「サーバが自分自身を WebAPI で呼ぶ」(client_credentials など)。 コンテナの中からは、外向けのホスト名・ポートに届かない。

コンテナ内   localhost:8080  : OPEN      ← 待ち受け(HTTP)
コンテナ内   localhost:8081  : OPEN      ← 待ち受け(HTTPS)
コンテナ内   localhost:44301 : CLOSED    ← ホスト側の公開ポート。**届かない**

docker-compose.yml が OAuth2ContainerizatedAuthSvrEPRootURI に http://localhost:8080 を与えている。 Helper.GetContainerizatedAuthZServerUri が、 Helper を通る WebAPI 呼び出しの宛先をこれに差し替える(Windows でないときだけ働く)。

HTTP のループバックにしてある。 HTTPS(8081)にすると、 コンテナの中でホストの開発用証明書を検証できず、証明書を信頼させる手当てが要る。 自分自身への呼び出しなので、コンテナの外には出ない。

実測(/Home/Saml2OAuth2Starters のボタンを叩いた結果)。

ボタン 結果
ClientCredentialsFlow access_token が返る
ResourceOwnerPasswordCredentialsFlow access_token が返る
JWTBearerTokenFlow access_token が返る
DeviceAuthZGrant 応答画面(DeviceAuthZResponse)まで進む
FAPI_CIBA_Profile access_denied : The authentication device is not registered.(認証デバイスの登録が要る。RT-246.3 が Skip なのと同じ理由)

画面遷移を伴うもの(認可エンドポイントへブラウザが飛ぶ Authorization Code / Implicit / Hybrid / PKCE、 および mTLS を使う FAPI2)は、ここでは測っていない。 mTLS はクライアント証明書の持ち込みが要るので、コンテナでは動かない。

起動できたかは、ディスカバリで確かめる。

Invoke-RestMethod https://localhost:44301/.well-known/openid-configuration
Invoke-RestMethod https://localhost:44301/jwkcerts   # RS256 と ES256 の 2 つが出る

jwkcerts が返れば、マウントした署名鍵まで読めている。

DataProtection の鍵の置き場(#251)も、ここで効いている。 appSettings__DataProtectionKeyPath=/keys を store/keys にマウントしてあるので、 コンテナを作り直してもサインインが切れない。

実測 : サインインしてから docker restart / docker compose rm -sf + up を それぞれ 2 回。4 回とも /Manage/Index は 200(サインインは維持された)。

起動を待たずに叩くと、サインイン画面に飛ばされる。 判定の前に jwkcerts が返るまで待つこと(決め打ちの sleep では足りないことがある)。

ID フェデレーションの目視・E2E は、まだこれから(5 節「ID フェデレーション」)。

4 つのストアの実測(#245 の段階 3)

実測 2026/09/29。 ビルドは net48 / net10.0 とも エラー 0 / 警告 0。

ストア 成功 失敗 Skip 備考
mem(既定) 410 0 3 RT-245.4 ×2(C-10 未修正)+ RT-246.3 core(mTLS フックの副作用)
sql 410 0 3 同上
ora 410 0 3 同上
npg 207 0 206 net48 版を起動しないぶんが Skip(Npgsql が #if NETCORE)

RT-230.1 は、この 3 方言の通しでは再現しなかった(下の #242 の記録は残す。間欠なので「直った」とは言えない)。

古いデータベースを使い回すと、列が足りない(#245 の段階 3 で踏んだ)

DDL は更新されるが、既に作ってあるデータベースは更新されない。 移行スクリプトは持っていないため、使い回すなら DDL と突き合わせること。

実際に起きたこと。 npg と ora の RefreshTokenDictionary に、 #188 で足した 2 列(FamilyId / UsedDate)が無かった。 sql は作り直してあったので揃っていた。

列だけでなく、表が増えることもある。 #151 の段階 2 で SubjectIdentifier を足した(16 表 → 17 表)。 store/(E2E)は 2_DockerComposeDown.bat → 1_DockerComposeUp.bat で作り直せば済む。 手動確認の DB(LocalServicesOnDocker)は、自分で Create_UserStore.sql を流し直すこと。 表が無いと、サインイン(sub の記録)で落ちる。

  • 症状は HTTP 500 が 67 件(42703: column "familyid" ... does not exist)。 トークンが出ないので、関係の無いケースまで巻き添えで落ちる(85 件 失敗)
  • エラーはサイトのログに出る(programs\Tests\E2ETests\Result\MpasSite.out.log)。 E2E の失敗メッセージは「前提: access_token が返ること」までしか言わない

突き合わせ方(列の一覧を出して、DDL と比べる)。

# PostgreSQL
docker exec -e PGPASSWORD=<pw> <container> psql -U postgres -d UserStore -tAc `
  "SELECT table_name || '.' || column_name FROM information_schema.columns WHERE table_schema='public'"

# Oracle("..." で囲った大文字小文字混在の名前で作ってある)
select lower(table_name)||'.'||lower(column_name) from user_tab_columns;

足りないだけなら、作り直さずに足せる。

-- PostgreSQL(行が無ければ NOT NULL をそのまま足せる)
ALTER TABLE RefreshTokenDictionary
    ADD COLUMN FamilyId varchar(64) NOT NULL, ADD COLUMN UsedDate timestamp;

-- Oracle(行が在ると ORA-01758 になる。NULL 可で足す → 埋める → NOT NULL にする)
ALTER TABLE "RefreshTokenDictionary" ADD ("FamilyId" NVARCHAR2(64), "UsedDate" DATE);
UPDATE "RefreshTokenDictionary" SET "FamilyId" = SUBSTR("Key", 1, 64) WHERE "FamilyId" IS NULL;
COMMIT;
ALTER TABLE "RefreshTokenDictionary" MODIFY ("FamilyId" NOT NULL);

Create_UserStore.sql を流し直すのが正道である。 上は行を消さずに済ませる手順で、 古い行が残ることを承知で使うものである(テスト用のストアなので、それで困らない)。

mem と違い、状態が残る。 同じデータベースを使い回すと、前回のテスト ユーザや クライアント登録がそのまま残る。作り直したいときは、データベースを作り直す。

2. 構造

3 層になっている。 OpenTouryo が root/programs/*.ps1 から CS/*.bat を呼ぶのと同じ形。

root/2_RunAllTests.ps1          呼び出しと集計(TRX を読む)
  └ programs/Tests/test.ps1     どう起動して、どう流すか
      └ dotnet test → E2ETests  xUnit

分けてある理由。

  • test.ps1 は Tests フォルダから直接叩ける入口になる。1 件だけ流すときに使う
  • 2_RunAllTests.ps1 は集計だけを持つ。起動方法が変わっても、こちらは影響を受けない

3. テストは外から叩く

テストはアプリを HTTP で外から叩く。 実装側のクラス(CmnEndpoints / Helper など)を参照しない。

JWT のデコードも Request Object の署名も、テスト側で独立に実装している。 同じコードで作って同じコードで読むと、型や値の誤りを検出できないためである。

このため、サイトが動いていることが前提になる。

同梱の自己テスト(/Home/Saml2OAuth2Starters)とは役割が違う。 自己テストは Open棟梁 のクライアント ライブラリを使い、人が目で確かめるための場で、 E2E は自前の実装で外から叩き、合否を判定する。 E2E から自己テストを駆動してよい(IdPClient.StartSelfTestAsync)。 Open棟梁 のクライアントを通る経路はそこしか無いので、相互接続性の回帰だけは E2E が押さえる (RT-197 / RT-246)。線引きの全文は programs/MultiPurposeAuthSiteCore/ANALYSIS.md 7 節。

4. 起動する URL を合わせる(重要)

サイトは、構成ファイルに書かれた URL で待ち受けている必要がある。

アプリ同梱の自己テスト(FAPI2 / CIBA / Device AuthZ)は、 サーバ自身が OAuth2AuthorizationServerEndpointsRootURI へ HTTP で折り返す。 叩き先と構成が食い違うと、その折り返しが接続不能になり HTTP 500 になる。

https で動かすこと。 認証まわりの Cookie は SameSite=None で発行されるため、http では保持されない。 max_age を使うフロー(FAPI2)は auth_time Cookie を見るので、http だとエラー画面になる。

Visual Studio(IIS Express)で起動する分には、構成ファイルのままなので食い違わない。 -Launch は、構成ファイルを書き換えずに、環境変数で上書きしてから起動する。

OAuth2AuthorizationServerEndpointsRootURI
OAuth2ClientEndpointsRootURI
FcmOutboxDirectory

FcmOutboxDirectory は、プッシュ通知の送信箱(テスト用)。サイトは FCM に送らず、ここにファイルを書く。 テストは MPAS_CORE_FCM_OUTBOX / MPAS_NETFX_FCM_OUTBOX で場所を受け取り、認証デバイスの代わりに読む(CIBA の EX-8)。

Open棟梁 の GetConfigParameter は、appSettings の FxContainerization が ON のとき 設定ファイルより環境変数を優先する(net48 / net10.0 の両方)。 キー名がそのまま環境変数名になる。 接頭辞は付かない。

このため、net48 版を app.config の URL に置く必要がない。 別のポートへ寄せられるので、2 つのサイトを同時に立てても衝突しない。

環境変数は、子プロセスの起動時に写される。 2 つのサイトへ別々の URL を渡せるのは、この性質による。 起動の直前に書き換えること。後から変えても、動いている側には効かない。

詳細は CONFIGURATION.md。

5. 判定基準

識別子

テストには識別子が付いている。原本と報告は、これで突き合わせる。

接頭辞 対象 置き場所
SM-n 疎通(テスト基盤そのものの確認) Tests/SmokeTests.cs
TC-n.n 基本テストケース(OAuth 2.0 / OIDC の基本的な検証項目) Tests/Basic/
EX-n.n 拡張仕様(Revocation / Introspection / Device / Hybrid / response_mode / JWT Bearer / CIBA) Tests/Extended/
RT-<Issue>.n 個別 Issue の回帰(RT-186.2 なら #186 の 2 番目) Tests/Issues/

報告書の一覧と詳細、原本は、この順(SM → TC → EX → RT)に並ぶ。

土台から順に並べる。 SM が倒れていれば、TC の合否は読む意味がない。 サイトに届いていない・サインインできていない、という話であって、 仕様に適合しているかどうか以前だからである。 同じ理由で、拡張(EX)は基本(TC)の上に乗っている。 TC が倒れている状態の EX は、拡張の問題なのか土台の問題なのかを判断できない。RT も同じ。

先に出る群が倒れていたら、後ろは読まずに原因を潰す。

報告は 2 つに分かれている

説明と結果を混ぜない。

場所 いつ変わるか
原本(何を・何を根拠に確かめるのか) programs\Tests\TESTCASES.md テストを変えたときだけ。リポジトリに入れる
報告(その回に何が起きたか) Result\E2ETests.report.md 実行のたび。.gitignore 済み

観点・根拠・手順は実行しても変わらないので、毎回刷り直さない。 報告は「検証・観測の期待と実測」だけを持ち、冒頭から原本へリンクする。

妥当性を評価するときは、両方を渡すこと。

原本はテストの記録から生成する。テスト コードが一次情報である。

Skip にしているテストは実行されないので、記録が出ない。 そのぶんは「保留中のテストケース」として、Skip の理由から別枠で載る。 このため Skip の理由は「未修正」で始め、Issue 番号・実測日・実測結果を書く

.\2_RunAllTests.ps1 -Launch -UpdateTestCases

net48 版も同時に測る

-Launch は 2 つのサイトを立てる。 原本のケースを、両系統に同じだけ流す。 実測 2026/10/01 : 439 件(原本 221 件 × 2 − 片系統だけのもの)。数は増え続けるので、ここに書いた値は目安である(正確な数は実行結果と TESTCASES.md を見る)。

対象 待ち受け 立て方
net10.0 https://localhost:44300 Kestrel(ビルド済みの MultiPurposeAuthSite.exe)
net48 https://localhost:44302 IIS Express

dotnet run は使わない。 アプリを子プロセスとして起動するため、親(dotnet)を止めてもアプリが残り、 次回の起動がポートを奪われる。ビルド済みの exe を直接起動すれば、 止めた時点で確実に終わる。

報告書は 1 回の実行で上書きされる。 別々に測ると、後から測った方しか残らない。両方を残したいなら、同じ実行で測る。

net48 版は ASP.NET なので Kestrel では動かない。test.ps1 は IIS Express の雛形

%ProgramFiles%\IIS Express\config\templates\PersonalWebServer\applicationhost.config

を読み、サイト 1 つ分を書き換えて Result\applicationhost.config に出す。

  • 物理パス : programs\MultiPurposeAuthSite\MultiPurposeAuthSite
  • バインド : https の *:44302:localhost
  • 44300〜44399 は IIS Express の開発用証明書が http.sys に登録済みなので、そのまま使える

仮想ディレクトリは作らない。 待ち受け URL は環境変数で構成へ反映されるので(4 節)、 /MultiPurposeAuthSite の下に置く必要がない。アプリはサイト直下に置く。

立てられないときは、理由を出してその対象を Skip する。 失敗にはしない。

状況 見るところ
-NoNetFx を付けた 意図的に測らない
IIS Express が無い %ProgramFiles%\IIS Express\iisexpress.exe
net48 版がビルドされていない 1_BuildAll.ps1(bin\MultiPurposeAuthSite.dll)

.vs を消すと、Visual Studio 用の applicationhost.config は消える。 1_DeleteDir.bat の削除対象に .vs が入っているため。 test.ps1 が使うのは自前で作る方なので、こちらは影響を受けない。

mTLS(FA-6)と、net48 版の -NetFxMtls

mTLS(クライアント証明書)のテストは、既定では net10.0 版だけを測る(#226)。 -Launch は、テスト専用のフック programs/Tests/MtlsTestHook を net10.0 版にだけ読ませ、 発行元を問わずにクライアント証明書を受け付けさせる(アプリのコードは変えない)。 証明書はテストがその場で作る自己署名のもので、証明書ストアには入れない。

net48 版(IIS Express)は、準備だけを手動で行い、-NetFxMtls を付けて回す。 IIS は信頼できない証明書を、アプリより前で HTTP 403.16 として断る。 自己署名の証明書を通す設定は IIS に無いので、テスト用 CA をコンピューターの信頼されたルートに入れる(管理者権限)。 -NetFxMtls を付けなければ、net48 版のケースは作らない(Skip にもならない)。

実施済み(2026/09/23。Windows 11 / IIS Express 10)。 net48 版でも FA-6 の 4 件が通った(-NetFxMtls 付きで 276 件 / 失敗 0 / Skip 0)。

測り直した(2026/09/29。#245 の段階 3) : 414 成功 / 失敗 0 / Skip 4 (Skip は RT-245.4 ×2 = C-10 未修正、RT-246.3 ×2 = mTLS フックの副作用。core 2 / netfx 2)。 既定の通し(410 / 0 / 3)と比べて、FA-6 の netfx が 5 件増え、RT-246.3 の netfx が Skip に回った。 手順は SetupNetFxMtls.ps1(下記)。

管理者権限と後片付けが要るため、通しの一部にはしていない。 -NetFxMtls を付けない限り、FA-6 の net48 版は測られない(net10.0 版は毎回測っている)。 失効の情報(CRL)を持たない証明書でも、IIS は通した。

クライアント証明書を要求させるのは /token と /userinfo だけ (applicationhost.config の location path="MPAS48/token" / "MPAS48/userinfo")。 /token は mTLS のクライアント認証、/userinfo は証明書に紐づくトークン(cnf)の照合に要る。 サイト全体に掛けない。 net48 版の FAPI2 の自己テスト(サーバが自分自身を HTTPS で呼ぶ)が 証明書を求められて止まり、RT-197.1 が 60 秒で時間切れになる(実測で切り分けた)。

なお、5.1 で起動待ちが 90 秒で失敗する不具合が、この手順で見つかって直っている(#226)。 証明書のネゴシエーションが入ると、サーバ証明書の検証がランスペースの無いスレッドで呼ばれるため、 ServerCertificateValidationCallback がスクリプト ブロックでは実行できない。 test.ps1 は、コンパイルしたデリゲートを使う(CODING.md 5 節)。

手順は SetupNetFxMtls.ps1 にある(#245 の段階 3 でファイルにした)。 シークレットは含まない。 私有鍵は証明書ストアの中で生成され、スクリプトには現れない (Trust が読む .cer は公開部分だけ)。

cd root
.\SetupNetFxMtls.ps1 -Action Prepare   # 通常の PowerShell。CA + クライアント証明書 2 枚を作る
.\SetupNetFxMtls.ps1 -Action Trust     # **管理者**。CA を信頼されたルートに入れる
.\2_RunAllTests.ps1 -Launch -NetFxMtls # 通常の PowerShell(証明書を作った利用者)
.\SetupNetFxMtls.ps1 -Action Cleanup   # **管理者**。必ず行う(信頼されたルートに残さない)

.\SetupNetFxMtls.ps1 -Action Check     # いま何が在るかを見る

Prepare と Trust を分けてあるのは、証明書の入る先が違うからである。 TestCertificate.ForTarget は、netfx のとき CurrentUser\My を Subject で引く。 管理者の PowerShell が別アカウントなら、証明書は別の利用者のストアに入り、テストから見えない (症状は「証明書がありません」)。同じアカウントで昇格するなら、まとめて実行しても同じ結果になる。

  • -UpdateTestCases は付けない。 原本に netfx の mTLS ケースが混ざる
  • Prepare / Check は、Subject が E2E の定数(Flows.cs / MtlsTests.cs)と一致することを確かめる。 綴りが違えば、その場で止まる
  • 別アカウントで Prepare / Trust をするなら、両方に同じ -CerPath を渡す(%TEMP% が違う)

期待する結果(既定の通しは 410 成功 / 失敗 0 / Skip 3)。

変わるところ 期待
FA-6.1〜FA-6.5 の netfx 5 件増えて成功(付けないと対象すら作られない)
RT-246.3 の netfx 成功 → Skip(netfx もクライアント証明書を要求するため。既知の副作用)
失敗 0 のまま

ID フェデレーション(#140 / #250 の段階 5)

E2E で駆動している(RT-140.4 〜 RT-140.7)。上流の IdP が要る。

RT-140.4 連携でサインインできる(PKCE(S256) と prompt=none を付けて要求していることも見る)
RT-140.5 二度目の連携でも同じ利用者になる(連携キーが (iss, sub) であること)
RT-140.6 上流が未サインインなら成立しない(prompt=none の意味)
RT-140.7 連携の認可応答にも iss が付く(#252 が実経路で効いていること)

上流を先に建てておくこと。 建っていなければ、この 4 件(×2 ターゲット)だけが Skip される。

cd store
.\3_PublishUpstream.ps1
docker compose up -d upstream

建て忘れると、黙って Skip される。 「全テスト OK」と出ても、連携は測れていない。 Skip の件数(サマリに出る)と、Skip の理由で気付くこと。

上流は「作り直さないと古いまま」である。 ここが一番踏みやすい。 アプリを直したら、必ず 3_PublishUpstream.ps1 と docker compose up -d --build upstream を回す。

実測(#250 の段階 5) : FormPost.cshtml を直した(#252)あと、上流を作り直さずに RT-140.7 を回して落ちた。下流は新しく、上流だけが古いという状態で、 「直したはずのものが直っていない」ように見える。

下流の設定は test.ps1 が差し込む(Set-IdFederationEnv)。

設定 値
OAuth2AndOidcClientID / Secret ターゲットごとに別のクライアント(redirect_uri は 1 件に 1 つのため)
IdFederationRedirectEndpoint そのサイトの URL + /Account/IDFederationRedirectEndPoint

上流のエンドポイント(IdFederation{Authorize,Token,UserInfo}Endpoint)は上書きしない。 構成ファイルの値が、そのままサイトの向き先である。 テストはその値を読んで上流を探すので、上書きすると、両者がずれたときに気付けなくなる。

上流側のクライアント登録は store/docker-compose.yml にある(IdFederationE2ECore / IdFederationE2ENetFx)。 雛形の IdFederation クライアントは手動確認(VS)用で、/MultiPurposeAuthSite 付きのまま残してある。

上流は preferred_username も返す(docker-compose.yml の UserClaimsMapping。#151 の段階 4)。 subject_types の既定が public になり、sub は利用者 ID になったので、 下流が新規に作る利用者名は、preferred_username から取る (無ければメアドの @ より前。CONFIGURATION.md「ID 連携・外部ログインで作られる利用者名」)。

入れ忘れても連携は成立する(鍵はメアドなので)。 変わるのは、新規に作られる利用者の名前だけである。

#140 の段階 3 で、この経路をまとめて直した (連携キーを (iss, sub) へ/認可応答の iss を検証/PKCE(S256) を追加/ 要求スコープを標準だけに/Helper のホスト書き換えを回避)。 ビルドと通し(414 件)で「他を壊していないこと」までは確かめたが、 経路そのものは動かしていない。

上流のコンテナは #250 の段階 2〜3 で建った(1 節「上流の IdP も store/ で立てる」)。 段階 4 で、下流の設定をそこへ向け、目視が net48 版・net10.0 版の両方で通った。 段階 5 で E2E に入れた(RT-140.4 〜 RT-140.7。5 節「ID フェデレーション」)。 上流を建て忘れると Skip されるので、そこだけは人が見ること。

目視の手順(#250 の段階 4)

  1. 上流を建てる。

    cd store
    .\3_PublishUpstream.ps1
    docker compose up -d upstream
  2. 上流でサインインしておく。 https://localhost:44301/Account/Login prompt=none で連携するので、先に上流のセッションが要る(無いと login_required)。

  3. 下流を VS から動かす(net48 版 / net10.0 版のどちらでも)。 どちらも https://localhost:44300/MultiPurposeAuthSite で待ち受けるので、 上流に登録済みの IdFederation クライアントの redirect_uri_code と一致する。

  4. 下流の /Account/Login で「ID連携でサインイン」を押す。

向け先は雛形に入れてある(_appsettings.json / _app.config)。書き換えは要らない。

Cookie の名前を、上流と下流で分けてある(#250 の段階 4)。 Cookie のスコープにポートは入らない(RFC 6265 §8.5)ので、 localhost:44300(下流)と localhost:44301(上流)は Cookie を共有する。 名前が同じだと、次の 2 つが起きる(どちらも実測した)。

同名の Cookie 症状
セッション(MultiPurposeAuthSiteCoreSession) 上流のサインインが下流のセッションを消す → state / nonce が読めず「エラー」画面
認証(.AspNetCore.Identity.Application) 後にサインインした側が相手を蹴り出す → 連携は正常終了するのに下流がサインイン状態にならない

docker-compose.yml が、上流に別名と接頭辞を与えている(#250 の段階 4 / #255)。 雛形の既定は空=従来どおりなので、1 サイトだけの配備には影響しない。

接頭辞は、名前を決められるものすべてに掛かる(実測)。

.upstream_MultiPurposeAuthSite                     認証(サインイン)
upstream_Identity.External                         外部ログイン・ID 連携の途中
upstream_MultiPurposeAuthSiteSession               セッション
upstream_auth_time / upstream_re_auth_at           max_age の判定
.upstream_AspNetCore.Mvc.CookieTempDataProvider    画面のメッセージ

先頭が . のものは、その後ろに接頭辞が入る(. は host-only を表す慣習なので潰さない)。

サインインの Cookie だけでは足りない。 外部ログイン(Identity.External)は ID フェデレーションの途中で使うので、ここが混ざると連携が壊れる。 auth_time は再認証の要否、TempData は画面のメッセージに効く。

分けられないものが 1 つ残っている。

Cookie いまの扱い
SessionTimeOut Open棟梁 の定数(FxHttpCookieIndex)。雛形は FxSessionTimeOutCheck を off にしており、読まれないので無害。分けるなら Open棟梁 側の対応が要る

net48 版は、そもそも同名になりにくい(実測)。 セッションは mas_session、AntiForgery は __RequestVerificationToken で、 net10.0 版の名前と重ならない。TempData は Cookie に載らない(セッションに載る)。 ただし net48 版どうしを同じホストに立てると、__RequestVerificationToken が衝突する。 いまの構成では起きない(上流は net10.0 版のコンテナ 1 つ)。

net48 版の下流では、もともと起きない(Owin の既定名が .AspNet.ApplicationCookie で、 net10.0 版と重ならないため)。net10.0 版の下流でだけ出る。

上流のコンテナを作り直す前に触っていたブラウザには、古い Cookie が残る。 直したあとも直らないときは、localhost の Cookie を消してから試すこと。

設定 値
IdFederationAuthorizeEndpoint https://localhost:44301/authorize
IdFederationTokenEndpoint https://localhost:44301/token
IdFederationUserInfoEndpoint https://localhost:44301/userinfo
IdFederationRedirectEndpoint https://localhost:44300/MultiPurposeAuthSite/Account/IDFederationRedirectEndPoint

/MultiPurposeAuthSite を外した(#250 の段階 4)。コンテナは root で配信する。 付いていたのは IIS Express の仮想ディレクトリの形で、上流の実体が無かった。

上流側は、下流を通さずに測ってある(#250 の段階 4)

下流がすることを、そのまま上流に対して行って確かめた。

手順 結果
上流でサインイン OK
/authorize(prompt=none / PKCE S256 / response_mode=form_post) 200。code と state が自動送信フォームで返る
/token(code + code_verifier + Basic 認証) access_token / id_token / refresh_token
id_token の iss / aud / nonce https://ssoauth.opentouryo.com / 06d2…(一致)/ 一致
/userinfo の sub id_token の sub と一致(OIDC Core §5.3.2)
/userinfo の email_verified true(C-23 の判定を通る)
code_verifier を外した /token 400 で拒否(C-22 の守り)

残っているのは「下流がこれを受け取って、利用者を作る/結び付ける」ところだけである。

失敗したら、下流の OPERATION ログを見る(#253)

Error 画面が出たときの理由は、すべて OPERATION ログに出る(C:\root\files\resource\Log\OPERATION.<日付>.log)。

The state of the authorization response did not match the session. (response: len=32, session: (empty))
The id_token of the ID federation was not accepted. (verified: True, nonce matched: False)
The token response of the ID federation had no id_token.
The iss of the authorization response did not match the expected issuer.
The sub of /userinfo did not match the sub of the id_token.
The id_token had no iss claim.
The ID federation redirect endpoint is locked down. (IsLockedDownTestEndpoints)
The ID federation did not complete. (the error view was returned)

最後の 1 行は、経路の終わりを示す受け皿である。 それだけが出ていたら、利用者の作成か外部ログインの追加に失敗している(そこは AddErrors するだけで画面に出ない)。

state / nonce の値そのものは出さない((empty) か len=<長さ> だけ)。 切り分けに要るのはそこまでである。

目視で 2 つ見つけた(#250 の段階 4)。どちらも E2E では出なかった。

見つけたもの 出る側
Cookie の名前が上流と下流で同じ(認証 / セッション) net10.0 版の下流だけ(net48 版は Owin の既定名が違う)
OAuth2AndOIDCClient.HttpClient が初期化されていない net48 版だけ(net10.0 版は Program.Main で入れている)

後者は #140 の段階 3 の副作用である。 ID フェデレーションが Helper を通さなくなり、 Helper のコンストラクタが設定していた HttpClient が入らなくなった(/token で null 参照)。 Global.asax.cs の Application_Start で、net10.0 版と同じように入れるようにした。

この 2 つは、E2E に入れていれば見つかった。 段階 5 の理由がここにある。

sub は利用者 ID(GUID)である。 上流の IdFederation クライアントに subject_types の登録が無く、既定(public)に従うため(#151 の段階 4。 それより前は利用者名=メアドが入っていた)。 連携キー (iss, sub) はこの値で作られる。

既定を変えると、既存の連携は鍵が合わなくなる。 その場合はメアドで引き直して、新しい鍵を足す (下流の IDFederationRedirectEndPoint。上流が email_verified を言っている必要がある)。 つまり張り直しは自動で起きるが、上流が検証済みと言わない場合は結び付けない。

id_token には email / email_verified が入らない。 下流は /userinfo から読むので、C-23 の判定はそちらで通る。

上流はコンテナ 1 つで、下流は 2 つとも E2E が立てるサイトである。 -Launch が立てる 2 サイト(net48 / net10.0)を、どちらも同じ上流へ向ける。

Cookie は 1 つの入れ物で扱う(IdPClient の CookieContainer。ブラウザと同じ)。 上流と下流が同じホストでも成り立つのは、Cookie 名を分けたからである(#250 の段階 4)。

subject_types の既定(#151 の段階 4)

既定は public である(#151 の段階 4 で変え、段階 5 で独自値を廃止した)。 E2E で 2 件測っている。

RT-151.1 subject_types を書かないクライアントの sub が、利用者名ではなく利用者 ID(GUID)である
RT-151.2 public の sub は、クライアントが違っても同じ(pairwise との対照)

使うのは TestClient_6 / TestClient_7(test.ps1 -Launch が差し込む。構成ファイルには無い)。

既に使った client_id では、既定を測れない。 発行した sub は対応表に記録されるので(#151 の段階 2)、 設定を変えても、その組み合わせでは以前の値が返る(それが段階 2 の目的である)。 新しい client_id を使うと、表に行が無いので、新しい既定で作られる。

だから、既定の判定は「新しい client_id でだけ」行うこと。 TestClient や MVC_Sample で sub の値を決め打ちすると、 mem では新しい既定、使い回した DB では以前の値になり、ストアによって結果が変わる。

sub の値そのもので「誰か」を判定しているテストは、すべて直した(段階 4)。

直したところ いまの判定
TC-6.2(id_token の必須クレーム) sub が /userinfo の sub と一致する(OIDC Core §5.3.2)
TC-6.5 / RT-196.7(/userinfo) sub が id_token の sub と一致する
EX-4.3(Device AuthZ) /userinfo の email が承認した利用者である
TC-4.1(ROPC) トークンの email が認証した利用者である

sub は「同じ利用者・同じ RP なら同じ値」であることに意味がある。 値の形は、配備(既定を変える前か後か)によって違う。

有効期限(RT-188)と -ShortLifetimes

既定の寿命(認可コード 600 秒・Request Object 300 秒・refresh_token 14 日)を待つのは現実的でない。 そこで -ShortLifetimes が、寿命をごく短くしてサイトを起動する(#188)。

.\2_RunAllTests.ps1 -Launch -ShortLifetimes -Filter "FullyQualifiedName~LifetimeTests"
  • -Filter と併せて使う。 寿命が短いので、他のテストは落ちる
  • 既定の通しでは、RT-188 を除外している(test.ps1 が FullyQualifiedName!~LifetimeTests を足す)。 既定の寿命では測れず、xUnit は「ケースが 0 件の Theory」を失敗として数えるため
  • そのため RT-188 は TESTCASES.md(原本)に載らない。 原本は通しの結果から作るので、-ShortLifetimes の実行で -UpdateTestCases を付けないこと (付けると、その 6 件だけの原本に置き換わる)

実測 2026/09/29(#245 の段階 3) : 6 成功 / 失敗 0 / Skip 0(mem)。 net48 版も 44302 で起動した。 以前に見られた起動待ちの時間切れは、再現しなかった。

取り違えは検出する

net48 版と net10.0 版は、構成ファイルの既定ではどちらも同じ URL を指している。 -Launch は別のポートへ寄せるが、手で立てた場合や testsettings.json で URL を指定した場合は、同じ URL を 2 回測ることが起こり得る。

そのままだと同じアプリを 2 回測って「両方 OK」と報告する。(実際にやった)

このため、応答ヘッダでどちらのアプリが応答したのかを確かめている。

見分け方
net48 X-AspNet-Version が付く
net10.0 Server: Kestrel

期待と食い違う場合は、そのターゲットを Skip して理由を残す。

net10.0版 (MultiPurposeAuthSiteCore) のはずの https://localhost:44302 に、
net48版(ASP.NET Framework)が応答しました。同じURLで構成されているため取り違えます。
片方を別のURLにするか、順番に実行してください。

報告書の「叩いた先」も、-Url の値ではなく、実際に応答したアプリから作る。 引数を書き写すだけでは、測れていない対象まで「叩いた」ことになってしまう。

| 叩いた先 | net10.0版 (MultiPurposeAuthSiteCore) (https://localhost:44300)
            / 応答: net10.0(Kestrel)
            net48版 (MultiPurposeAuthSite) (https://localhost:44302)
            / 応答: net48(ASP.NET Framework / Microsoft-IIS/10.0) |

個々のテストの記録にも残る。

対象: net48版 (MultiPurposeAuthSite) (https://localhost:44302)
      / 応答: net48(ASP.NET Framework / Microsoft-IIS/10.0)

TRX を読む

コンソールの集計行(テストの合計数: ...)はロケールで変わる。 TRX(XML)の outcome は Passed / Failed / NotExecuted で固定なので、こちらを読む。

test.ps1 -TrxPath が出力先を受け取る。2_RunAllTests.ps1 がそれを渡している。

合否

条件 判定
失敗 0 件、かつ成功 1 件以上 OK(終了コード 0)
失敗 1 件以上 NG(1)
成功 0 件 NG(1)
TRX が出ていない NG(1)。テストの起動そのものに失敗している

成功 0 件を NG にしているのが肝。 サイトを起動し忘れると全件 Skip になり、「失敗 0 件」で緑に見えてしまう。 それがいちばん危ない。

Skip は失敗ではない

テストは net10.0 版と net48 版の両方に同じものを流す([SkippableTheory] + AllTargets)。 クロスコンパイルで下位互換版を維持しているので、 「片方だけ直っている」状態を検出できることを最優先にしている。

起動していない対象は Skip する。net48 版は IIS Express での手動起動が前提で、 常に動いているとは限らないため、そこで落とさない。

Skip は 2 種類ある。混ぜて数えると、どちらも見えなくなる。

  Skip 30 件の内訳
        28  netfx          ← そのサイトが起動していないだけ(環境)
         2  (対象なし)     ← 未修正と分かっている項目(仕様)

2_RunAllTests.ps1 は、テスト名の targetKey: "..." で切り分けて内訳を出す。

6. 未修正の項目の扱い

未修正だと分かっている項目は、期待する動作を書いたうえで Skip にする。 消さずに残すのは、直したときに Skip を外すだけで検証できるようにするため。

Skip の理由には、Issue 番号・実測日・実測結果を書く。

[SkippableTheory(Skip = "未修正(#197)。実測(2026/09/09, net10.0)では、"
    + "誤った redirect_uri を送ってもトークンが発行される。")]

現在 Skip にしているものは programs/Tests/README.md の「未修正の項目」にある。

7. 実行結果の例

================ サマリ ================

対象     結果 成功 失敗 Skip   秒
-------- ---- ---- ---- ---- ----
E2ETests OK    220    0    1 146.7

  Skip 1 件の内訳
         1  (対象なし)

  対象ごとの Skip は、そのサイトが起動していないだけのことが多い。
  (対象なし) は、未修正として Skip 指定しているもの(Tests\README.md)。

  所要時間 : 2.4 分
  TRX      : C:\MultiPurposeAuthSite\root\programs\Tests\E2ETests\Result\E2ETests.trx
  ログ     : C:\MultiPurposeAuthSite\root\programs\Tests\E2ETests\Result\E2ETests.log
  報告書   : C:\MultiPurposeAuthSite\root\programs\Tests\E2ETests\Result\E2ETests.report.md
  原本     : C:\MultiPurposeAuthSite\root\programs\Tests\TESTCASES.md

  全テスト OK

8. 前提条件

前提 備考
ビルド済み 1_BuildAll.ps1、または Visual Studio
設定ファイル appsettings.json / app.config。テストはここから資格情報を読む
UserStoreType 既定は mem。テスト ユーザは初回アクセスで作られ、再起動で消える。sql / ora / npg に切り替えるときは 1 節「ストアを切り替える」(#207)
証明書 SpRp_RsaPfxFilePath の pfx。Request Object の署名に使う
FxContainerization ON。待ち受け URL の上書きに要る(4 節)。雛形には入っている
IIS Express net48 版を測るときだけ。無ければその分が Skip される

-Launch は、mTLS のテスト(FA-6)のために net10.0 版にクライアント証明書を要求させる (Tests\MtlsTestHook。#226。net48 版は -NetFxMtls のときだけ)。 その状態では、アプリ自身の内部呼び出し(自己テスト)も証明書を提示する (Helper の HttpClient が SpRp_ClientCertPfxFilePath を添える)。 サーバは証明書を提示したクライアントをコンフィデンシャル扱いにするので、 公開クライアントで始める自己テストが invalid_client になる(/device_authz の TestClient3)。 製品の欠陥ではなく、測り方の都合である。 そのため RT-246.3 は、要求している側を Skip する。

構成ファイルの既定では、net10.0 版と net48 版は同じ URL を指している。 -Launch は環境変数で別のポートへ寄せるので、同時に測れる。 手で立てるときは、片方を別の URL にすること。 取り違えは検出して Skip する(5 節「取り違えは検出する」)。

9. 秘密情報を出さない

TestUserPWD / client_secret / pfx のパスワードは、 実行時にアプリ自身の構成ファイルから読み出す。 テスト側は値を持たない。

client_id も直書きしない。環境ごとに違う(CreateClientsIdentity.exe で生成する)ので、 client_name(TestClient / MVC_Sample など)から引く。

テストの出力にトークンや秘密情報を書かないこと。 JsonResponse.ToString() はキー名とエラーだけを出す。

HTTP 200 / keys=[access_token, expires_in, id_token, refresh_token, token_type] / error=-

10. テストを足すとき

  1. programs/Tests/E2ETests/Tests/ に置く。ファイル ヘッダは既存に合わせる(CODING.md)
  2. TargetTestBase を継承し、[SkippableTheory] + [MemberData(nameof(AllTargets))] にする
    • net10.0 版でしか成立しないものだけ CoreOnly を使う
  3. client_id は Flows.Registration(client, KnownClients.XXX) で引く
  4. 未修正の挙動を見つけたら、期待する動作を書いて Skip。 Issue を起こして番号を書く
  5. ANALYSIS-IdP.md にも反映する(あちらが適合上の穴の一覧)

画面(HTML)の中身を日本語で判定するときは、実体参照を戻してから比べる。 net10.0 版の Razor は非 ASCII を数値文字参照(&#x8A8D; など)で出すが、net48 版はそのまま出す。 そのため html.Contains("認証要求が…") は net48 版だけ通る(RT-246.2 で踏んだ)。 System.Net.WebUtility.HtmlDecode を通してから判定する。