Salesforce シングルサインオン(SSO)をマスターする:アーキテクトのための包括的ガイド

役割:Salesforce アーキテクト


背景と適用シナリオ

現代のエンタープライズ環境において、アプリケーションの数は爆発的に増加しています。従業員は日々の業務で Salesforce をはじめとする多数のシステムにアクセスする必要があり、それぞれに異なる認証情報を管理することは、生産性の低下とセキュリティリスクの増大を招きます。ここで重要な役割を果たすのが Single Sign-On (SSO) 、日本語では「シングルサインオン」です。

SSO は、ユーザーが一度の認証プロセスで複数の関連システムにアクセスできるようにする認証メカニズムです。Salesforce アーキテクトとして、私たちが SSO を設計・導入する主な目的は以下の通りです。

  • ユーザーエクスペリエンスの向上:ユーザーは企業のポータル(例:Microsoft Azure AD, Okta, Google Workspace)に一度ログインするだけで、パスワードを再入力することなく Salesforce にシームレスにアクセスできます。これにより、パスワード忘れによる問い合わせが減少し、業務効率が向上します。
  • セキュリティの強化:認証を一元管理することで、多要素認証(MFA)やパスワードポリシーの強制など、より高度なセキュリティ対策を企業の Identity Provider (IdP)(ID プロバイダー)側で集中的に実施できます。これにより、個々のアプリケーションでのセキュリティ設定のばらつきを防ぎ、企業全体のセキュリティレベルを底上げします。
  • 管理コストの削減:IT 部門はユーザーの認証情報を IdP で一元的に管理できます。従業員の入退社に伴うアカウントの有効化・無効化も IdP で行うだけで、連携するすべてのサービス(Salesforce を含む)へのアクセス権が即座に反映されるため、運用管理の工数が大幅に削減されます。

具体的な適用シナリオとしては、「社内のイントラネットにログインしたら、クリック一つで Salesforce のダッシュボードにアクセスできる」「パートナー企業の担当者が、自社の認証システムを使ってパートナーコミュニティにログインする」といったケースが挙げられます。アーキテクトとしては、これらの要件を満たし、かつスケーラブルでセキュアな認証基盤を設計することが求められます。

原理の説明

Salesforce の SSO を理解するためには、その中核をなすプロトコルと登場人物(コンポーネント)を把握することが不可欠です。エンタープライズ SSO で最も広く利用されているのが SAML (Security Assertion Markup Language) です。

SAML 認証フローの主要なコンポーネントは以下の通りです。

  • User (ユーザー): Salesforce にアクセスしようとしている本人。
  • Service Provider (SP) (サービスプロバイダー): ユーザーが利用したいサービス。この文脈では Salesforce が SP となります。
  • Identity Provider (IdP) (ID プロバイダー): ユーザーを認証し、その身元情報を証明するシステム。企業の Active Directory Federation Services (ADFS) や Okta、Azure AD などがこれに該当します。

一般的な SP-Initiated(SP 起点)の SAML フローは以下のように進行します。

  1. ユーザーがブラウザで Salesforce のログインページにアクセスします。
  2. Salesforce (SP) はユーザーが SSO 対象であると判断し、認証要求(SAML Request)を生成して、ユーザーのブラウザを IdP へリダイレクトさせます。
  3. ユーザーは IdP のログイン画面で認証情報(ID/パスワード、MFA など)を入力し、認証を受けます。すでに IdP にログイン済みの場合は、このステップは省略されます。
  4. 認証が成功すると、IdP はユーザーの身元情報(ユーザー名、メールアドレス、プロファイル情報など)を含む XML 形式の SAML Assertion (SAML アサーション) を生成し、デジタル署名を付与します。
  5. IdP はこの SAML アサーションをユーザーのブラウザに送り返します。
  6. ブラウザは、受け取った SAML アサーションを Salesforce の Assertion Consumer Service (ACS) URL に POST メソッドで送信します。
  7. Salesforce は受け取った SAML アサーションを検証します。具体的には、IdP の公開鍵証明書を使ってデジタル署名を検証し、発行者 (Issuer) や対象者 (Audience) が正しいか、有効期限内であるかなどをチェックします。
  8. 検証が成功すると、Salesforce はアサーション内の Federation ID (フェデレーション ID) と呼ばれる一意の識別子を使って、対応する Salesforce ユーザーを特定します。
  9. 該当するユーザーが見つかると、Salesforce はセッションを確立し、ユーザーをログインさせます。

このフローにより、Salesforce はパスワードを直接受け取ることなく、信頼する IdP からの身元保証(SAML アサーション)に基づいてユーザーを安全に認証できるのです。アーキテクトとしては、このフローの各ステップでどのような情報がやり取りされ、どこにセキュリティ上の要点があるのかを正確に理解しておく必要があります。


Just-in-Time (JIT) プロビジョニングのサンプルコード

SSO の強力な機能の一つに Just-in-Time (JIT) Provisioning があります。これは、ユーザーが初めて SSO で Salesforce にログインしようとした際に、SAML アサーションの情報に基づいて Salesforce ユーザーアカウントを自動的に作成または更新する仕組みです。これにより、手動でのユーザー作成が不要になり、管理が大幅に効率化されます。

JIT プロビジョニングのロジックは Apex クラスでカスタマイズできます。以下に、Salesforce 公式ドキュメントに基づく SamlJitHandler インターフェースを実装した Apex クラスの例を示します。

global class SamlJitHandler implements Auth.SamlJitHandler {

    private class JitException extends Exception {}

    // ユーザーが存在しない場合に呼び出されるメソッド
    global User createUser(Boolean isSaml, Id samlSsoProviderId, Id communityId, Id portalId,
        String federationIdentifier, Map<String, String> attributes, String assertion) {
        
        // 新しいユーザーオブジェクトをインスタンス化
        User u = new User();

        // SAML アサーションの属性からユーザー情報を取得
        // 'User.Username'、'User.Email'、'User.LastName'、'User.FirstName' は必須属性
        if (attributes.containsKey('User.Username')) {
            u.Username = attributes.get('User.Username');
        } else {
            // 必須属性がない場合は例外をスローして処理を中断
            throw new JitException('SAML Assertion is missing Username attribute');
        }
        
        if (attributes.containsKey('User.Email')) {
            u.Email = attributes.get('User.Email');
        } else {
            throw new JitException('SAML Assertion is missing Email attribute');
        }

        if (attributes.containsKey('User.LastName')) {
            u.LastName = attributes.get('User.LastName');
        } else {
            throw new JitException('SAML Assertion is missing LastName attribute');
        }

        if (attributes.containsKey('User.FirstName')) {
            u.FirstName = attributes.get('User.FirstName');
        } else {
            throw new JitException('SAML Assertion is missing FirstName attribute');
        }

        // Federation ID を設定(これが SSO のキーとなる)
        u.FederationIdentifier = federationIdentifier;

        // SAML アサーションからプロファイル名を取得し、対応する Profile ID を検索
        if (attributes.containsKey('User.ProfileName')) {
            String profileName = attributes.get('User.ProfileName');
            Profile p = [SELECT Id FROM Profile WHERE Name = :profileName];
            u.ProfileId = p.Id;
        } else {
            // デフォルトのプロファイルを指定することも可能
            // 例: Profile p = [SELECT Id FROM Profile WHERE Name = 'Standard User'];
            // u.ProfileId = p.Id;
            throw new JitException('SAML Assertion is missing ProfileName attribute');
        }

        // その他の必須項目やカスタム項目を設定
        u.Alias = u.Username.substring(0, 8); // Alias は 8 文字以内
        u.CommunityNickname = u.Username;
        u.EmailEncodingKey = 'ISO-2022-JP';
        u.LanguageLocaleKey = 'ja';
        u.LocaleSidKey = 'ja_JP';
        u.TimeZoneSidKey = 'Asia/Tokyo';

        // DML を実行してユーザーを作成
        // このクラスは without sharing で実行されるため、権限に注意
        insert u;
        return u;
    }

    // ユーザーが既に存在する場合に呼び出されるメソッド
    global void updateUser(Id userId, Boolean isSaml, Id samlSsoProviderId, Id communityId, Id portalId,
        String federationIdentifier, Map<String, String> attributes, String assertion) {

        // 更新対象のユーザーオブジェクトを準備
        User u = new User(Id = userId);

        // SAML アサーションの属性に基づいてユーザー情報を更新
        // 例えば、部署や役職が変更された場合に同期するロジックをここに追加
        if (attributes.containsKey('User.Department')) {
            u.Department = attributes.get('User.Department');
        }
        
        if (attributes.containsKey('User.Title')) {
            u.Title = attributes.get('User.Title');
        }

        // Email が変更された場合も更新
        if (attributes.containsKey('User.Email')) {
            u.Email = attributes.get('User.Email');
        }

        // DML 操作を実行
        update u;
    }
}

注意事項

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

  • 権限:SSO 設定を行うには、「アプリケーションのカスタマイズ」および「シングルサインオン設定の管理」権限が必要です。また、JIT ハンドラの Apex クラスを作成・編集するには「Apex の作成」権限が求められます。
  • Federation ID の選定:Federation ID は、IdP と Salesforce の間でユーザーを一意に識別するためのキーです。変更される可能性が低く、全ユーザーで一意であり、再割り当てされない値(例:従業員番号)を選定することが極めて重要です。変更されうるメールアドレスを Federation ID にすると、メールアドレス変更時に SSO が機能しなくなるリスクがあります。
  • 証明書の管理:SAML アサーションの署名検証に使われる IdP の証明書には有効期限があります。期限切れになるとすべての SSO ログインが失敗するため、証明書の有効期限を監視し、計画的に更新するプロセスを確立する必要があります。
  • JIT ハンドラのガバナ制限:JIT ハンドラの Apex コードは、他の Apex コードと同様にガバナ制限(SOQL クエリの発行回数、DML 操作の回数など)の対象となります。複雑なロジックを実装すると、特に複数ユーザーが同時に初回ログインするようなケースで制限に抵触する可能性があります。コードは可能な限り効率的に記述し、バルク処理を意識する必要があります。
  • エラーハンドリング:JIT プロビジョニングが失敗した場合(例:必須項目が不足、ライセンス不足)、ユーザーは Salesforce にログインできません。JIT ハンドラ内で適切な例外処理を実装し、失敗時には管理者へ通知が飛ぶような仕組みを検討することが望ましいです。
  • 「バックドア」の確保:SSO を有効化し、通常の Salesforce ログインを無効化した場合、IdP 側で障害が発生すると誰も Salesforce にログインできなくなります。この事態に備え、SSO の対象外としたシステム管理者プロファイルを持つユーザーアカウントを最低一つは確保し、その認証情報を安全な場所に保管しておくべきです(例: `https://login.salesforce.com/?login` のように URL にパラメータを追加することで、SSO をバイパスしてログイン画面を表示できます)。

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

Single Sign-On は、単なる利便性向上のための機能ではありません。エンタープライズ全体のセキュリティポリシーを Salesforce に適用し、ID 管理を一元化するための戦略的な基盤です。Salesforce アーキテクトとして、私たちはその技術的な詳細とビジネス上のインパクトの両方を深く理解する必要があります。

以下に、SSO 設計・導入におけるベストプラクティスをまとめます。

  • 綿密な計画:導入前に、どのユーザーを対象とするか、どの IdP を使用するか、Federation ID として何を使用するかなど、ID 管理戦略を明確に定義します。
  • サンドボックスでの徹底的なテスト:本番環境へ展開する前に、必ず Full Sandbox などの環境で IdP との接続テスト、JIT プロビジョニングの動作確認、さまざまなユーザープロファイルでのログインテストを繰り返し行います。
  • JIT プロビジョニングの活用:可能な限り JIT プロビジョニングを活用し、ユーザーライフサイクル管理を自動化します。ただし、そのロジックはシンプルに保ち、エラーハンドリングを堅牢に実装します。
  • 段階的なロールアウト:全社一斉導入ではなく、まずは特定の部署やパイロットユーザーグループを対象に SSO を展開し、フィードバックを得ながら対象範囲を拡大していくアプローチを推奨します。
  • ドキュメントの整備:設定内容、証明書の更新手順、緊急時の対応フローなどを詳細にドキュメント化し、関係者間で共有します。

これらの原則に従うことで、私たちはセキュアでスケーラブル、そしてユーザーフレンドリーな Salesforce の認証基盤を構築し、ビジネスの成功に貢献することができるのです。

コメント