Salesforce シングルサインオン (SSO) をマスターする:セキュアなID管理に関するアーキテクトの視点

背景と適用シナリオ

現代のエンタープライズ環境において、従業員、パートナー、顧客は日々多数のアプリケーションにアクセスする必要があります。その結果、password fatigue (パスワード疲労) という問題が深刻化しています。ユーザーは複数のパスワードを記憶・管理する必要に迫られ、結果として単純なパスワードを使い回す、付箋に書き留めるなどの危険な行動に走り、セキュリティリスクが増大します。また、IT部門にとっては、ユーザーアカウントのプロビジョニング、パスワードリセット、アクセス権の剥奪といった管理タスクが大きな負担となります。

Single Sign-On (SSO) (シングルサインオン) は、これらの課題に対する強力なソリューションです。SSOを導入することで、ユーザーは一度の認証で複数の許可されたアプリケーションにシームレスにアクセスできるようになります。Salesforceアーキテクトの視点から見ると、SSOは単なる利便性向上のツールではありません。それは、企業のID管理戦略の中核をなし、セキュリティ体制を強化し、運用効率を劇的に改善するためのアーキテクチャ上の重要な決定です。

具体的な適用シナリオとしては、以下のようなケースが挙げられます。

  • 社内利用: 従業員が企業のポータルサイト(例: Microsoft SharePoint)やID管理システム(例: Okta, Azure Active Directory)にログインするだけで、Salesforceにも自動的にアクセスできる。
  • パートナーコミュニティ: パートナー企業の担当者が、自社の認証システムを用いてSalesforce Partner Communityにログインできる。
  • 顧客向けサービス: 顧客が、GoogleやFacebookなどのソーシャルアカウントを利用して、Salesforce Customer Communityや外部アプリケーションにログインできる。

これらのシナリオを実現するためには、堅牢でスケーラブルなSSOアーキテクチャの設計が不可欠です。


原理の説明

SalesforceにおけるSSOは、主に2つの標準プロトコル、SAML (Security Assertion Markup Language)OpenID Connect (OIDC) に基づいて実装されます。アーキテクトは、要件に応じてどちらのプロトコルを選択するか、またそのフローを深く理解する必要があります。

SSOの仕組みを理解する上で重要な概念が、Identity Provider (IdP) (IDプロバイダー)Service Provider (SP) (サービスプロバイダー) です。IdPはユーザーの認証情報を管理・検証するシステム(例: Azure AD, ADFS, Okta)であり、SPはユーザーがアクセスしたいサービス(この場合はSalesforce)です。

SAML 2.0

SAMLは、XMLベースのプロトコルで、主にエンタープライズ向けのWebベースSSOで広く利用されています。SAMLの認証フローは以下のようになります。

1. ユーザーアクセス: ユーザーがSalesforce (SP) にアクセスしようとします。
2. SPからIdPへのリダイレクト: Salesforceはユーザーが未認証であると判断し、ユーザーのブラウザをIdPのログインページにリダイレクトします。
3. IdPでの認証: ユーザーはIdPに対して認証情報(ユーザー名、パスワード、多要素認証など)を入力します。
4. SAMLアサーションの生成: 認証が成功すると、IdPはユーザーのID情報(ユーザー名、メールアドレス、プロファイルなど)を含むデジタル署名付きのXMLドキュメント、すなわちSAML Assertion (SAMLアサーション) を生成します。
5. SPへのアサーション送信: IdPは、このSAMLアサーションをユーザーのブラウザ経由でSalesforceのAssertion Consumer Service (ACS) URLにPOSTします。
6. アサーションの検証とログイン: Salesforceは受け取ったアサーションの署名を検証し、発行者や対象者などの情報が信頼できるものであることを確認します。検証が成功すると、ユーザーのセッションを作成し、ログインを許可します。

このフローにおいて、SalesforceとIdPは直接通信するのではなく、ユーザーのブラウザを介して情報をやり取りします。Salesforce側のユーザーを一意に識別するために、Federation ID が一般的に利用されます。これはIdPのユーザー識別子(例: 社員番号)とSalesforceのユーザーレコードを紐付けるためのキーとなります。

OpenID Connect (OIDC)

OIDCは、OAuth 2.0 プロトコルを拡張した、よりモダンな認証プロトコルです。モバイルアプリケーションやシングルページアプリケーション (SPA) との親和性が高く、APIベースの連携にも適しています。OIDCはJSON Web Tokens (JWT) を使用してID情報をやり取りします。

OIDCのフロー(認可コードフロー)は以下のようになります。

1. ユーザーアクセス: ユーザーがSalesforce (OIDCでは Relying Party (RP) と呼ばれる) にアクセスします。
2. OPへのリダイレクト: SalesforceはユーザーをOpenID Provider (OP)(例: Google, Salesforce自身, Auth0)の認証ページにリダイレクトします。
3. 認証と同意: ユーザーはOPで認証を行い、Salesforceが自身の情報にアクセスすることに同意 (consent) します。
4. 認可コードの返却: OPは、Authorization Code (認可コード) を生成し、ユーザーのブラウザを介してSalesforceに返します。
5. トークンの交換: Salesforceはバックチャネル(サーバー間通信)で、受け取った認可コードをOPのトークンエンドポイントに送信し、ID TokenAccess Token を要求します。
6. ID Tokenの検証とログイン: Salesforceは受け取ったID Token (JWT形式) の署名と内容(claims)を検証します。検証が成功すると、ID Token内のユーザー情報(例: `sub` クレーム)を基にユーザーを特定し、セッションを確立してログインを許可します。

Just-in-Time (JIT) プロビジョニング

アーキテクチャ設計において、ユーザープロビジョニングは重要な考慮事項です。Just-in-Time (JIT) Provisioning は、ユーザーがSSOで初めてSalesforceにログインした際に、ユーザーアカウントを自動的に作成または更新する仕組みです。SAMLアサーションやOIDCのID Tokenに含まれる属性情報(部署、役職など)を利用して、Salesforceのユーザーレコードを動的に生成・更新できます。

JITプロビジョニングを利用することで、管理者は事前に全ユーザーを手動で作成する必要がなくなり、運用負荷を大幅に削減できます。さらに複雑なロジックが必要な場合は、ApexでカスタムのJITハンドラを実装することも可能です。


示例コード

複雑なユーザープロビジョニングロジック、例えば特定の属性に基づいてプロファイルやロールを動的に割り当てる、あるいはカスタムオブジェクトにログを記録するといった要件がある場合、ApexでカスタムのSAML JITハンドラを作成します。以下は、Salesforce公式ドキュメントに基づく `SamlJitHandler` インターフェースの実装例です。

// SamlJitHandlerインターフェースを実装したグローバルクラスを定義します。
// このハンドラは、SSO構成ページで指定される必要があります。
global class SamlJitHandler implements Auth.SamlJitHandler {

    // createUserメソッドは、ユーザーがSalesforceに存在しない場合に呼び出されます。
    // SAMLアサーションの情報を使って新しいユーザーを作成します。
    private class JitUser createUser(String samlSsoProviderId, String communityId, String portalId,
        String federationIdentifier, Map<String, String> attributes, String assertion) {
        
        // 新しいUserオブジェクトをインスタンス化します。
        User u = new User();

        // SAMLアサーションの属性から必要な情報を取得します。
        // 'User.Username'、'User.Email'、'User.LastName'、'User.Alias'は必須です。
        // 属性マップのキーは、SSO設定で定義した属性名と一致する必要があります。
        u.Username = attributes.get('username');
        u.Email = attributes.get('email');
        u.LastName = attributes.get('lastname');
        u.FirstName = attributes.get('firstname');
        
        // エイリアスは8文字以内で一意である必要があります。
        String alias = attributes.get('username');
        if (alias.length() > 8) {
            alias = alias.substring(0, 8);
        }
        u.Alias = alias;
        
        // コミュニティのログインであれば、PortalIdからAccountIdとContactIdを検索します。
        if (portalId != null) {
            Account a = [SELECT Id FROM Account WHERE Id IN (SELECT AccountId FROM NetworkMember WHERE NetworkId = :portalId) LIMIT 1];
            u.AccountId = a.Id;
            Contact c = [SELECT Id FROM Contact WHERE AccountId = :a.Id LIMIT 1];
            u.ContactId = c.Id;
        }

        // タイムゾーンやロケールなどの追加情報を設定します。
        u.TimeZoneSidKey = 'America/Los_Angeles';
        u.LocaleSidKey = 'en_US';
        u.EmailEncodingKey = 'UTF-8';
        u.LanguageLocaleKey = 'en_US';
        
        // Federation IDを設定します。これはIdPのユーザーとSalesforceユーザーを紐付けるための重要なキーです。
        u.FederationIdentifier = federationIdentifier;

        // 属性に基づいてプロファイルを動的に割り当てます。
        // ここでは、'user_profile'属性の値に応じてプロファイルIDを決定しています。
        // このロジックはビジネス要件に合わせてカスタマイズします。
        String profileName = attributes.get('user_profile');
        if (profileName != null) {
            Profile p = [SELECT Id FROM Profile WHERE Name = :profileName];
            u.ProfileId = p.Id;
        }
        
        // JitUserクラスのインスタンスを返します。
        // 2番目の引数は、リダイレクト先のURLです。nullの場合、SAMLリレー状態で指定されたページにリダイレクトされます。
        return new JitUser(u, null);
    }
    
    // updateUserメソッドは、ユーザーが既にSalesforceに存在する場合に呼び出されます。
    // SAMLアサーションの情報を使って既存のユーザー情報を更新します。
    private void updateUser(Id userId, String samlSsoProviderId, String communityId, String portalId,
        String federationIdentifier, Map<String, String> attributes, String assertion) {
        
        // 更新対象のUserオブジェクトをインスタンス化し、IDを設定します。
        User u = new User(Id = userId);

        // SAMLアサーションの属性から更新したいフィールドを取得します。
        u.Username = attributes.get('username');
        u.Email = attributes.get('email');
        u.LastName = attributes.get('lastname');
        u.FirstName = attributes.get('firstname');
        
        // ユーザー情報を更新します。
        update(u);
    }

    // handleJitメソッドが最初に呼び出されるエントリポイントです。
    // ユーザーが存在するかどうかを判断し、createUserまたはupdateUserを呼び出します。
    global void handleJit(String samlSsoProviderId, String communityId, String portalId,
        Id userId, String federationIdentifier, Map<String, String> attributes, String assertion) {
        
        // userIdがnullの場合、ユーザーは存在しないため、createUserを呼び出します。
        if (userId == null) {
            JitUser u = createUser(samlSsoProviderId, communityId, portalId,
                federationIdentifier, attributes, assertion);
            
            // ユーザー作成ロジック
            if (u.user.ProfileId != null) {
                // データベースにユーザーを挿入します。
                insert(u.user);
                
                // 必要であれば、リダイレクト先を指定します。
                // String startUrl = u.startUrl;
                // if (startUrl != null) {
                //    // ApexPages.PageReferenceを使用できます。
                // }
            } else {
                // プロファイルが割り当てられない場合はエラーとして処理します。
                // ここでは例外をスローしていますが、実際にはより丁寧なエラーハンドリングが推奨されます。
                throw new JitHandlerException('Profile not found for user.');
            }
        } else {
            // userIdが存在する場合、updateUserを呼び出します。
            updateUser(userId, samlSsoProviderId, communityId, portalId,
                federationIdentifier, attributes, assertion);
        }
    }
}

注意事項

SSOアーキテクチャを設計・実装する際には、以下の点に注意する必要があります。

  • セキュリティ:
    • 証明書の管理: SAMLでは、SPとIdP間の信頼関係を確立するためにデジタル証明書を使用します。証明書の有効期限を監視し、期限切れになる前に更新するプロセスを確立する必要があります。
    • シングルログアウト (SLO): ユーザーがIdPまたは1つのSPからログアウトした際に、他のすべてのSPセッションも同時に終了させる仕組みです。SLOの実装は複雑であり、すべてのアプリケーションが対応しているとは限らないため、慎重な計画が必要です。
    • 多要素認証 (MFA): MFAはIdP側で強制することがベストプラクティスです。これにより、Salesforceだけでなく、IdPに接続されているすべてのアプリケーションのセキュリティを一度に強化できます。
  • ユーザープロビジョニングとデプロビジョニング:
    • JITプロビジョニングはユーザー作成・更新には便利ですが、ユーザーの非アクティブ化(デプロビジョニング)は処理しません。従業員が退職した場合などに、IdP側でアクセス権を剥奪しても、Salesforce上のユーザーアカウントはアクティブなまま残ります。これを解決するには、SCIM (System for Cross-domain Identity Management) のようなプロビジョニング専用プロトコルを併用するか、定期的にIdPとSalesforceのユーザー情報を同期するバッチ処理を構築する必要があります。
  • IDマッピング:
    • IdPとSalesforceのユーザーを紐付けるFederation IDは、不変で一意な値(例: 社員ID)を選択することが極めて重要です。メールアドレスのように変更される可能性がある値をキーにすると、将来的にIDの不整合を引き起こすリスクがあります。
  • エラーハンドリングとフォールバック:
    • IdPがダウンした場合やSSO設定に誤りがあった場合に備え、Salesforceのシステム管理者は標準のユーザー名とパスワードでログインできる経路を確保しておく必要があります。これは、SalesforceのログインURLに `?login` を追加(例: `https://login.salesforce.com/?login`)することで実現できます。この「バックドア」へのアクセスは厳格に管理されるべきです。

まとめとベストプラクティス

Single Sign-Onは、SalesforceプラットフォームをエンタープライズのITエコシステムに統合し、セキュリティとユーザーエクスペリエンスを向上させるための基本的なアーキテクチャパターンです。成功するSSO実装のためには、技術的な詳細だけでなく、ビジネスプロセス全体を見据えた戦略的な設計が求められます。

以下に、Salesforceアーキテクトとしてのベストプラクティスをまとめます。

  1. 適切なプロトコルの選択

    従来のエンタープライズWebアプリケーション連携ではSAMLが堅実な選択ですが、モバイルやAPI連携を含むモダンなアーキテクチャではOpenID Connectの採用を積極的に検討します。

  2. IDの信頼できる唯一の情報源 (Single Source of Truth) の確立

    IdPをユーザーID情報のマスターと位置付け、Salesforceはそれに従属するSPとして設計します。ユーザー属性の変更はすべてIdPで行い、SSOを通じてSalesforceに反映させるのが理想的なモデルです。

  3. 完全なユーザーライフサイクルの計画

    ログイン(認証)だけでなく、ユーザーの作成(プロビジョニング)、更新、そして最も重要な非アクティブ化(デプロビジョニング)までの完全なライフサイクルを考慮したソリューションを設計します。必要に応じてSCIMやカスタム統合を組み合わせます。

  4. 委任認証モデルの検討

    Salesforceが認証要求をIdPにリアルタイムで検証させる「委任認証」も選択肢の一つです。これは、パスワードポリシーをIdPに一元化したい場合に有効ですが、IdPが常に利用可能である必要があります。このモデルとSSOのトレードオフを理解し、要件に合った方式を選択します。

  5. 段階的な導入とテスト

    本番環境に展開する前に、必ずSandbox環境でSSO設定の徹底的なテストを行います。パイロットグループを対象に段階的に展開し、フィードバックを収集しながら全社展開に進めることで、リスクを最小限に抑えます。

  6. 緊急アクセス手順の文書化と周知

    IdP障害時に備えた管理者向けの緊急ログイン手順を文書化し、関係者に周知徹底します。これにより、インシデント発生時にも迅速に対応できます。

これらの原則に従うことで、SalesforceのSSO実装は単なる技術的なタスクではなく、ビジネスの俊敏性とセキュリティを支える戦略的な基盤となります。

コメント