Salesforce OpenID Connect 連携:エンジニア向け完全ガイド

背景と適用シナリオ

現代のエンタープライズ環境では、複数のクラウドサービスやオンプレミスアプリケーションが混在しており、ユーザーは日々多くのシステムにログインする必要があります。これにより、パスワード管理の煩雑化やセキュリティリスクの増大といった課題が生じています。Salesforce 連携エンジニアとして、我々の使命はこれらのシステムをシームレスかつ安全に連携させ、ビジネスプロセスの効率を最大化することです。ここで重要な役割を果たすのが、OpenID Connect (OIDC) (OpenID Connect、オープンアイディーコネクト) です。

OpenID Connect は、業界標準の認証プロトコルであり、OAuth 2.0 (OAuth 2.0、オーオースニーテンゼロ) プロトコルの上に構築されたシンプルなアイデンティティレイヤーです。OAuth 2.0 が「認可」(何ができるか)に焦点を当てているのに対し、OpenID Connect は「認証」(誰であるか)を扱うことに特化しています。これにより、アプリケーションはユーザーの本人確認を、信頼できる Identity Provider (IdP) (IDプロバイダー、アイデンティティプロバイダー) に委任することができます。

具体的な適用シナリオ

Salesforce 環境における OpenID Connect の主な適用シナリオは以下の通りです。

  • シングルサインオン (SSO) の実現: 従業員が Google、Microsoft 365、Okta といった企業の IdP アカウントを使用して、一度のログインで Salesforce やその他の連携アプリケーションにアクセスできるようにします。これにより、ユーザー体験が向上し、管理者のパスワードリセット業務も削減されます。
  • Salesforce を IdP として利用: Salesforce を中心としたエコシステムを構築している場合、Salesforce を IdP として機能させ、外部のカスタムアプリケーション(例: Heroku 上の顧客ポータル、Experience Cloud サイト外の Web アプリ)への認証を提供します。これにより、Salesforce のユーザー情報を信頼の基点として活用できます。
  • セキュアな API 連携: ユーザーのコンテキストで API を呼び出す必要がある場合に、OpenID Connect フローを通じて取得した ID Token を用いてユーザーを特定し、Access Token で API アクセスを保護します。これにより、どのユーザーが操作を実行したかを確実に追跡できます。

連携エンジニアの視点から見ると、OpenID Connect は標準化されたプロトコルであるため、異なるベンダーのシステム間でも高い相互運用性を確保できるという大きな利点があります。これにより、カスタム実装の複雑さを軽減し、より堅牢で保守性の高い連携ソリューションを構築することが可能になります。


原理の説明

OpenID Connect の動作を理解するためには、まず主要な登場人物と概念を把握する必要があります。これらはプロトコルの根幹をなす要素です。

主要な用語

  • Relying Party (RP): 依拠当事者。IdP を信頼し、ユーザーの認証とアイデンティティ情報を要求するアプリケーションを指します。Salesforce を IdP に接続する場合、Salesforce が RP となります。
  • Identity Provider (IdP): IDプロバイダー。ユーザーアカウントを管理し、認証を行い、RP の要求に応じてユーザーに関する情報(クレーム)を提供するサーバーです。Google や Okta、または Salesforce 自身も IdP になり得ます。
  • ID Token: IDトークン。ユーザーの認証に関する情報を含む JSON Web Token (JWT) (JSON Web Token、ジェイソンウェブトークン) 形式のトークンです。ユーザーの一意の識別子 (sub)、発行者 (iss)、対象者 (aud) などの情報が含まれており、RP はこのトークンを検証することでユーザーが確かに認証されたことを確認します。
  • Access Token: アクセストークン。OAuth 2.0 から引き継がれた概念で、特定のリソース(API など)へのアクセス権限を保持するトークンです。
  • UserInfo Endpoint: ユーザー情報エンドポイント。RP が Access Token を使用して、ID Token に含まれていない追加のユーザー情報(氏名、メールアドレスなど)を取得できる IdP 上の保護された API エンドポイントです。

認証コードフロー (Authorization Code Flow)

Web アプリケーションにおいて最も一般的で安全なフローが「認証コードフロー」です。このフローは、機密情報であるトークンがブラウザ経由で直接やり取りされるのを防ぎ、サーバーサイドで安全に交換されるように設計されています。連携エンジニアとしてこのフローを正確に理解することは不可欠です。

以下に、ユーザーが Salesforce (RP) にログインしようとし、外部 IdP を利用する際のステップを示します。

  1. 認証要求: ユーザーが Salesforce のログインページで「Google でログイン」などのボタンをクリックします。Salesforce はユーザーのブラウザを IdP の認証エンドポイント (Authorization Endpoint) にリダイレクトします。このリクエストには、`client_id`、`redirect_uri`、`response_type=code`、`scope=openid profile email` などのパラメータが含まれます。
  2. ユーザー認証と同意: ユーザーは IdP のログイン画面で認証情報(ID/パスワード、MFA など)を入力します。認証後、IdP はユーザーに対し、Salesforce が要求している情報(プロフィール、メールアドレスなど)へのアクセスを許可するかどうかの同意を求めます。
  3. 認証コードの発行: ユーザーが同意すると、IdP はユーザーのブラウザを Salesforce 側で指定された `redirect_uri` にリダイレクトさせます。このとき、URL のクエリパラメータとして一度限りの「認証コード (Authorization Code)」が付与されます。
  4. トークン交換: Salesforce のバックエンドサーバーは、受け取った認証コードと、事前に IdP に登録しておいた `client_id` および `client_secret` を使用して、IdP のトークンエンドポイント (Token Endpoint) に直接リクエストを送信します。
  5. トークン発行と検証: IdP は認証コードとクライアント情報を検証し、問題がなければ ID TokenAccess Token を Salesforce のバックエンドに返します。
  6. ログインセッションの確立: Salesforce は受け取った ID Token の署名、発行者 (iss)、対象者 (aud) などを検証し、正当性を確認します。検証が成功すると、ID Token に含まれるユーザー識別子を基に Salesforce 内のユーザーとマッピングし、ログインセッションを確立します。ユーザーが見つからない場合は、Just-in-Time (JIT) プロビジョニングにより新規ユーザーを作成することも可能です。

この一連の流れにより、ユーザーのパスワードが Salesforce に渡ることなく、安全に認証が完了します。


サンプルコード

Salesforce を RP として設定し、外部 IdP で認証されたユーザーを Salesforce 内に自動的に作成または更新(Just-in-Time プロビジョニング)するためには、Apex の `Auth.RegistrationHandler` インターフェースを実装したクラスを作成する必要があります。このクラスは、IdP から受け取ったユーザー情報に基づいて、ユーザーの作成や更新ロジックをカスタマイズする役割を担います。

以下は、Salesforce の公式ドキュメントに基づく `Auth.RegistrationHandler` の実装例です。このコードは、ソーシャルサインオン(例: Facebook や Google)で初めてログインしたユーザーのために新しい Salesforce ユーザーを作成し、既存ユーザーの場合は情報を更新します。

Registration Handler の Apex コード例

global class MyRegHandler implements Auth.RegistrationHandler {

    // createUser メソッドは、IdP からの認証情報をもとに新しいユーザーを作成します。
    // このメソッドは、Salesforce 内に該当するユーザーが存在しない場合に呼び出されます。
    global User createUser(Id portalId, Auth.UserData data) {
        // portalId は、Experience Cloud サイトの ID です。指定されていない場合は null になります。
        // data オブジェクトには、IdP から提供されたユーザー情報が含まれます。
        // (例: data.firstName, data.lastName, data.email, data.attributeMap.get('company'))

        if (data.email != null && data.email != '') {
            // 同じメールアドレスを持つ取引先責任者を検索
            List<Contact> contacts = [SELECT Id, AccountId FROM Contact WHERE Email = :data.email];
            if (!contacts.isEmpty()) {
                // 取引先責任者が見つかった場合、新しいユーザーオブジェクトを作成
                User u = new User();
                // ProfileId は組織に合わせてハードコードするか、動的に取得する必要があります
                Profile p = [SELECT Id FROM Profile WHERE Name = 'Customer Community User'];
                u.ProfileId = p.Id;
                
                // IdP からの情報を使用してユーザーの項目を設定
                u.FirstName = data.firstName;
                u.LastName = data.lastName;
                u.Email = data.email;
                u.CommunityNickname = data.username.substring(0, data.username.indexOf('@'));
                u.Username = data.email; // ユーザ名は一意である必要があります
                u.Alias = u.CommunityNickname.substring(0, Math.min(u.CommunityNickname.length(), 8)); // Alias は最大8文字
                u.TimeZoneSidKey = 'America/Los_Angeles';
                u.LocaleSidKey = 'en_US';
                u.EmailEncodingKey = 'UTF-8';
                u.LanguageLocaleKey = 'en_US';
                
                // 取引先責任者と取引先をユーザーに関連付け
                u.ContactId = contacts[0].Id;
                u.AccountId = contacts[0].AccountId;
                
                return u;
            }
        }
        // 条件に合致しない場合は null を返し、ユーザー作成を中止します
        return null;
    }

    // updateUser メソッドは、既存のユーザーが IdP 経由でログインした際に呼び出されます。
    // ユーザー情報の更新や、ログイン時のカスタムロジックを実装できます。
    global void updateUser(Id userId, Id portalId, Auth.UserData data) {
        // 更新対象のユーザーを取得
        User u = [SELECT Id, FirstName, LastName, Email FROM User WHERE Id = :userId];
        
        // IdP からの最新情報でユーザー情報を更新するロジック
        // 例: 姓や名前に変更があった場合に Salesforce 側も更新
        boolean needsUpdate = false;
        if (data.firstName != null && u.FirstName != data.firstName) {
            u.FirstName = data.firstName;
            needsUpdate = true;
        }
        if (data.lastName != null && u.LastName != data.lastName) {
            u.LastName = data.lastName;
            needsUpdate = true;
        }

        // 変更があった場合のみ DML 操作を実行
        if (needsUpdate) {
            update u;
        }
    }
}

このクラスを認証プロバイダーの設定画面で「登録ハンドラ」として指定することで、OpenID Connect による SSO フローが完了した際にこの Apex コードが自動的に実行されます。


注意事項

OpenID Connect 連携を実装する際には、いくつかの重要な点に注意する必要があります。これらを怠ると、セキュリティ上の脆弱性や予期せぬエラーにつながる可能性があります。

権限と設定

  • メタデータ API 権限: 認証プロバイダーや名前付き資格情報の設定には「アプリケーションのカスタマイズ」および「認証プロバイダーの管理」権限が必要です。
  • Apex 実行権限: 登録ハンドラとして指定された Apex クラスを実行するプロファイルには、そのクラスへのアクセス権が必要です。また、クラス内で実行される SOQL や DML 操作の対象となるオブジェクトや項目へのアクセス権も必要です。
  • CORS とリモートサイト設定: IdP のエンドポイントに Apex からコールアウトする場合(トークンイントロスペクションなど)、該当の URL をリモートサイト設定(または名前付き資格情報)に登録する必要があります。

API 制限とガバナ制限

  • ガバナ制限: 登録ハンドラ(`Auth.RegistrationHandler`)内の Apex コードは、通常のガバナ制限(SOQL クエリの発行回数、DML ステートメントの実行回数、CPU 時間など)に従います。特に、多くのユーザーが同時に初回ログインする可能性がある場合は、コードが効率的に実行されるように注意深く設計する必要があります。
  • API コール: 登録ハンドラ内で外部システムへのコールアウトを行う場合、コールアウトの制限にも注意が必要です。

エラー処理

  • `createUser` メソッドが `null` を返した場合、ユーザーの作成は行われず、ユーザーにはエラーメッセージが表示されます。なぜ `null` を返したのか、ログを残すなどの仕組みを検討することが重要です。
  • DML 操作(`insert u` や `update u`)が失敗した場合(例: 入力規則違反、必須項目不足)、トランザクション全体がロールバックされ、ユーザーはログインできません。`try-catch` ブロックを使用して例外を適切に捕捉し、デバッグログに詳細を記録することを強く推奨します。

セキュリティ

  • State パラメータ: OpenID Connect の認証リクエストでは、CSRF (Cross-Site Request Forgery) 攻撃を防ぐために `state` パラメータの使用が強く推奨されます。Salesforce が RP として機能する場合、この処理はフレームワークによって自動的に管理されます。
  • Client Secret の管理: `client_secret` は非常に機密性の高い情報です。バージョン管理システムにハードコードするのではなく、名前付き資格情報や保護されたカスタムメタデータなど、Salesforce の安全な場所に保管してください。
  • ID Token の検証: Salesforce が RP の場合、ID Token の署名、発行者 (iss)、対象者 (aud)、有効期限 (exp) の検証は自動的に行われますが、カスタムで IdP と連携する際はこれらの検証を自前で実装する必要があり、漏れなく行うことが不可欠です。

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

OpenID Connect は、Salesforce と外部システム間の認証連携を標準化し、セキュリティとユーザー体験を大幅に向上させるための強力なツールです。連携エンジニアとしてこの技術をマスターすることは、現代の複雑な IT 環境において不可欠なスキルと言えるでしょう。

ベストプラクティス

  1. 認証コードフローを優先する: クライアントサイドのアプリケーションであっても、セキュリティを最大化するために、サーバーサイドのコンポーネントと連携する認証コードフロー(PKCE を伴う場合も含む)を常に第一選択肢とします。
  2. Just-in-Time (JIT) プロビジョニングを積極的に活用する: `Auth.RegistrationHandler` を実装し、ユーザー管理を自動化することで、管理者の手動作業をなくし、ヒューマンエラーを削減します。ロジックはできるだけシンプルに保ち、パフォーマンスを意識してください。
  3. 設定の外部化: IdP のエンドポイント URL や `client_id` といった設定値は、Apex コード内にハードコードせず、カスタムメタデータ型やカスタム設定に格納します。これにより、サンドボックスと本番環境での切り替えや、将来的な設定変更が容易になります。
  4. 最小権限の原則に従う: 認証リクエスト時に要求する `scope` は、必要最小限(例: `openid email profile`)に留めます。不要な情報へのアクセス権を要求しないことで、ユーザーのプライバシーを尊重し、セキュリティリスクを低減します。
  5. 徹底的なテスト: 正常系のログインフローだけでなく、初回ログイン時のユーザー作成、既存ユーザーの更新、IdP 側での同意拒否、無効なユーザーでのログイン試行など、あらゆるシナリオを網羅したテストを実施してください。

OpenID Connect を適切に実装することで、Salesforce はより広範なエコシステムの一部としてシームレスに機能し、ビジネス全体の価値を最大化することができます。このガイドが、皆様の連携プロジェクトの一助となれば幸いです。

コメント