社内は ADFS の OpenID Connect(OIDC)で統一できているが、取引先や委託先は自社の SAML 2.0 IdP を使いたい――そんな“プロトコル混在”の現場は珍しくありません。本記事は、既存の ADFS 登録済み OIDC アプリに外部 SAML IdP 認証を安全に追加するための設計指針・実装手順・トラブル対処までを、実運用の視点で徹底解説します。
シナリオと到達目標
到達目標は「同じ Web / ネイティブ アプリで、社内ユーザーは従来どおり OIDC(AD FS)で、外部ユーザーは各社の SAML 2.0 IdP でサインインできる」構成です。AD FS を「ブリッジ(中継)」として用い、外部 SAML のアサーションを AD FS 側で受け、アプリが理解できる OIDC の ID トークン(JWT)へ発行し直します。これによりアプリ側は issuer(iss)= 自社 AD FS からのトークンだけを検証すればよく、実装が簡潔でセキュアになります。
全体アーキテクチャ(要点)
- Claims Provider(CP): 外部 SAML IdP を AD FS に「クレーム プロバイダー信頼」として登録。
- Relying Party(RP): アプリは AD FS に対する RP(OIDC クライアント)として既存利用。必要に応じて同一サービスの SAML 用 RP を追加(アプリが SAML も直接受けたい場合のみ)。
- HRD(ホーム レルム ディスカバリ): ユーザー属性やクエリ パラメータで内部/外部を自動振り分け。
- トークン変換: 外部からの SAML アサーションを AD FS で検証→アプリ向けに OIDC ID トークンを発行。
採用しやすい実装パターン比較
| パターン | 概要 | 長所 | 留意点 | 適合場面 |
|---|---|---|---|---|
| A. ブリッジ構成 (推奨) | 外部 SAML IdP を CP として追加。既存 OIDC クライアント(アプリ)はそのまま。AD FS が SAML→OIDC 変換。 | アプリは OIDC のまま・発行者統一・保守容易。 | クレーム マッピング設計が必要。 | 最も一般的。将来の外部 IdP 追加にも強い。 |
| B. 併存構成 | 同アプリで OIDC と SAML の両 RP を AD FS に作成し、UI で入口を切替。 | 既存パートナーの SAML フローを崩さない。 | アプリ側も SAML を理解する必要。運用複雑化。 | 歴史的理由で SAML を直接受けたい場合。 |
| C. SAML Bearer Assertion 交換 | 外部 IdP が発行した SAML アサーションをアプリが AD FS トークン エンドポイントへ持ち込み、OIDC トークンに交換。 | コード/バックエンドで柔軟に制御可能。 | 実装負荷が高い。適合/制約の確認が必要。 | API 中心・B2B マシン間連携など。 |
事前に決めておくべきこと(成功の鍵)
- 一意なユーザーキー(sub)の定義: 例)社内=UPN、外部=メールアドレスまたは SAML の persistent NameID。
両者が衝突しないよう、外部はidentityproviderと組み合わせてアプリ側でネームスペース分離するのが堅実。 - 必要クレームの最小化:
email/name/given_name/family_name/groups/roleなど。最小権限の原則で。 - MFA の取り扱い: 外部 IdP の多要素を AD FS 側へ伝播する場合は
authnmethodsreferences(http://schemas.microsoft.com/claims/authnmethodsreferences)を活用。 - ライフサイクル: 外部 IdP の署名証明書ロールオーバー、メタデータ有効期限の監視、障害時フェールセーフ。
実装手順(詳細)
手順の俯瞰
| 手順 | 概要 | 重要ポイント |
|---|---|---|
| 1 | 外部 IdP を Claims Provider Trust として追加 | メタデータ URL/XML を登録。署名証明書の信頼と有効期限の監視を設定。 |
| 2 | アプリの RP 整備(既存 OIDC の確認/必要により SAML RP を追加) | OIDC のリダイレクト URI、SAML の ACS URL を正確に設定。スコープ/クレームの設計。 |
| 3 | HRD(ホーム レルム ディスカバリ)で内部/外部を振り分け | UPN ドメイン、whr/login_hint、クエリ ?idp= などで分岐。UI ボタン実装も可。 |
| 4 | トークン変換(SAML → OIDC) | 受入クレーム整形 → OIDC ID トークン発行規則でマッピング。identityprovider も発行。 |
| 5 | アプリ側の実装/設定 | iss は自社 AD FS で固定。sub の一意性ポリシー、identityprovider によるテナント分離、ロール/権限の割当。 |
1. 外部 IdP を「クレーム プロバイダー信頼」に追加
GUI の例
- AD FS 管理コンソール > 信頼関係 > クレーム プロバイダー信頼 > 追加。
- 外部 IdP のメタデータ URL または XML を指定。
- 発行者名(表示名)をわかりやすく命名(例:「Partner-Contoso」)。
- 証明書(署名)を確認し、メタデータの自動更新を有効化。
PowerShell の例
# メタデータから CP を追加
Add-AdfsClaimsProviderTrust `
-Name "Partner-Contoso" `
-MetadataUrl "https://idp.contoso.example/saml/metadata" `
-AutoUpdateEnabled $true
# 監査のため表示名などを整理
Set-AdfsClaimsProviderTrust -TargetName "Partner-Contoso" -OrganizationInfo "Contoso Ltd."
受入(Acceptance Transform)クレーム規則
外部 IdP から受けた SAML アサーションを社内標準のクレームへ合わせます。最低限、email/nameidentifier/givenname/surname を正規化しましょう。
# NameID が email 形式なら email クレームへ
c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
Properties["http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format"]
== "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"]
=> issue(Type = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", Value = c.Value);
# givenName, surname をパススルー
c:[Type == "[http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname](http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname)"]
=> issue(claim = c);
c:[Type == "[http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname](http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname)"]
=> issue(claim = c);
# 外部 IdP 名を保持(後段の発行で使う)
# identityprovider は AD FS が自動付与するが、明示的に発行する場合
c1:[Type == "[http://schemas.microsoft.com/identity/claims/identityprovider](http://schemas.microsoft.com/identity/claims/identityprovider)"]
=> issue(claim = c1);
2. アプリの RP 整備(OIDC 既存の確認/必要により SAML RP 追加)
AD FS 2016/2019 以降では、OIDC/OAuth クライアントは「アプリケーション グループ」で管理され、SAML は「リライング パーティ信頼」です。本記事では便宜上まとめて「RP」と表記します。
OIDC クライアント(既存)の確認ポイント
- リダイレクト URI(
https://app.example.com/callbackなど)が最新の本番/検証 URL と一致。 - 必要スコープ(
openid、profile、emailなど)を定義。 - ID トークン発行規則(後述)で、アプリが期待するクレームを発行。
(任意)SAML RP の追加
アプリが SAML も直接受けたい場合のみ、SAML 用の RP を追加します(ACS URL、エンティティ ID、署名/暗号化証明書を設定)。
# SAML RP を手動定義する例(メタデータが無い時)
Add-AdfsRelyingPartyTrust `
-Name "App-SAML" `
-Identifier "urn:example:app" `
-AssertionConsumerServiceUrl "https://app.example.com/SAML/acs"
3. HRD(ホーム レルム ディスカバリ)による内部/外部の自動振り分け
AD FS では、HRD を有効化し、OIDC/SAML/WS-Fed いずれでもクエリ whr(Home Realm)で IdP を指定できます。UX を損ねない分岐が肝です。
実装パターン
| パターン | 手法 | 例 | 向き/不向き |
|---|---|---|---|
| 自動振り分け | UPN ドメインで判断 | @partner.example は外部へ、@corp.local は内部 | メールドメインが明確なときに有効 |
| パラメータ指定 | whr / login_hint / ?idp= | /adfs/oauth2/authorize?...&whr=Partner-Contoso | ディープリンクや B2B 専用入口に最適 |
| UI 切替 | AD FS ログインページに「社内」「外部」ボタン | ボタンで whr 付与 URL に遷移 | 利用者が混在する汎用入口で有効 |
設定例(PowerShell)
# HRD(ドメイン ヒント)を使えるよう Web 設定を確認
Set-AdfsWebConfig -HRDSelectionEnabled:$true
# ログインページに「外部ログイン」ボタンを追加するカスタム テーマの準備(抜粋)
New-AdfsWebTheme -Name "CorpTheme" -SourceName "Default"
Set-AdfsWebTheme -TargetName "CorpTheme" -AdditionalFileResource @{Uri="/adfs/portal/script/onload.js";Path="C:\adfs\theme\onload.js"}
Set-AdfsWebConfig -ActiveThemeName "CorpTheme"
onload.js のイメージ(抜粋)
document.addEventListener("DOMContentLoaded", function () {
var ext = document.createElement("a");
ext.textContent = "外部パートナーでサインイン";
ext.className = "btn btn-secondary";
// Partner-Contoso という CP 名を whr で指定
ext.href = window.location.origin + "/adfs/ls/?whr=Partner-Contoso";
var container = document.querySelector("#content");
if (container) container.prepend(ext);
});
4. トークン変換(SAML → OIDC)とクレーム マッピング
外部 IdP から受けたクレームを、OIDC の ID トークン(JWT)として発行する規則を設定します。AD FS 2019 以降なら GUI で「ID トークン発行変換規則」「アクセス トークン発行変換規則」を編集できます。
代表的なマッピング方針
| OIDC クレーム | 元クレーム(受入) | 変換/注意点 |
|---|---|---|
sub | nameidentifier(または emailaddress) | 一意性が重要。外部は persistent NameID を推奨。 |
name | givenname + surname | 無ければ displayname を採用。 |
email | emailaddress | ドメイン検証・禁止ドメイン遮断を推奨。 |
amr(認証方法) | authnmethodsreferences | 外部 IdP 側 MFA を表現。ポリシー判定に利用可。 |
identityprovider | AD FS 既定 | 外部/内部の識別に必須。監査にも有用。 |
ID トークン発行規則(例)
# sub を NameID から作成(SAML 側が persistent ID を出す前提)
c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"]
=> issue(Type = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", Value = c.Value);
# email をパススルー
c:[Type == "[http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress](http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress)"]
=> issue(claim = c);
# 氏名の整形(givenname/surname がある場合)
c1:[Type == "[http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname](http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname)"],
c2:[Type == "[http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname](http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname)"]
=> issue(Type = "[http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name](http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name)", Value = c1.Value + " " + c2.Value);
# 認証方法参照(MFA 伝播)
c:[Type == "[http://schemas.microsoft.com/claims/authnmethodsreferences](http://schemas.microsoft.com/claims/authnmethodsreferences)"]
=> issue(claim = c);
# identityprovider を発行(AD FS 既定のものを明示)
c:[Type == "[http://schemas.microsoft.com/identity/claims/identityprovider](http://schemas.microsoft.com/identity/claims/identityprovider)"]
=> issue(claim = c);
※ クレーム ルール言語では単純な加算連結が効かない環境があります。その場合は 名前 の作成を displayname に切り替えるなど、環境に合わせて調整してください。
(参考)SAML Bearer Assertion グラントの考え方
外部 IdP で認証後、アプリ(サーバー側)が受け取った SAML アサーションを AD FS のトークン エンドポイントへ提出し、OIDC トークンに交換する方式です。プロトコルの理解と実装が必要なため、まずは前述の「ブリッジ構成」を推奨します。
POST /adfs/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:saml2-bearer&
assertion=BASE64ENCODED_SAML_ASSERTION&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
scope=openid profile email
5. アプリ側の実装ポイント
- トークン検証:
iss(発行者)は常に自社 AD FS。署名鍵は AD FS の公開鍵のみを信頼リストに。 - 外部/内部の区別: ID トークンの
identityprovider(例:AD AUTHORITY / Partner-Contoso)で判定。 - ユーザー一意性:
subの衝突を避ける。内部は UPN、外部は persistent NameID/email を標準とし、必要に応じてアプリ側でテナント名と組合わせて論理キー化。 - 権限付与: 役割(
role)やグループ(groups)は最小権限で。外部は専用ロールへスコープを限定。 - UX: サインイン入口を 1 本にまとめる場合も、UI で「社内」「外部」を選択できる動線を用意すると問合せが減ります。
セキュリティとコンプライアンスの勘所
- 証明書ロールオーバー監視: 外部 IdP のメタデータ有効期限と署名証明書の期限を定期監視。AD FS のメタデータ自動更新が失敗した場合の手順書を整備(手動入替)。
- MFA の扱い: 外部 IdP 側の MFA を尊重するか、AD FS 側で追加の MFA を強制するかを明確化。
authnmethodsreferencesを用い、アプリ側で高リスク操作時のみ MFA 必須にする設計も可。 - NameID 形式整合: 外部と内部で NameID 形式(email/persistent/unspecified)を合わせて、
subの一貫性を確保。 - 監査ログと個人情報: ログ保管場所・期間・マスキングのポリシーを定義。誰がどの IdP で認証したか を追跡可能に。
- セッションとログアウト: OIDC の end_session_endpoint によるログアウトと、外部 SAML IdP 側のセッションをどう扱うか(完全連動するか分離するか)を決める。
テスト計画とトラブルシューティング
テスト観点
- 外部/内部ともに 1 回目サインイン成功、その後のリダイレクトと Cookie の整合性(SameSite 属性含む)。
- HRD の自動/手動分岐が期待どおりか(
whr、UPN ドメイン、UI ボタン)。 - ID トークンの中身(
sub/email/identityprovider/amr)が要件通りか。 - 外部 IdP 側のユーザー属性不足(例:surname が無い)時のリカバリー。
- 時計ずれ(NotBefore/NotOnOrAfter)に対する寛容度(スキュー)。
ログと解析
- AD FS 要求の追跡: 管理コンソールから有効化し、関連するトークン処理のトレースを収集。
- イベント ビューア: アプリケーションとサービス ログ > AD FS/Admin。イベント ID 364(トークン発行失敗)は最初に確認。
- ブラウザ/ネットワーク: 開発者ツールや Fiddler で
SAMLResponseと OIDC のid_tokenを比較。
よくあるエラーと対処
| 症状 | 原因 | 対処 |
|---|---|---|
| イベント 364 / 署名検証失敗 | 外部 IdP の署名証明書が更新済み | メタデータを再取得、手動で証明書を更新 |
| HRD がループ | whr 指定ミス/CP 名不一致 | CP の表示名と whr 値を一致させる |
| クレーム不足でアクセス拒否 | 外部 IdP から必須属性が来ない | 受入規則で代替を定義/外部 IdP へ属性要求 |
| アプリで audience 不一致 | クライアント ID/リダイレクト URI 相違 | AD FS とアプリの登録値を再同期 |
| 時間不一致で無効 | サーバー間の時刻同期不良 | NTP を統一、許容スキューを見直し |
実運用のベストプラクティス
- パートナーごとに CP を分離: CP 名に企業名を付け、権限と監査を分ける。
- Access Control Policy: 外部は発行先 RP を限定、特定クレーム所持者のみ発行許可。
- スコープ設計: openid を最小単位に、必要時のみ email/profile を要求。
- サンドボックス: まずは検証用 AD FS/アプリで受入規則・発行規則を完成させてから本番へ。
- ドキュメント化: CP メタデータ URL、証明書期間、連絡先(技術/運用)を台帳化し、年次レビュー。
実践コピペ用:代表コマンド集
# --- 外部 CP の作成 ---
Add-AdfsClaimsProviderTrust `
-Name "Partner-Fabrikam" `
-MetadataUrl "https://idp.fabrikam.example/federationmetadata/2007-06/federationmetadata.xml" `
-AutoUpdateEnabled $true
# --- OIDC クライアントの作成(機密クライアント例) ---
Add-AdfsClient ` -Name "App-OIDC"`
-ClientId "app-client-id" ` -ClientSecret (ConvertTo-SecureString "your-secret" -AsPlainText -Force)`
-RedirectUri @("[https://app.example.com/callback](https://app.example.com/callback)")
# --- HRD を有効化 ---
Set-AdfsWebConfig -HRDSelectionEnabled:$true
# --- ログ強化(必要に応じて) ---
Set-AdfsProperties -LogLevel Verbose
設計チェックリスト(配布可)
- [ ]外部 IdP メタデータ/証明書/連絡先を入手し台帳化した
- [ ]CP を作成し、受入クレームを標準化した
- [ ]OIDC クライアント(既存)のスコープ/リダイレクト URI を見直した
- [ ]HRD ルール(UPN ドメイン or
whr)を決め、UI に反映した - [ ]ID トークン発行規則で
sub/email/identityproviderを発行 - [ ]MFA の扱い(外部伝播 or 追加要求)を決定しテストした
- [ ]監査・障害対応(イベント 364、メタデータ更新失敗時)の手順を整備した
- [ ]本番前にパートナーと相互テストを実施し、スクリーンショットとトークン例を共有した
まとめ
既存の ADFS + OIDC アプリに外部 SAML IdP を安全に取り込む最短ルートは、外部 IdP を Claims Provider として追加し、HRD で振り分け、AD FS で SAML→OIDC をブリッジすることです。アプリは発行者を AD FS に一本化でき、セキュリティ監査も容易になります。
クレーム設計(sub の一意性)と HRD 体験(自動/手動の併用)を先に固めれば、導入はスムーズに進みます。あとはメタデータと証明書のライフサイクル管理、ログ/監査の運用を整えれば、内外ユーザーが同じサービスを無理なく安全に利用できる体制が完成します。
付録:要点のコンパクト表
| 項目 | 推奨/要点 |
|---|---|
| 外部 IdP 追加 | CP(クレーム プロバイダー信頼)としてメタデータ登録、自動更新 |
| アプリの RP | OIDC は既存を継続。SAML RP は必要時のみ |
| HRD | whr / ドメイン / UI ボタンの併用 |
| トークン変換 | SAML 受入 → ID トークン発行規則で OIDC へマップ |
| アプリ実装 | iss=AD FS 固定、identityprovider で外部判定、権限は最小 |
| セキュリティ | 証明書ロールオーバー監視、MFA 伝播/強制、監査ログ整備 |
| トラブル対応 | イベント 364、クレーム不足、HRD ループ、時間ずれを優先確認 |
FAQ(現場でよく出る疑問)
Q: アプリのサインイン URL は 1 本にできますか?
A: 可能です。入口は OIDC の認可エンドポイント 1 本にまとめ、HRD で外部を外部 IdP へ、内部は AD FS へ誘導します。アプリ側は受け取る ID トークンの identityprovider で発行元(内部/外部)を判断できます。
Q: 外部パートナーごとに権限を分けたい。
A: CP ごとに identityprovider が異なるため、それをロール付与のキーとして使う設計が簡潔です。必要に応じてパートナー固有のグループ/属性をマッピングして権限を分岐します。
Q: 将来 Azure AD / Entra ID など別 IdP を追加したい。
A: 新しい IdP ごとに CP を追加し、HRD のルールに合流させるだけで対応できます。アプリは AD FS 発行の OIDC トークンを受けるだけなので変更は最小です。
本記事の手順・ルール例は、AD FS 2016/2019 以降の一般的な構成を念頭に解説しています。実運用では環境差(既存規則やポリシー)に合わせて調整してください。

コメント