第7章 OpenID ConnectとSAML
なぜこの章が重要か
「一度のログインですべてのシステムが使える」- この理想を実現するのがシングルサインオン(SSO)です。企業環境では、従業員が日々10以上のシステムを使うことも珍しくありません。OpenID ConnectとSAMLは、このSSO実現の中核技術です。しかし、両者の選択を誤ると、セキュリティリスクや運用コストの増大を招きます。この章では、フェデレーション認証の本質を理解し、適切な技術選択と実装方法を学びます。
7.1 フェデレーション認証の概念 - 組織間連携の必要性
7.1.1 なぜフェデレーション認証が必要なのか
class FederationAuthenticationConcept:
"""フェデレーション認証の概念と必要性"""
def explain_traditional_problems(self):
"""従来の認証の問題点"""
return {
'password_proliferation': {
'problem': 'パスワードの氾濫',
'scenario': '''
ある企業の従業員の1日:
1. 社内ポータル:独自のID/パスワード
2. メールシステム:別のID/パスワード
3. 経費精算システム:また別のID/パスワード
4. SaaSサービスA:個別のアカウント
5. SaaSサービスB:さらに別のアカウント
...
結果:平均15個のパスワードを管理
''',
'consequences': [
'パスワードの使い回し',
'付箋にパスワードを書く',
'頻繁なパスワードリセット',
'ヘルプデスクの負担増大'
]
},
'identity_silos': {
'problem': 'アイデンティティのサイロ化',
'illustration': '''
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ System A │ │ System B │ │ System C │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ User: john │ │ User: j.doe │ │ User: jdoe │
│ Dept: Sales │ │ Dept: 営業 │ │ Dept: SLS │
│ Role: Mgr │ │ Role: 課長 │ │ Role: M │
└─────────────┘ └─────────────┘ └─────────────┘
同じ人物の情報が異なる形式で重複管理
''',
'issues': [
'情報の不整合',
'更新の手間(退職時など)',
'コンプライアンスリスク',
'監査の困難さ'
]
},
'cross_organization_collaboration': {
'problem': '組織間連携の困難さ',
'real_world_example': '''
# 企業間プロジェクトのシナリオ
企業A:プロジェクトオーナー
企業B:開発パートナー
企業C:デザインパートナー
課題:
- 企業Aのシステムに企業B,Cの従業員がアクセス必要
- 各企業のセキュリティポリシーを満たす必要
- プロジェクト終了時にアクセス権を確実に削除
従来の解決策:
- ゲストアカウントの大量発行
- VPNの設定
- 手動でのアカウント管理
問題:管理コスト、セキュリティリスク、柔軟性の欠如
'''
}
}
7.1.2 フェデレーション認証が解決する課題
class FederationSolution:
"""フェデレーション認証のソリューション"""
def core_principles(self):
"""フェデレーション認証の中核原則"""
return {
'trust_relationship': {
'concept': '信頼関係の確立',
'explanation': '''
# 信頼の連鎖
User ─認証→ Identity Provider (IdP)
↓
信頼関係
↓
Service Provider (SP) ←アサーション─ IdP
SPはIdPを信頼し、IdPの認証結果を受け入れる
''',
'benefits': [
'ユーザーは一箇所(IdP)でのみ認証',
'SPは認証機能を持つ必要なし',
'組織の境界を越えた連携が可能'
]
},
'separation_of_concerns': {
'concept': '責任の分離',
'roles': {
'identity_provider': {
'responsibility': 'ユーザーの認証',
'functions': [
'クレデンシャルの管理',
'多要素認証の実施',
'アイデンティティ情報の管理'
]
},
'service_provider': {
'responsibility': 'サービスの提供',
'functions': [
'リソースの提供',
'認可の判断',
'ビジネスロジックの実行'
]
},
'user': {
'responsibility': '適切な利用',
'experience': 'シームレスなアクセス'
}
}
},
'standardization': {
'concept': '標準プロトコルの使用',
'importance': '''
なぜ標準化が重要か:
1. 相互運用性
- 異なるベンダーの製品間で連携可能
- 実装の選択肢が増える
2. セキュリティ
- 広くレビューされた仕様
- 既知の脆弱性への対策
3. 開発効率
- ライブラリの利用
- ベストプラクティスの共有
4. 将来性
- ベンダーロックインの回避
- 技術の進化への対応
'''
}
}
def implementation_benefits(self):
"""実装によって得られる利益"""
return {
'user_experience': {
'single_sign_on': {
'description': '一度の認証で複数サービス利用',
'flow': '''
朝9:00 - 社内ポータルにログイン
↓
9:30 - メールシステム → 自動的にアクセス可能
↓
10:00 - SaaSサービスA → 再認証不要
↓
14:00 - SaaSサービスB → まだセッション有効
''',
'satisfaction': '認証の手間が1/10以下に削減'
},
'consistent_experience': {
'description': '一貫した認証体験',
'benefits': [
'同じ認証画面',
'統一されたMFA',
'共通のパスワードポリシー'
]
}
},
'administrative_benefits': {
'centralized_management': {
'description': '集中管理',
'capabilities': '''
# 管理者の操作
def onboard_employee(employee):
# IdPで一度だけ作成
idp.create_user(employee)
# 自動的にすべてのSPで利用可能に
# 個別のアカウント作成不要
def offboard_employee(employee):
# IdPで無効化
idp.disable_user(employee)
# すべてのSPへのアクセスが即座に停止
'''
},
'improved_security': {
'controls': [
'統一されたパスワードポリシー',
'集中的な監査ログ',
'リアルタイムのアクセス制御',
'コンプライアンスの簡素化'
]
}
},
'business_benefits': {
'cost_reduction': {
'areas': [
'ヘルプデスク対応の削減(最大40%)',
'パスワードリセットコストの削減',
'システム統合コストの削減',
'監査コストの削減'
],
'roi': '通常6-12ヶ月で投資回収'
},
'agility': {
'capabilities': [
'新サービスの迅速な導入',
'M&A時の迅速な統合',
'パートナー連携の容易化',
'クラウドサービスの活用'
]
}
}
}
7.1.3 フェデレーション認証のアーキテクチャ
class FederationArchitecture:
"""フェデレーション認証のアーキテクチャ"""
def architectural_patterns(self):
"""主要なアーキテクチャパターン"""
return {
'hub_and_spoke': {
'description': 'ハブ&スポーク型',
'diagram': '''
┌─────────┐
│ IdP │
│ (Hub) │
└────┬────┘
│
┌───────────┼───────────┐
│ │ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│ SP1 │ │ SP2 │ │ SP3 │
│(Spoke)│ │(Spoke)│ │(Spoke)│
└───────┘ └───────┘ └───────┘
''',
'characteristics': [
'単一のIdPがすべてのSPと信頼関係',
'管理が簡単',
'IdPが単一障害点になるリスク'
],
'use_case': '企業内SSO'
},
'federated_network': {
'description': 'フェデレーションネットワーク型',
'diagram': '''
┌───────┐ ┌───────┐
│ IdP-A │◀────▶│ IdP-B │
└───┬───┘ └───┬───┘
│ │
┌───▼───┐ ┌───▼───┐
│ SP-A1 │ │ SP-B1 │
└───────┘ └───────┘
相互信頼関係
''',
'characteristics': [
'複数のIdP間で信頼関係',
'組織間連携に適している',
'複雑な信頼関係の管理'
],
'use_case': '大学間連携、企業間連携'
},
'proxy_model': {
'description': 'プロキシ型',
'diagram': '''
┌────────┐ ┌─────────┐ ┌────────┐
│External│───▶│ Proxy │───▶│Internal│
│ IdP │ │ IdP │ │ SP │
└────────┘ └─────────┘ └────────┘
''',
'characteristics': [
'内部と外部の橋渡し',
'プロトコル変換が可能',
'セキュリティ境界の明確化'
],
'use_case': 'B2B連携、クラウド統合'
}
}
def trust_establishment(self):
"""信頼関係の確立方法"""
return {
'metadata_exchange': {
'concept': 'メタデータの交換',
'saml_example': '''
<!-- IdPメタデータ -->
<EntityDescriptor entityID="https://idp.example.com">
<IDPSSODescriptor>
<KeyDescriptor use="signing">
<KeyInfo>
<X509Data>
<X509Certificate>
MIIDXTCCAkWgAwIBAgIJAKl...
</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
<SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://idp.example.com/sso"/>
</IDPSSODescriptor>
</EntityDescriptor>
''',
'oidc_example': '''
// OpenID Connect Discovery
GET /.well-known/openid-configuration
{
"issuer": "https://idp.example.com",
"authorization_endpoint": "https://idp.example.com/auth",
"token_endpoint": "https://idp.example.com/token",
"jwks_uri": "https://idp.example.com/jwks",
"response_types_supported": ["code", "id_token"]
}
'''
},
'key_management': {
'signing_keys': {
'purpose': 'アサーション/トークンの署名',
'rotation': '定期的な鍵のローテーション',
'distribution': 'JWKSエンドポイントまたはメタデータ'
},
'encryption_keys': {
'purpose': '機密情報の暗号化',
'algorithms': ['RSA-OAEP', 'AES-GCM'],
'key_agreement': '事前の鍵交換'
}
}
}
7.2 OpenID Connectの仕組み - OAuthとの違いと追加価値
7.2.1 なぜOpenID Connectが生まれたのか
class OpenIDConnectRationale:
"""OpenID Connect誕生の背景"""
def oauth2_limitations(self):
"""OAuth 2.0の限界"""
return {
'authentication_vs_authorization': {
'oauth2_purpose': '認可のためのプロトコル',
'common_misuse': '''
# OAuth 2.0の誤用例
# ❌ 間違い:OAuth 2.0を認証に使う
def authenticate_with_oauth(access_token):
# アクセストークンがあるから認証済み?
# 誰のトークンかわからない!
user_info = api.get_user_info(access_token)
# このAPIコールが追加で必要
return user_info
問題点:
1. トークンの所有者が不明
2. 認証時刻が不明
3. 認証方法が不明
4. APIコールのオーバーヘッド
''',
'security_issues': [
'トークン置換攻撃',
'認証コンテキストの欠如',
'標準化されていないユーザー情報取得'
]
},
'lack_of_standardization': {
'problem': '各社独自の実装',
'examples': '''
# Facebook
GET /me?access_token=TOKEN
Response: {id, name, email}
# Google
GET /oauth2/v1/userinfo?access_token=TOKEN
Response: {id, email, verified_email, name}
# GitHub
GET /user
Authorization: token TOKEN
Response: {login, id, email, name}
すべて異なるエンドポイント、形式、フィールド名
''',
'developer_impact': '各プロバイダーごとに個別実装が必要'
}
}
def openid_connect_solution(self):
"""OpenID Connectの解決策"""
return {
'identity_layer': {
'concept': 'OAuth 2.0の上に認証レイヤーを追加',
'architecture': '''
┌─────────────────────────────────┐
│ OpenID Connect (OIDC) │ ← 認証
├─────────────────────────────────┤
│ OAuth 2.0 │ ← 認可
├─────────────────────────────────┤
│ HTTP/TLS │ ← 転送
└─────────────────────────────────┘
''',
'additions': [
'IDトークン(認証の証明)',
'標準化されたスコープとクレーム',
'UserInfoエンドポイント',
'ディスカバリメカニズム'
]
},
'id_token': {
'purpose': '認証イベントの証明',
'structure': '''
{
// 必須クレーム
"iss": "https://idp.example.com", // 発行者
"sub": "248289761001", // サブジェクト(ユーザー)
"aud": "s6BhdRkqt3", // オーディエンス(クライアント)
"exp": 1311281970, // 有効期限
"iat": 1311280970, // 発行時刻
// 認証関連クレーム
"auth_time": 1311280969, // 認証時刻
"nonce": "n-0S6_WzA2Mj", // リプレイ攻撃防止
"acr": "urn:mace:incommon:iap:silver", // 認証コンテキスト
"amr": ["pwd", "otp"], // 認証方法
// ユーザー情報
"name": "Jane Doe",
"email": "janedoe@example.com",
"email_verified": true
}
''',
'benefits': [
'認証の証明がトークン内に含まれる',
'追加のAPI呼び出し不要',
'改ざん防止(署名付き)',
'標準化された形式'
]
}
}
7.2.2 OpenID Connectのフロー
class OpenIDConnectFlows:
"""OpenID Connectの各種フロー"""
def authorization_code_flow(self):
"""認可コードフロー(最も安全)"""
return {
'flow_diagram': '''
End-User RP (Client) OP (IdP)
│ │ │
│ 1. Access │ │
├─────────────▶│ │
│ │ │
│ │ 2. AuthN Request │
│ ├───────────────────▶│
│ │ (response_type=code)│
│ │ │
│ 3. Authenticate │
│◀──────────────────────────────────┤
│ │
│ 4. Authorize │
├──────────────────────────────────▶│
│ │
│ │ 5. AuthN Response │
│ │◀───────────────────┤
│ │ (code) │
│ │ │
│ │ 6. Token Request │
│ ├───────────────────▶│
│ │ (code + PKCE) │
│ │ │
│ │ 7. Token Response │
│ │◀───────────────────┤
│ │ (ID Token + │
│ │ Access Token) │
│ │ │
│ 8. Service │ │
│◀─────────────┤ │
''',
'implementation': '''
from authlib.integrations.flask_client import OAuth
import secrets
class OpenIDConnectClient:
def __init__(self, app):
self.oauth = OAuth(app)
self.client = self.oauth.register(
name='oidc',
client_id='your-client-id',
client_secret='your-client-secret',
server_metadata_url='https://op.example.com/.well-known/openid-configuration',
client_kwargs={
'scope': 'openid profile email'
}
)
def login(self):
"""ログイン開始"""
# nonce生成(リプレイ攻撃対策)
nonce = secrets.token_urlsafe(32)
session['nonce'] = nonce
redirect_uri = url_for('callback', _external=True)
return self.client.authorize_redirect(
redirect_uri,
nonce=nonce
)
def callback(self):
"""コールバック処理"""
# トークン取得
token = self.client.authorize_access_token()
# IDトークンの検証
nonce = session.pop('nonce', None)
id_token = token.get('id_token')
claims = self._verify_id_token(id_token, nonce)
# ユーザー情報取得(必要な場合)
if 'userinfo_endpoint' in self.client.server_metadata:
user_info = self.client.get('userinfo').json()
claims.update(user_info)
return claims
def _verify_id_token(self, id_token, nonce):
"""IDトークンの検証"""
# JWTの検証
claims = jwt.decode(
id_token,
self.client.jwks,
claims_options={
"iss": {"essential": True, "value": self.client.server_metadata['issuer']},
"aud": {"essential": True, "value": self.client.client_id},
"exp": {"essential": True},
"iat": {"essential": True}
}
)
# nonce検証
if claims.get('nonce') != nonce:
raise SecurityError("Nonce mismatch")
# 追加の検証
self._validate_timestamps(claims)
return claims
def _validate_timestamps(self, claims):
"""タイムスタンプの検証"""
current_time = int(time.time())
# 有効期限
if current_time >= claims['exp']:
raise TokenExpiredError("ID token expired")
# 発行時刻(未来でないこと)
if claims['iat'] > current_time + 60: # 1分の猶予
raise SecurityError("Token issued in the future")
# 認証時刻(あまりに古くないこと)
if 'auth_time' in claims:
max_age = 3600 # 1時間
if current_time - claims['auth_time'] > max_age:
raise ReauthenticationRequired("Authentication too old")
'''
}
def implicit_flow_deprecated(self):
"""暗黙的フロー(非推奨)"""
return {
'deprecation_notice': '''
⚠️ 重要:Implicit Flowは非推奨
理由:
1. トークンがURLフラグメントに露出
2. ブラウザ履歴に残る可能性
3. より安全な代替手段の存在
代替:
- SPAの場合:Authorization Code Flow + PKCE
- ネイティブアプリ:Authorization Code Flow + PKCE
''',
'migration_guide': '''
# Implicit Flowからの移行
# Before (Implicit)
response_type=id_token token
# After (Authorization Code + PKCE)
response_type=code
code_challenge=XXXXX
code_challenge_method=S256
'''
}
def hybrid_flow(self):
"""ハイブリッドフロー"""
return {
'use_case': '即座にIDトークンが必要な場合',
'response_types': [
'code id_token',
'code token',
'code id_token token'
],
'benefits': [
'認証の即座の確認',
'フロントエンドでの早期処理',
'バックエンドでの追加検証'
],
'security_considerations': [
'フロントチャネルでのトークン露出',
'適切なnonce使用が必須'
]
}
7.2.3 OpenID Connectの重要な概念
class OpenIDConnectConcepts:
"""OpenID Connectの重要な概念"""
def claims_and_scopes(self):
"""クレームとスコープ"""
return {
'standard_scopes': {
'openid': {
'required': True,
'purpose': 'OpenID Connectの使用を示す',
'claims': ['sub']
},
'profile': {
'claims': [
'name', 'family_name', 'given_name',
'middle_name', 'nickname', 'preferred_username',
'profile', 'picture', 'website',
'gender', 'birthdate', 'zoneinfo',
'locale', 'updated_at'
]
},
'email': {
'claims': ['email', 'email_verified']
},
'address': {
'claims': ['address']
},
'phone': {
'claims': ['phone_number', 'phone_number_verified']
}
},
'custom_claims': {
'example': '''
# カスタムクレームの定義
{
"sub": "248289761001",
"name": "Jane Doe",
"email": "janedoe@example.com",
// カスタムクレーム
"department": "Engineering",
"employee_id": "EMP001234",
"cost_center": "CC-789",
"manager": "john.smith@example.com",
"security_clearance": "confidential"
}
''',
'best_practices': [
'名前空間の使用(例:https://example.com/claims/)',
'必要最小限の情報のみ含める',
'PII(個人識別情報)の扱いに注意'
]
},
'claims_request': {
'purpose': '必要なクレームを明示的に要求',
'example': '''
{
"id_token": {
"email": {"essential": true},
"email_verified": {"essential": true},
"department": null
},
"userinfo": {
"name": null,
"picture": null
}
}
'''
}
}
def discovery_and_dynamic_registration(self):
"""ディスカバリと動的登録"""
return {
'discovery': {
'endpoint': '/.well-known/openid-configuration',
'purpose': 'OPの設定情報を自動取得',
'example_response': '''
{
"issuer": "https://op.example.com",
"authorization_endpoint": "https://op.example.com/authorize",
"token_endpoint": "https://op.example.com/token",
"userinfo_endpoint": "https://op.example.com/userinfo",
"jwks_uri": "https://op.example.com/jwks",
"registration_endpoint": "https://op.example.com/register",
"scopes_supported": ["openid", "profile", "email"],
"response_types_supported": ["code", "code id_token"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"subject_types_supported": ["public", "pairwise"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"],
"claims_supported": ["sub", "iss", "name", "email"],
"request_parameter_supported": true,
"request_uri_parameter_supported": false
}
''',
'benefits': [
'手動設定不要',
'設定変更への自動対応',
'エラーの削減'
]
},
'dynamic_registration': {
'purpose': '実行時のクライアント登録',
'flow': '''
POST /register
Content-Type: application/json
{
"application_type": "web",
"redirect_uris": ["https://client.example.com/callback"],
"client_name": "Example Client",
"logo_uri": "https://client.example.com/logo.png",
"token_endpoint_auth_method": "client_secret_basic",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}
Response:
{
"client_id": "s6BhdRkqt3",
"client_secret": "ZJYCqe3GGRvdrudKyZS0XhGv_Z45DuKhCUk0gBR1vZk",
"registration_access_token": "this.is.an.access.token.value.ffx83",
"registration_client_uri": "https://op.example.com/register/s6BhdRkqt3",
"client_secret_expires_at": 1577858400
}
''',
'use_cases': [
'マルチテナントSaaS',
'開発者ポータル',
'自動プロビジョニング'
]
}
}
def advanced_features(self):
"""高度な機能"""
return {
'request_object': {
'purpose': 'リクエストパラメータのJWT化',
'benefits': [
'パラメータの改ざん防止',
'機密情報の暗号化',
'リクエストの否認防止'
],
'example': '''
// リクエストオブジェクトの作成
const requestObject = jwt.sign({
iss: "s6BhdRkqt3",
aud: "https://op.example.com",
response_type: "code",
client_id: "s6BhdRkqt3",
redirect_uri: "https://client.example.com/cb",
scope: "openid profile",
state: "af0ifjsldkj",
nonce: "n-0S6_WzA2Mj",
max_age: 86400,
claims: {
id_token: {
auth_time: { essential: true }
}
}
}, clientPrivateKey, { algorithm: 'RS256' });
// 認可リクエスト
https://op.example.com/authorize?request=${requestObject}
'''
},
'session_management': {
'purpose': 'SSOセッションの管理',
'mechanisms': [
'check_session_iframe',
'end_session_endpoint',
'front/back-channel logout'
],
'implementation': '''
// セッション監視
window.addEventListener("message", (e) => {
if (e.origin !== "https://op.example.com") return;
if (e.data === "changed") {
// セッション状態が変更された
// サイレント再認証または再ログイン
checkSessionStatus();
}
});
'''
}
}
7.3 SAMLとの比較 - それぞれの適用領域
7.3.1 SAMLの概要と特徴
class SAMLOverview:
"""SAML 2.0の概要"""
def saml_fundamentals(self):
"""SAMLの基本"""
return {
'what_is_saml': {
'full_name': 'Security Assertion Markup Language',
'version': '2.0 (2005年から標準)',
'format': 'XML-based',
'purpose': 'セキュリティアサーションの交換'
},
'core_components': {
'assertions': {
'description': 'セキュリティ情報の声明',
'types': [
'Authentication Assertion(認証アサーション)',
'Attribute Assertion(属性アサーション)',
'Authorization Decision Assertion(認可決定アサーション)'
],
'example': '''
<saml:Assertion ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6"
Version="2.0"
IssueInstant="2024-01-04T05:00:00Z">
<saml:Issuer>https://idp.example.com</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">
jdoe@example.com
</saml:NameID>
</saml:Subject>
<saml:AuthnStatement AuthnInstant="2024-01-04T05:00:00Z">
<saml:AuthnContext>
<saml:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="email">
<saml:AttributeValue>jdoe@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="department">
<saml:AttributeValue>Engineering</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
'''
},
'protocols': {
'description': 'リクエスト/レスポンスのルール',
'main_protocols': [
'Authentication Request Protocol',
'Single Logout Protocol',
'Artifact Resolution Protocol',
'Name Identifier Management Protocol'
]
},
'bindings': {
'description': 'プロトコルメッセージの転送方法',
'types': [
'HTTP Redirect Binding',
'HTTP POST Binding',
'HTTP Artifact Binding',
'SOAP Binding'
]
}
}
}
def saml_flow(self):
"""SAML SSOフロー"""
return {
'sp_initiated_sso': {
'description': 'SP起動のSSO',
'flow': '''
User SP IdP
│ │ │
│ 1.Access │ │
├──────────▶│ │
│ │ │
│ │ 2.AuthnRequest│
│ ├──────────────▶│
│ │ │
│ 3.Login Form │
│◀──────────────────────────┤
│ │
│ 4.Credentials │
├──────────────────────────▶│
│ │
│ │ 5.SAMLResponse│
│ │◀──────────────┤
│ │ (Assertion) │
│ │ │
│ 6.Service │ │
│◀──────────┤ │
''',
'implementation': '''
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
from saml2.client import Saml2Client
from saml2.config import Config as Saml2Config
class SAMLServiceProvider:
def __init__(self):
self.saml_client = Saml2Client(config=self._get_saml_config())
def _get_saml_config(self):
return {
'entityid': 'https://sp.example.com',
'metadata': {
'local': ['idp_metadata.xml']
},
'service': {
'sp': {
'endpoints': {
'assertion_consumer_service': [
('https://sp.example.com/saml/acs', BINDING_HTTP_POST)
]
},
'authn_requests_signed': True,
'want_assertions_signed': True,
}
},
'key_file': 'sp_key.pem',
'cert_file': 'sp_cert.pem'
}
def initiate_sso(self):
"""SSO開始"""
session_id, auth_request = self.saml_client.prepare_for_authenticate()
# セッションIDを保存(後で使用)
session['saml_session_id'] = session_id
# IdPへリダイレクト
redirect_url = auth_request
return redirect(redirect_url)
def handle_sso_response(self, saml_response):
"""SAML Responseの処理"""
# レスポンスの検証
authn_response = self.saml_client.parse_authn_request_response(
saml_response,
BINDING_HTTP_POST
)
# アサーションの検証
if not self._validate_response(authn_response):
raise SecurityError("Invalid SAML response")
# ユーザー情報の抽出
user_info = {
'nameid': authn_response.name_id.text,
'attributes': authn_response.ava, # Attribute Value Assertions
'session_index': authn_response.session_index
}
return user_info
def _validate_response(self, response):
"""レスポンスの検証"""
# 署名検証
if not response.is_signed():
return False
# 有効期限検証
if response.not_on_or_after and response.not_on_or_after < time.time():
return False
# Audienceの検証
if self.saml_client.config.entityid not in response.assertion.audience:
return False
return True
'''
}
}
7.3.2 OpenID ConnectとSAMLの詳細比較
class OIDCvsSAMLComparison:
"""OpenID ConnectとSAMLの比較"""
def technical_comparison(self):
"""技術的な比較"""
return {
'data_format': {
'oidc': {
'format': 'JSON/JWT',
'example': '''
{
"iss": "https://idp.example.com",
"sub": "248289761001",
"aud": "s6BhdRkqt3",
"exp": 1311281970,
"name": "Jane Doe",
"email": "jane@example.com"
}
''',
'size': '~1KB',
'parsing': 'JSONパーサー(すべての言語で利用可能)'
},
'saml': {
'format': 'XML',
'example': '(前述のXMLアサーション)',
'size': '~10KB',
'parsing': 'XMLパーサー、署名検証ライブラリが必要'
}
},
'transport': {
'oidc': {
'primary': 'HTTPS REST API',
'bindings': ['フロントチャネル', 'バックチャネル'],
'simplicity': 'シンプル(HTTPのみ)'
},
'saml': {
'primary': 'HTTP POST/Redirect, SOAP',
'bindings': ['複数のバインディング'],
'complexity': '複雑(用途に応じて選択)'
}
},
'security_model': {
'oidc': {
'token_validation': 'JWT署名検証',
'encryption': 'JWE(オプション)',
'key_distribution': 'JWKS endpoint'
},
'saml': {
'assertion_validation': 'XML署名',
'encryption': 'XML暗号化',
'key_distribution': 'メタデータ交換'
}
},
'implementation_complexity': {
'oidc': {
'learning_curve': '低〜中',
'library_support': '豊富',
'debugging': '容易(JSON形式)',
'common_issues': [
'トークン有効期限',
'CORS設定',
'Nonceの扱い'
]
},
'saml': {
'learning_curve': '高',
'library_support': '成熟しているが限定的',
'debugging': '困難(XML署名)',
'common_issues': [
'XML署名の検証',
'時刻同期',
'メタデータ管理'
]
}
}
}
def use_case_comparison(self):
"""ユースケース別の比較"""
return {
'enterprise_sso': {
'scenario': '大企業の内部SSO',
'recommendation': 'SAML',
'reasons': [
'既存のSAML対応製品が多い',
'IT部門がXML/SAMLに精通',
'詳細な認証コンテキスト',
'複雑な属性マッピング'
],
'example_products': ['AD FS', 'Okta', 'Ping Identity']
},
'mobile_apps': {
'scenario': 'モバイルアプリの認証',
'recommendation': 'OpenID Connect',
'reasons': [
'RESTful APIとの親和性',
'コンパクトなトークン',
'ネイティブアプリサポート',
'PKCE対応'
]
},
'b2c_services': {
'scenario': 'コンシューマー向けサービス',
'recommendation': 'OpenID Connect',
'reasons': [
'ソーシャルログイン統合',
'開発者フレンドリー',
'JavaScriptでの実装容易性',
'モダンなWeb標準'
]
},
'legacy_integration': {
'scenario': 'レガシーシステム統合',
'recommendation': 'SAML',
'reasons': [
'2005年から標準',
'多くのレガシー製品が対応',
'SOAP Webサービスとの統合'
]
},
'microservices': {
'scenario': 'マイクロサービスアーキテクチャ',
'recommendation': 'OpenID Connect',
'reasons': [
'JWT(ステートレス)',
'API Gateway統合',
'サービスメッシュ対応',
'トークンイントロスペクション'
]
}
}
def migration_considerations(self):
"""移行に関する考慮事項"""
return {
'saml_to_oidc': {
'drivers': [
'モバイル/SPA対応の必要性',
'RESTful API採用',
'開発効率の向上',
'クラウドネイティブ化'
],
'challenges': [
'属性マッピングの違い',
'セッション管理の違い',
'既存統合の書き換え'
],
'migration_pattern': '''
# 段階的移行パターン
Phase 1: デュアルプロトコルサポート
├── 新規アプリ → OpenID Connect
└── 既存アプリ → SAML維持
Phase 2: プロキシ経由の移行
├── SAML → Proxy → OIDC変換
└── 透過的な移行
Phase 3: 完全移行
└── すべてOIDCに統一
'''
},
'coexistence_strategy': {
'approach': 'ハイブリッドIdP',
'implementation': '''
class HybridIdentityProvider:
"""SAMLとOIDCの両方をサポート"""
def authenticate(self, request):
# 共通の認証処理
user = self._perform_authentication(request)
# プロトコルに応じた応答
if request.is_saml():
return self._create_saml_response(user)
elif request.is_oidc():
return self._create_oidc_response(user)
def _create_saml_response(self, user):
# SAML Assertion生成
pass
def _create_oidc_response(self, user):
# ID Token生成
pass
'''
}
}
7.4 エンタープライズでの活用 - 実際の導入事例と課題
7.4.1 エンタープライズSSO実装パターン
class EnterpriseSSOPatterns:
"""エンタープライズSSO実装パターン"""
def implementation_patterns(self):
"""実装パターン"""
return {
'centralized_idp': {
'pattern': '中央集権型IdP',
'architecture': '''
┌─────────────────────────────────────┐
│ Enterprise IdP (Okta等) │
│ ┌─────────┬─────────┬─────────┐ │
│ │ LDAP │ MFA │ SIEM │ │
│ └─────────┴─────────┴─────────┘ │
└───────────┬─────────────────────────┘
│
┌───────────┼───────────┬─────────────┐
│ │ │ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐ ┌─────▼─────┐
│Office │ │ CRM │ │ ERP │ │External │
│ 365 │ │System │ │System │ │SaaS Apps │
└───────┘ └───────┘ └───────┘ └───────────┘
''',
'benefits': [
'単一の管理ポイント',
'統一されたセキュリティポリシー',
'包括的な監査ログ'
],
'challenges': [
'単一障害点',
'ベンダーロックイン',
'カスタマイズの制限'
]
},
'federated_multi_idp': {
'pattern': 'フェデレーション型マルチIdP',
'use_case': 'M&A、グローバル企業',
'implementation': '''
class FederatedSSOManager:
def __init__(self):
self.idp_registry = {
'corp_hq': {
'type': 'saml',
'metadata_url': 'https://hq-idp.corp.com/metadata'
},
'subsidiary_a': {
'type': 'oidc',
'discovery_url': 'https://idp.subsidiary-a.com/.well-known'
},
'partner_b': {
'type': 'saml',
'metadata_url': 'https://partner-b.com/saml/metadata'
}
}
def route_authentication(self, email):
"""メールドメインに基づいてIdPを選択"""
domain = email.split('@')[1]
idp_mapping = {
'corp.com': 'corp_hq',
'subsidiary-a.com': 'subsidiary_a',
'partner-b.com': 'partner_b'
}
idp_id = idp_mapping.get(domain)
if not idp_id:
raise UnknownDomainError(f"No IdP for domain: {domain}")
return self._initiate_sso(idp_id)
def establish_trust(self, idp_config):
"""IdP間の信頼関係確立"""
if idp_config['type'] == 'saml':
return self._setup_saml_trust(idp_config)
elif idp_config['type'] == 'oidc':
return self._setup_oidc_trust(idp_config)
'''
}
}
def real_world_case_studies(self):
"""実際の導入事例"""
return {
'global_manufacturer': {
'company_profile': {
'industry': '製造業',
'employees': '50,000+',
'countries': '30+',
'systems': '200+'
},
'challenges': [
'各国の規制対応',
'レガシーシステムの統合',
'M&Aによる異なるIT環境'
],
'solution': {
'architecture': 'ハイブリッド(SAML + OIDC)',
'implementation': '''
Phase 1: 地域別IdPの構築
- EMEA: AD FS (SAML)
- Americas: Okta (SAML/OIDC)
- APAC: Azure AD (SAML/OIDC)
Phase 2: フェデレーション確立
- 地域間の信頼関係
- 属性マッピングの標準化
Phase 3: アプリケーション移行
- 優先度に基づく段階的移行
- レガシーアプリのプロキシ経由統合
''',
'results': {
'password_reset_reduction': '75%',
'onboarding_time': '2日 → 2時間',
'security_incidents': '60%削減'
}
}
},
'financial_services': {
'company_profile': {
'industry': '金融サービス',
'employees': '20,000+',
'regulations': ['PCI-DSS', 'SOX', 'GDPR'],
'security_requirements': 'HIGHEST'
},
'implementation': {
'special_requirements': [
'ステップアップ認証',
'コンテキストベース認証',
'セッション管理の厳格化'
],
'technical_solution': '''
class RiskBasedAuthentication:
def evaluate_auth_requirements(self, context):
risk_score = self.calculate_risk_score(context)
if risk_score < 30:
return {'method': 'password', 'session_lifetime': 8*3600}
elif risk_score < 70:
return {'method': 'password+otp', 'session_lifetime': 4*3600}
else:
return {'method': 'password+otp+biometric', 'session_lifetime': 1*3600}
def calculate_risk_score(self, context):
score = 0
# デバイスの信頼性
if not context.is_managed_device:
score += 30
# アクセス場所
if context.is_unusual_location:
score += 40
# アクセス対象
if context.resource_sensitivity == 'high':
score += 30
return score
'''
}
}
}
def common_challenges_and_solutions(self):
"""共通の課題と解決策"""
return {
'legacy_system_integration': {
'challenge': 'レガシーシステムがSAML/OIDC非対応',
'solutions': [
{
'approach': 'リバースプロキシ',
'implementation': '''
# Nginxでのヘッダーインジェクション
location /legacy-app {
auth_request /auth/verify;
auth_request_set $user $upstream_http_x_user;
auth_request_set $email $upstream_http_x_email;
proxy_set_header X-User $user;
proxy_set_header X-Email $email;
proxy_pass http://legacy-app-server;
}
'''
},
{
'approach': 'エージェントベース',
'description': 'アプリケーションサーバーにエージェント導入'
}
]
},
'attribute_mapping': {
'challenge': '異なるシステム間での属性の不一致',
'solution': '''
class AttributeMapper:
def __init__(self):
self.mappings = {
'saml_to_internal': {
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress': 'email',
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name': 'full_name',
'Department': 'department',
'EmployeeID': 'employee_id'
},
'oidc_to_internal': {
'email': 'email',
'name': 'full_name',
'department': 'department',
'employee_id': 'employee_id'
}
}
def map_attributes(self, source_attributes, source_type):
mapping = self.mappings.get(f'{source_type}_to_internal', {})
internal_attributes = {}
for source_key, source_value in source_attributes.items():
internal_key = mapping.get(source_key, source_key)
internal_attributes[internal_key] = source_value
return internal_attributes
def validate_required_attributes(self, attributes):
required = ['email', 'employee_id']
missing = [attr for attr in required if attr not in attributes]
if missing:
raise AttributeMissingError(f"Missing required attributes: {missing}")
'''
},
'session_management': {
'challenge': 'グローバルログアウトの実装',
'approaches': {
'front_channel_logout': {
'description': 'ブラウザ経由でのログアウト通知',
'pros': 'シンプル',
'cons': 'ブラウザ依存、信頼性低い'
},
'back_channel_logout': {
'description': 'サーバー間でのログアウト通知',
'pros': '信頼性高い',
'cons': '実装複雑',
'implementation': '''
async def handle_backchannel_logout(logout_token):
# ログアウトトークンの検証
claims = validate_logout_token(logout_token)
# 該当セッションの特定
session_id = claims.get('sid')
user_id = claims.get('sub')
# セッションの無効化
if session_id:
await invalidate_session(session_id)
elif user_id:
await invalidate_all_user_sessions(user_id)
# 他のサービスへの伝播
await propagate_logout(user_id)
'''
}
}
}
}
7.4.2 セキュリティとコンプライアンス
class SSOSecurityCompliance:
"""SSOのセキュリティとコンプライアンス"""
def security_best_practices(self):
"""セキュリティベストプラクティス"""
return {
'token_security': {
'signing': {
'algorithms': ['RS256', 'ES256'],
'key_management': '''
class KeyRotationManager:
def __init__(self):
self.rotation_interval = 90 * 24 * 3600 # 90日
self.keys = self._load_keys()
def get_current_key(self):
"""現在の署名鍵を取得"""
return self.keys['current']
def rotate_keys(self):
"""鍵のローテーション"""
# 新しい鍵を生成
new_key = self._generate_key_pair()
# 鍵の更新
self.keys = {
'current': new_key,
'previous': self.keys['current'],
'next_rotation': time.time() + self.rotation_interval
}
# JWKSエンドポイントの更新
self._update_jwks()
# 監査ログ
audit_log.info("Key rotation completed", {
'key_id': new_key['kid'],
'algorithm': new_key['alg']
})
'''
},
'encryption': {
'when_needed': [
'PII(個人識別情報)を含む場合',
'ネットワーク境界を越える場合',
'規制要件がある場合'
],
'implementation': 'JWE (JSON Web Encryption)'
}
},
'authentication_assurance': {
'levels': {
'aal1': {
'description': '単一要素認証',
'methods': ['password'],
'use_case': '低リスクアクセス'
},
'aal2': {
'description': '多要素認証',
'methods': ['password+otp', 'password+push'],
'use_case': '中リスクアクセス'
},
'aal3': {
'description': 'ハードウェアベース認証',
'methods': ['fido2', 'smartcard'],
'use_case': '高リスクアクセス'
}
},
'implementation': '''
def determine_required_aal(resource, user_context):
"""必要な認証保証レベルを決定"""
# リソースの機密性
resource_sensitivity = classify_resource(resource)
# ユーザーコンテキスト
risk_factors = assess_risk(user_context)
# AAL決定ロジック
if resource_sensitivity == 'high' or risk_factors['score'] > 70:
return 'aal3'
elif resource_sensitivity == 'medium' or risk_factors['score'] > 40:
return 'aal2'
else:
return 'aal1'
'''
},
'audit_and_monitoring': {
'required_events': [
'authentication_success',
'authentication_failure',
'token_issued',
'token_refreshed',
'logout',
'authorization_change'
],
'implementation': '''
class SSOAuditLogger:
def __init__(self):
self.logger = self._setup_logger()
self.siem_client = SIEMClient()
def log_authentication_event(self, event_type, context):
"""認証イベントのログ"""
event = {
'timestamp': datetime.utcnow().isoformat(),
'event_type': event_type,
'user_id': context.get('user_id'),
'ip_address': context.get('ip_address'),
'user_agent': context.get('user_agent'),
'authentication_method': context.get('auth_method'),
'result': context.get('result'),
'risk_score': context.get('risk_score'),
'session_id': context.get('session_id')
}
# ローカルログ
self.logger.info(json.dumps(event))
# SIEM転送
self.siem_client.send_event(event)
# アラート条件チェック
self._check_alert_conditions(event)
def _check_alert_conditions(self, event):
"""アラート条件のチェック"""
# 連続した認証失敗
if event['event_type'] == 'authentication_failure':
failure_count = self._get_recent_failures(
event['user_id'],
window=300 # 5分
)
if failure_count >= 5:
self._trigger_alert('excessive_auth_failures', event)
# 異常なアクセスパターン
if event.get('risk_score', 0) > 80:
self._trigger_alert('high_risk_access', event)
'''
}
}
def compliance_requirements(self):
"""コンプライアンス要件"""
return {
'gdpr': {
'requirements': [
'データ最小化原則',
'目的外利用の禁止',
'データポータビリティ',
'忘れられる権利'
],
'implementation': '''
class GDPRCompliantSSO:
def minimize_data_collection(self):
"""データ最小化"""
# 必要最小限の属性のみ要求
return {
'required_claims': ['sub', 'email'],
'optional_claims': ['name'],
'prohibited_claims': ['gender', 'birthdate'] # 業務上不要
}
def implement_consent_management(self):
"""同意管理"""
return {
'consent_required_for': [
'marketing_communications',
'data_analytics',
'third_party_sharing'
],
'ui_implementation': 'Granular consent checkboxes',
'storage': 'Consent records with timestamps'
}
def handle_data_requests(self, request_type, user_id):
"""データリクエストの処理"""
if request_type == 'access':
return self.export_user_data(user_id)
elif request_type == 'deletion':
return self.delete_user_data(user_id)
elif request_type == 'portability':
return self.export_portable_data(user_id)
'''
},
'sox': {
'requirements': [
'アクセス制御の文書化',
'職務分離',
'変更管理',
'監査証跡'
],
'controls': {
'access_certification': '四半期ごとのアクセス権棚卸し',
'privileged_access': '特権アクセスの追加監視',
'change_control': 'IdP設定変更の承認プロセス'
}
}
}
まとめ
この章では、エンタープライズ環境でのSSO実現に不可欠な技術を学びました:
- フェデレーション認証の概念
- 組織間連携の必要性
- 信頼関係の確立
- 中央集権vs分散モデル
- OpenID Connectの仕組み
- OAuth 2.0との違い
- IDトークンの役割
- 各種フローと実装
- SAMLとの比較
- 技術的な違い
- それぞれの適用領域
- 移行・共存戦略
- エンタープライズでの活用
- 実装パターン
- 実際の導入事例
- セキュリティとコンプライアンス
次章では、これらの知識を基に、実際の認証システムの設計に入ります。
演習問題
問題1:OpenID Connect実装
以下の要件を満たすOpenID Connect RPを実装しなさい:
- ディスカバリによる自動設定
- IDトークンの完全な検証
- UserInfoエンドポイントの利用
- セッション管理
問題2:SAML統合
既存のWebアプリケーションにSAML SPを統合する設計を作成しなさい:
- メタデータ管理
- 属性マッピング
- シングルログアウト対応
問題3:プロトコル選択
以下のシナリオに対して、OpenID ConnectとSAMLのどちらを選択すべきか、理由とともに説明しなさい:
- 大学間の学術リソース共有
- スタートアップのSaaS統合
- 銀行のモバイルアプリ
- 製造業のサプライチェーン連携
問題4:ハイブリッド実装
SAMLとOpenID Connectの両方をサポートするIdPの設計を作成しなさい:
- 共通認証基盤
- プロトコル変換
- 属性の相互マッピング
問題5:セキュリティ監査
提供されたSSO実装に対してセキュリティ監査を実施し、改善提案を作成しなさい。