はじめに — 「SSOは使えますか?」という質問の本当の意味
B2B SaaSを運営していると、エンタープライズ営業の段階で必ず受ける質問があります。「SSOに対応していますか?」 このSSOは「Googleでログイン」のようなソーシャルログインではありません。顧客が尋ねているのはこれです。
「弊社の従業員が、**弊社のIdP**(Okta、Entra ID、自社Keycloakなど)を使って御社のサービスにログインできますか?」
つまり、マルチテナントSaaSにおけるSSOとは、**テナント(顧客企業)ごとに異なる外部IdPを連携するフェデレーションアーキテクチャ**を意味します。単一のIdPをつなぐ社内SSOとはまったく別次元の設計問題です。2026年現在、エンタープライズのセキュリティチェックリストにおいてSSO(そしてSCIM)は事実上の入札資格要件となり、passkeysのデフォルト化やZero Trustの流れの中で「顧客のIdPが認証を所有する」という原則はさらに強まっています。
本記事では、テナントごとのIdP設定モデル(realm-per-tenant vs Keycloak Organizations)、home realm discovery、ドメイン検証、JIT vs SCIM、セルフサービスオンボーディングUI、そして料金プランのSSO tax論争まで、マルチテナントSSOの全体像を描きます。
アーキテクチャの全体俯瞰
まず登場人物を整理します。
顧客A (acme.com) 当社SaaS 顧客B (globex.io)
+--------------------+ +---------------------+ +--------------------+
| Okta (IdP) | | 中央認証レイヤー | | Entra ID (IdP) |
| |<---->| (Keycloakなど) |<---->| |
| SAML 2.0 | SAML | | OIDC | OIDC |
+--------------------+ | - テナント別IdP設定 | +--------------------+
| - ドメイン -> IdP |
顧客C (スタートアップ) | ルーティング |
+--------------------+ | - JIT / SCIM |
| IdPなし |----->| - セッション管理 |
| メール+パスキー | +----------+----------+
+--------------------+ |
v
+---------------------+
| SaaSアプリケーション |
| (テナント分離された |
| 認可) |
+---------------------+
核心となる設計原則は3つです。
1. **アプリケーションはIdPを直接知りません。** アプリは中央認証レイヤー(自社Keycloak、またはAuth0/WorkOSのようなサービス)とのみOIDCで通信し、顧客IdPとのSAML/OIDCフェデレーションは認証レイヤーが担当します。これをbroker(仲介者)パターンと呼びます。
2. **テナント識別は認証より先です。** 「誰なのか」を尋ねる前に「どの会社の所属か」を知らなければ、正しいIdPへ送れません。
3. **認証は委譲しても認可は委譲しません。** 顧客IdPは「この人がacme.comの従業員である」ことまでを保証し、当社サービス内でのロールと権限は当社が管理します。
テナント別IdP設定モデル — realm-per-tenant vs Organizations
中央認証レイヤーをKeycloakで構築する場合、テナントをどうモデリングするかが最初の分かれ道です。
モデル1: realm-per-tenant
テナントごとにKeycloak realmを1つずつ作る伝統的な方式です。
Keycloak
├── realm: acme (Okta SAMLフェデレーション、acme専用クライアント)
├── realm: globex (Entra OIDCフェデレーション、globex専用クライアント)
└── realm: startupz (ローカルアカウント + パスキー)
利点は完全な分離です。認証フロー、パスワードポリシー、トークン寿命、テーマまでテナントごとに自由に変えられます。しかし致命的な欠点があります。
- **スケーラビリティ**: realmは重いオブジェクトです。数百〜数千テナント規模でrealm数が増えると、起動時間、キャッシュメモリ、管理コンソールの性能がすべて悪化します。Keycloakチームも大量のrealm運用を推奨していません。
- **運用負担**: realmごとにクライアント登録、鍵ローテーション、設定変更を繰り返す必要があります。テナントが100あれば設定変更も100回です(自動化必須)。
- **アプリ統合の複雑さ**: アプリがテナントごとに異なるissuer URLを扱わなければなりません。多くのOIDCライブラリはマルチissuerをエレガントにサポートしていません。
モデル2: 単一realm + Keycloak Organizations
Keycloak 26で正式化された[Organizations](https://www.keycloak.org/docs/latest/server_admin/index.html#_managing_organizations)機能は、この問題を正面から解決します。1つのrealmの中に「organization」という第一級のテナント概念を導入したものです。
Keycloak
└── realm: saas-prod
├── organization: acme
│ ├── ドメイン: acme.com, acme.co.kr (検証済み)
│ ├── identity provider: acme-okta (SAML)
│ └── メンバー: acme所属のユーザー
├── organization: globex
│ ├── ドメイン: globex.io
│ ├── identity provider: globex-entra (OIDC)
│ └── メンバー: globex所属のユーザー
└── organization: startupz
├── ドメイン: startupz.dev
└── (IdPなし — ローカル認証)
Organizationsが提供するものです。
- **organizationごとのIdP連携**: 各organizationにSAML/OIDC IdPを接続し、そのorganizationのドメインのユーザーをそのIdPへルーティングします。
- **ドメインマッピング**: organizationにメールドメインを登録すると、ログイン画面でメールアドレスだけを見てどのorganizationか判別できます(home realm discoveryのビルトイン実装)。
- **メンバーシップ管理**: ユーザー招待(invitation)、organizationメンバー属性、organization単位のメンバー取得APIを提供します。
- **トークンへのorganizationクレーム**: 発行されるトークンにorganization情報がクレームとして含まれ、アプリはテナントコンテキストを即座に把握できます。
{
"iss": "https://auth.example-saas.com/realms/saas-prod",
"sub": "f3a8c2e1-9b47-4d6a-8c21-0e5f7a9b3d44",
"preferred_username": "jane@acme.com",
"organization": {
"acme": {
"id": "b1e6f0c2-3d8a-47e5-9f12-6c4b8a0d2e91"
}
},
"exp": 1781234567,
"aud": "saas-web"
}
比較と選択基準
| 基準 | realm-per-tenant | 単一realm + Organizations |
| --- | --- | --- |
| 分離レベル | 最高 (ポリシー/鍵/テーマすべて分離) | 論理的分離 (ポリシーはrealm共有) |
| テナント数のスケーラビリティ | 数十個で限界 | 数千organizationまで現実的 |
| アプリ統合 | テナント別issuer — 複雑 | 単一issuer — シンプル |
| テナント別認証ポリシーの差別化 | 自由 | 制限的 (認証フローはrealm単位) |
| 運用自動化の負担 | 大きい | 小さい |
| 適するケース | 規制で強い分離が必要な少数の大型顧客 | 一般的なB2B SaaSの大半 |
2026年時点の結論は明確です。**一般的なB2B SaaSなら単一realm + Organizationsがデフォルト**であり、金融・公共のように契約上の物理的分離が要求される例外顧客のみ別realm(または別インスタンス)に分離するハイブリッドが現実的です。Keycloak 26.6のzero-downtime rolling updateのおかげで、単一の大型realm運用におけるアップグレードリスクも以前より大幅に減りました。
Home Realm Discovery — メールドメインベースのIdPルーティング
ユーザーがログインページに到着した時点では、まだ誰なのか分かりません。どのIdPへ送るかを決定するプロセスが **home realm discovery(HRD)** です。代表的な実装が「メールファースト(identifier-first)」パターンです。
1. ユーザーがメールを入力: jane@acme.com
2. システムがドメインを抽出: acme.com
3. ドメイン -> organization -> IdP のマッピングを照会
- acme.comはorganization acme、IdPはacme-okta(SAML)
4. 分岐:
- IdPあり -> acme-oktaへSAML AuthnRequestでリダイレクト
- IdPなし -> パスワード/パスキー入力UIを表示
5. IdP認証完了 -> コールバック -> セッション発行
Keycloak Organizationsを使えば、このフローは標準提供されます。ログインページでメールを入力すると、ドメインがorganizationにマッピングされているかを確認し、連携済みIdPへ自動リダイレクトします。自前で実装するなら次のような形になります。
@PostMapping("/auth/discover")
public ResponseEntity<DiscoveryResponse> discover(@RequestBody DiscoveryRequest req) {
String domain = extractDomain(req.email()); // "acme.com"
Optional<TenantIdpConfig> idp = tenantService.findVerifiedIdpByDomain(domain);
if (idp.isPresent()) {
// フェデレーション済みテナント: 認可リクエストURLを生成 (OIDCならauthorize、SAMLならSSO URL)
String redirect = authUrlBuilder.build(idp.get(), req.relayState());
return ResponseEntity.ok(DiscoveryResponse.federated(redirect));
}
// 非連携テナント: ローカル認証(パスワード/パスキー)へ
return ResponseEntity.ok(DiscoveryResponse.local());
}
HRD設計時の注意点です。
- **テナントの存在を漏らさない**: 「このドメインは登録されていません」のような応答は顧客リストの推測を許します。IdPの有無にかかわらず同一の次ステップUIを見せるのが安全です。
- **個人ドメインの扱い**: gmail.comのような公共ドメインはorganizationマッピングの対象から除外すべきです。
- **複数IdPドメイン**: M&Aなどで1つのドメインに複数のIdPが紐づくことがあります。この場合、IdP選択画面を見せるフォールバックが必要です。
SP-initiatedフローにおけるテナント識別
HRD以外にも、テナントを識別する経路はいくつかあります。
| 方法 | 例 | 長所と短所 |
| --- | --- | --- |
| メールドメイン (identifier-first) | jane@acme.com を入力 | 汎用的、UXが1ステップ増える |
| テナント専用URL | acme.example-saas.com | ブックマークしやすい、サブドメイン管理が必要 |
| テナント専用ログインパス | /login/acme | 実装が単純、URL共有時にテナントが露出 |
| IdP-initiated SSO | 顧客ポータルでアプリタイルをクリック | 顧客に優しいがセキュリティ上の推奨度は低い (特にSAML unsolicited response) |
実務では「サブドメイン + identifier-firstフォールバック」の組み合わせが最も一般的です。IdP-initiated SAMLはCSRF的な攻撃面があるため、可能ならSP-initiatedを強制し、顧客ポータルのタイルはSPのログインURLを指すよう案内するのが良いです。
ドメイン検証 — フェデレーションの信頼基盤
ドメインとorganizationのマッピングはセキュリティの根幹です。検証なしに任意のドメインを登録できるなら、悪意あるテナントがacme.comを自分のorganizationに登録し、acme従業員のログインを自分のIdPへ乗っ取れてしまいます。
標準的な検証方式はDNS TXTレコードです。
1. テナント管理者がドメイン登録を申請: acme.com
2. システムが検証トークンを発行:
saas-verify=4f8a2c1e-7b3d-4e9a-b6f0-1c5d8e2a9b47
3. 管理者がacme.comのDNSにTXTレコードを追加
4. システムがDNS照会でトークンを確認 -> 検証完了
5. その後は定期的な再検証 (ドメイン所有権喪失の検知)
検証側の実装の核心は単純です
dig +short TXT acme.com | grep "saas-verify=4f8a2c1e-7b3d-4e9a-b6f0-1c5d8e2a9b47"
追加の原則です。
- **検証前のルーティング有効化は禁止**: 検証完了まで、当該ドメインのHRDルーティングを有効にしてはいけません。
- **ドメイン衝突の処理**: すでに別のorganizationが検証済みのドメインは登録を拒否し、紛争手続き(手動審査)を設けます。
- **再検証サイクル**: ドメインの失効・売却を検知するため定期的に再確認します。
JIT Provisioning vs SCIM
顧客IdPでの認証はできた — では当社サービス内のユーザーレコードは誰が作るのでしょうか?
JIT (Just-in-Time) Provisioning
初回ログイン時にSAMLアサーション/OIDCクレームを見てユーザーをその場で作成する方式です。
public User jitProvision(OidcUserInfo info, Organization org) {
return userRepository.findByIssuerAndSubject(info.issuer(), info.subject())
.orElseGet(() -> {
User user = User.builder()
.email(info.email())
.displayName(info.name())
.organizationId(org.id())
.role(org.defaultRole()) // 最小権限がデフォルト
.source(UserSource.FEDERATED_JIT)
.build();
auditLog.record("user.jit_provisioned", user);
return userRepository.save(user);
});
}
比較
| 基準 | JIT | SCIM |
| --- | --- | --- |
| アカウント作成タイミング | 初回ログイン時 | 入社/配属の即時 (事前作成) |
| アカウント無効化 | 不可能 — ログインしないユーザーは不明 | IdPが即時プッシュ |
| グループ/権限の同期 | ログイン時点のクレームのスナップショット | 変更時にリアルタイム反映 |
| 実装コスト | 低い | SCIMサーバーの実装が必要 |
| ログイン前のコラボ機能 (メンバー招待、メンション) | ユーザーが存在せず不完全 | 全名簿を事前反映 |
結論は「両方」です。**JITはベースライン、SCIMはエンタープライズティアのアップグレード**と位置づけるのが業界標準のパターンです。重要なのはdeprovisioningの観点です。JITしかないテナントでは退職者のアカウントが当社側に残り続けるという事実を顧客のセキュリティチームへ明確に伝え、セッション寿命の短縮と再認証サイクルで補完すべきです。
セルフサービスSSOオンボーディングUIの設計
エンタープライズ顧客が数十社になると、SSO設定をサポートチケットで処理する方式は破綻します。テナント管理者が自分で設定するセルフサービスウィザードが答えです。
[ステップ1] プロトコル選択
SAML 2.0 | OIDC
|
[ステップ2] 当社側の情報を提供 (コピー可能な形で)
SAML: SP Entity ID, ACS URL, SPメタデータXMLダウンロード
OIDC: Redirect URI, 推奨スコープ
|
[ステップ3] 顧客IdP情報の入力
SAML: IdPメタデータURLまたはXMLアップロード (署名証明書含む)
OIDC: issuer URL (discovery自動取得), client_id, client_secret
|
[ステップ4] 属性マッピング
email、氏名、グループクレームのマッピング (ドロップダウン + デフォルト値)
|
[ステップ5] テストログイン
設定有効化の前に、管理者本人がテスト認証を実施
失敗時はSAMLレスポンス/エラーを人間が読める形で表示
|
[ステップ6] 有効化ポリシーの選択
- SSO任意 (ローカルログイン併用)
- SSO強制 (ローカルログイン遮断、ただしbreak-glass管理者は例外)
設計のディテールが成否を分けます。
- **メタデータの自動パース**: SAMLメタデータXMLやOIDC discoveryドキュメントをアップロード/入力すると、エンドポイントと証明書を自動抽出します。手動入力項目が増えるほど設定ミスのチケットが増えます。
- **テストログインの必須化**: テスト合格前の有効化をブロックするだけで、「SSOを有効にしたら全社ログイン不能」という事故を防げます。
- **break-glassアカウント**: SSO強制モードでも、テナント管理者1名はローカル認証(強力なMFA必須)で入れる必要があります。IdP障害や証明書失効時のロック解除経路がないと、サポート地獄が始まります。
- **証明書失効の監視**: SAML署名証明書の失効30日前/7日前にテナント管理者へ自動通知を送ります。SAML連携障害の原因第1位が証明書の失効です。
Keycloakベースなら、このウィザードのバックエンドはAdmin API呼び出しで実装します。
organizationにOIDC IdPを接続する例 (Admin API)
curl -X POST "https://auth.example-saas.com/admin/realms/saas-prod/identity-provider/instances" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"alias": "globex-entra",
"providerId": "oidc",
"config": {
"issuer": "https://login.microsoftonline.com/TENANT-GUID/v2.0",
"clientId": "globex-client-id",
"clientSecret": "globex-client-secret",
"useJwksUrl": "true",
"validateSignature": "true"
}
}'
IdPをorganizationにリンク
curl -X POST "https://auth.example-saas.com/admin/realms/saas-prod/organizations/ORG-ID/identity-providers" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '"globex-entra"'
料金プランとSSO Tax論争
SSOをどの料金プランに入れるかは、技術問題であると同時にビジネス上の論争です。SSOを最上位エンタープライズティアにのみ入れて大きな価格ジャンプを作る慣行を批判する用語が **SSO tax** です。[sso.tax](https://sso.tax)サイトがこの慣行を公開で追跡し、業界全体の論争になりました。
- **批判側**: SSOはセキュリティ機能であり、セキュリティをプレミアムに閉じ込めることはエコシステム全体のセキュリティ水準を下げる。特にSSO導入の意思がある中小顧客が価格を理由に諦めるのは社会的損失だ。
- **擁護側**: エンタープライズSSOは連携・サポートコストが実際に大きく、支払い意欲の高い顧客を見分ける自然な価格差別ポイントである。
2026年の現実的な折衷案はこう整理されます。
| 機能 | 推奨配置 |
| --- | --- |
| ソーシャルログイン、パスキー | 全ティア |
| SAML/OIDC SSO (単一IdP) | 中位ティアから、または低価格アドオン |
| SCIMプロビジョニング | エンタープライズティア |
| 監査ログAPI、セッションポリシーのカスタマイズ | エンタープライズティア |
セキュリティコミュニティの圧力により、「SSO自体は低いティアへ、ガバナンス機能(SCIM、監査、ポリシー)は上位ティアへ」というパターンが徐々に標準になりつつあります。アーキテクチャ観点の示唆は明確です。**SSOを後から任意のティアに付けたり外したりできるよう、認証レイヤーを最初からテナント別フィーチャーフラグで設計しておくべき**だということです。
セッションと権限のテナント分離
セッション分離
1人のユーザーが2つのテナントに所属することがあります(例: コンサルタントが顧客2社のワークスペース両方のメンバー)。このとき、セッションとトークンのテナントコンテキストを明確にしなければなりません。
- **トークンにテナントクレームを刻む**: 先に見たorganizationクレームのように、すべてのアクセストークンには「このトークンがどのテナントコンテキストか」が含まれるべきです。
- **テナント切り替えは再発行で**: ワークスペース切り替え時に既存トークンを再利用せず、新しいテナントコンテキストのトークンを再発行します。
- **テナント別セッションポリシー**: エンタープライズテナントは「アイドル30分、絶対8時間、IdPセッションと寿命連動」のような独自ポリシーを要求します。セッションポリシーをテナント設定として外部化しておくべきです。
認可の分離
すべてのAPIリクエストは「トークンのテナント == リソースのテナント」を強制する単一のミドルウェアを通過しなければなりません。
@Component
public class TenantIsolationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String tokenTenant = jwtContext.organizationId(); // トークンクレームから
String resourceTenant = tenantResolver.fromRequest(req); // URL/リソースから
if (!tokenTenant.equals(resourceTenant)) {
auditLog.record("authz.cross_tenant_denied", tokenTenant, resourceTenant);
res.sendError(HttpServletResponse.SC_NOT_FOUND); // 403でなく404で存在を隠蔽
return;
}
chain.doFilter(req, res);
}
}
cross-tenantアクセスの試行はセキュリティイベントとして記録し、応答は404でリソースの存在自体を隠すのが慣例です。より細かい権限(ドキュメント単位の共有など)が必要になったら、ReBACモデル([OpenFGA](https://openfga.dev/docs)、[Google Zanzibar](https://research.google/pubs/pub48190/)系)への拡張を検討します。その場合もテナント境界は関係グラフの最上位次元として維持すべきです。
監査とコンプライアンス
エンタープライズ顧客のセキュリティチームが要求するものです。
- **認証イベントの監査ログ**: ログイン成功/失敗、SSO設定変更、ドメイン検証、管理者権限変更をすべて記録し、テナント管理者が自テナント分を照会/エクスポートできる必要があります(SIEM連携用APIを含む)。
- **SOC 2 / ISO 27001のエビデンス**: SSO設定の変更履歴とアクセス制御の記録は当社側の監査資料でもあります。変更の主体がテナント管理者なのか当社オペレーターなのかを区別できなければなりません。
- **データレジデンシー(residency)**: 認証ログにもリージョン要件が付くことがあります。認証レイヤーのリージョン分離の可能性をアーキテクチャ段階で検討しておきます。
- **セッションの証明**: 「退職者Dの最後のアクセスはいつだったか」に答えられる必要があります。セッションの作成/失効/廃棄イベントを保持ポリシーとともに保存します。
実戦シナリオ集
実運用で出会うシナリオで設計を検証してみましょう。
**シナリオ1 — 顧客がOktaからEntraへ移行**
acmeがIdPを交換します。ユーザーのマッチングキーがIdPのsubjectだった場合、全員が新規ユーザーとして重複作成されます。教訓: フェデレーションユーザーのマッチングキーは「issuer+subject」を第1とし、検証済みドメインのメールを第2のリンクキーとして持ち、IdP移行手順(管理者が旧IdPリンクを一括再マッピング)を製品機能として用意すべきです。
**シナリオ2 — M&Aによるドメイン統合**
globexがinitechを買収し、initech.comのユーザーがglobex organizationに合流します。ドメイン再検証、既存initech organizationとのメンバー統合、進行中セッションの処理(強制再認証)が必要です。organization間のメンバー移動APIと監査ログがなければ手作業地獄になります。
**シナリオ3 — IdP障害でログイン不能**
acmeのOktaが障害を起こすと、acmeのユーザー全員がログイン不能になります。これは意図された動作ですが(認証の所有権は顧客にあるため)、break-glass管理者アカウント、ステータスページでの案内、そして「IdP障害は当社の障害ではない」ことを示せる連携ヘルスモニタリングを準備しておく必要があります。
**シナリオ4 — SSO強制後のAPIトークン**
acmeがSSO強制を有効にしたのに、acme従業員が過去に発行した個人APIトークンは依然として動作します。SSO強制ポリシーに「既存トークンの無効化 + IdPセッションベースの再発行」オプションを含めるかを決めなければなりません。エンタープライズのセキュリティチームの多くは無効化を期待します。
おわりに
マルチテナントSSOは「SAMLライブラリをつなぐこと」ではなく、テナントモデリング、信頼の検証、ライフサイクル、課金まで絡む製品アーキテクチャの問題です。要点をまとめます。
1. B2B SaaSのSSOはテナント別の外部IdPフェデレーションです。アプリとIdPの間にbrokerレイヤーを置き、アプリには単一のissuerだけを見せましょう。
2. テナントモデルは単一realm + Keycloak Organizations(26+)がデフォルト。強い分離が必要な例外だけ分離しましょう。
3. メールドメインベースのHRDとDNSドメイン検証がルーティング信頼の土台です。検証なしのドメインマッピングはアカウント乗っ取りの経路になります。
4. JITは出発点、SCIMはエンタープライズの完成形です。deprovisioningのギャップを顧客に正直に説明しましょう。
5. セルフサービスオンボーディングウィザード(メタデータ自動パース、テストログイン、break-glass)がSSOサポートコストを決定します。
6. SSO tax論争を意識し、認証機能をテナント別フラグで設計して価格政策の変化に備えましょう。
次回の記事では、このアーキテクチャの最後の難題であるSingle Logout — ログインより難しいログアウトの設計を扱います。
参考資料
- [Keycloak Server Administration — Managing Organizations](https://www.keycloak.org/docs/latest/server_admin/index.html#_managing_organizations)
- [Keycloak Documentation](https://www.keycloak.org/documentation)
- [Keycloak Release Notes](https://www.keycloak.org/docs/latest/release_notes/index.html)
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
- [SAML 2.0 Core Specification (OASIS)](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf)
- [RFC 6749 — The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)
- [RFC 9700 — Best Current Practice for OAuth 2.0 Security](https://datatracker.ietf.org/doc/html/rfc9700)
- [RFC 7644 — SCIM: Protocol](https://datatracker.ietf.org/doc/html/rfc7644)
- [OAuth 2.1 Draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/)
- [The SSO Wall of Shame (sso.tax)](https://sso.tax)
- [OpenFGA Documentation](https://openfga.dev/docs)
- [Google Zanzibar 論文](https://research.google/pubs/pub48190/)
- [FIDO Alliance — Passkeys](https://fidoalliance.org/passkeys/)
현재 단락 (1/262)
B2B SaaSを運営していると、エンタープライズ営業の段階で必ず受ける質問があります。「SSOに対応していますか?」 このSSOは「Googleでログイン」のようなソーシャルログインではありません。...