Salesforce OpenID Connect: 安全な認証を実現するための包括的ガイド

背景と適用シナリオ

現代のエンタープライズ環境では、従業員、顧客、パートナーが日々の業務で多数のアプリケーションを利用しています。これらすべてのアプリケーションに対して個別のユーザー名とパスワードを管理することは、ユーザーにとって大きな負担となるだけでなく、セキュリティ上のリスクも増大させます。パスワードの使い回しや単純なパスワードの設定は、不正アクセスの温床となり得ます。

この課題を解決する技術が Single Sign-On (SSO) (シングルサインオン) です。SSOは、ユーザーが一度の認証で複数のアプリケーションやサービスにアクセスできるようにする仕組みです。Salesforceは、SAMLやOpenID Connectといった標準プロトコルをサポートし、SSOのハブとして機能する強力なID管理プラットフォームを提供しています。

本記事では、特にモダンなWebアプリケーションやモバイルアプリケーションで広く採用されている OpenID Connect (OIDC) (OpenIDコネクト) に焦点を当てます。OIDCは、OAuth 2.0 (オーオース2.0) プロトコルを拡張した、シンプルでセキュアなID認証レイヤーです。Salesforce環境におけるOIDCの主な適用シナリオは以下の通りです。

SalesforceをIDプロバイダーとして利用する

最も一般的なシナリオです。ユーザーは使い慣れたSalesforceのログイン情報を使用して、外部のカスタムアプリケーション(例: Heroku上で稼働するWebアプリ、顧客向けポータルサイト、モバイルアプリなど)にログインできます。これにより、カスタムアプリケーション側でユーザー管理やパスワードポリシーを独自に実装する必要がなくなり、セキュリティと利便性が向上します。

Salesforceがサービスプロバイダーとして機能する

外部のIDプロバイダー(例: Google, Microsoft Azure AD, Okta, もしくは自社開発のOpenID Provider)を利用してSalesforceにログインするシナリオです。企業の既存のID管理基盤にSalesforceを統合し、全社的なID管理戦略を一元化する場合に有効です。


原理説明

OpenID Connectの仕組みを理解するためには、まずその基盤であるOAuth 2.0との違いを明確にすることが重要です。

  • OAuth 2.0: 「認可」のためのプロトコルです。ユーザーの同意に基づき、あるアプリケーション(クライアント)が別のアプリケーション(リソースサーバー)上のリソース(例: データ、API)にアクセスするための権限(アクセストークン)を付与する仕組みを定めています。誰であるか(認証)ではなく、何ができるか(認可)に焦点を当てています。
  • OpenID Connect: OAuth 2.0の上に構築された「認証」のためのレイヤーです。OAuth 2.0の認可フローを利用して、ユーザーの身元情報を安全に検証し、その結果を標準化された形式でクライアントに提供します。

OIDCのフローには、いくつかの主要な登場人物(役割)が存在します。

  • End-User (エンドユーザー): 認証を必要とする本人。ブラウザを操作してログインを行います。
  • Relying Party (RP) (リライング・パーティ): エンドユーザーの認証を要求し、その情報を受け取るアプリケーション。サービスプロバイダー(SP)とも呼ばれます。
  • OpenID Provider (OP) (OpenIDプロバイダー): エンドユーザーを認証し、その身元情報(クレーム)を提供するサーバー。IDプロバイダー(IdP)とも呼ばれます。Salesforceがこの役割を担うことができます。

OIDCの中心的な概念は ID Token (IDトークン) です。これは JSON Web Token (JWT) (ジェイソン・ウェブ・トークン) 形式でエンコードされた、認証イベントに関する情報のアサーションです。IDトークンには、誰が、いつ、どのように認証されたかといった情報(クレーム)が含まれており、RPはこれを検証することでユーザーの認証を完了します。

認証フロー (Authorization Code Flow)

最も一般的で安全な「認可コードフロー」の概略は以下の通りです。

  1. 認証リクエスト: RPがエンドユーザーをOPの認証エンドポイントにリダイレクトさせます。このとき、どの情報が必要かを示す `scope`(スコープ)パラメータ(OIDCでは `openid` が必須)や、CSRF攻撃を防ぐための `state` パラメータなどを付与します。
  2. ユーザー認証と同意: エンドユーザーはOPのログイン画面で認証情報(ID/パスワードなど)を入力します。初回アクセスの場合、RPへの情報提供に関する同意を求められます。
  3. 認可コードの発行: 認証と同意が成功すると、OPはエンドユーザーをRPの指定した `redirect_uri`(リダイレクトURI)にリダイレクトさせます。このとき、URLのクエリパラメータとして一時的な「認可コード」が付与されます。
  4. トークンリクエスト: RPのサーバーサイドは、受け取った認可コードと自身のクライアントID/シークレットを使い、OPのトークンエンドポイントにリクエストを送信します。
  5. トークンの発行: OPは認可コードを検証し、問題がなければIDトークンとアクセストークンをRPに返却します。
  6. トークン検証とセッション確立: RPは受け取ったIDトークンの署名やクレーム(発行者、対象者、有効期限など)を検証します。検証が成功すれば、ユーザーの認証は完了し、RPはユーザー向けのセッションを確立します。

示例代码

ここでは、SalesforceがRelying Party (RP) として、外部のOpenID Provider (OP) から発行されたIDトークンを検証するシナリオを考えます。例えば、外部IdPで認証されたユーザー情報を使ってSalesforce内で何らかの処理を行うApex RESTサービスを構築する場合などが該当します。

この実装には、まずSalesforceの「設定」メニューから「認証プロバイダー」を構成し、外部OPの情報を登録しておく必要があります。登録が完了すると、Salesforceは指定された発行者(Issuer)の公開鍵を取得し、JWT署名の検証に利用できるようになります。

以下のApexコードは、`Auth.OidcTokenValidator` クラスを使用してIDトークンを検証する方法を示しています。このクラスは、トークンの署名、発行者、対象者(Audience)、有効期限などを自動的に検証してくれるため、セキュリティクリティカルな処理を安全かつ簡潔に実装できます。

ApexによるIDトークンの検証

// このApexクラスは、外部からIDトークンを受け取り検証するRESTエンドポイントの例です。
@RestResource(urlMapping='/validateToken/*')
global with sharing class OidcTokenValidationService {

    @HttpPost
    global static void validateToken() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;

        // HTTPヘッダーからBearerトークン(IDトークン)を取得
        String authHeader = req.headers.get('Authorization');
        if (String.isBlank(authHeader) || !authHeader.startsWith('Bearer ')) {
            res.statusCode = 401; // Unauthorized
            res.responseBody = Blob.valueOf('{"error":"Missing or invalid Authorization header"}');
            return;
        }
        
        String idToken = authHeader.substring(7); // "Bearer " の部分を削除

        // 認証プロバイダーの「デベロッパー名」を指定します。
        // これは「設定 > ID > 認証プロバイダー」で確認できます。
        String authProviderDeveloperName = 'MyExternalGoogleProvider';
        
        // 発行者URL(issクレーム)は認証プロバイダー設定から自動的に取得されます。
        // デベロッパー名から発行者URLを取得
        String issuerUrl = Auth.AuthConfiguration.getAuthProviderAuthUrl(authProviderDeveloperName);

        if(issuerUrl == null) {
            res.statusCode = 500; // Internal Server Error
            res.responseBody = Blob.valueOf('{"error":"Auth. Provider not found: ' + authProviderDeveloperName + '"}');
            return;
        }

        try {
            // OidcTokenValidatorインスタンスを作成
            // コンストラクタは発行者URLと接続アプリケーション名(または認証プロバイダー名)を引数に取ります。
            Auth.OidcTokenValidator validator = new Auth.OidcTokenValidator(issuerUrl, authProviderDeveloperName);

            // validateメソッドを呼び出してトークンを検証します。
            // 第2、第3引数は、nonceとstateの検証用ですが、この例ではnullを渡しています。
            Auth.VerificationResult result = validator.validate(idToken, null, null);

            if (result.isValid()) {
                // 検証成功
                System.debug('ID Token is valid.');

                // トークンからクレーム(ユーザー情報)を取得
                Map<String, Object> claims = result.getClaims();
                String subject = (String)claims.get('sub');
                String email = (String)claims.get('email');
                
                System.debug('Subject: ' + subject);
                System.debug('Email: ' + email);
                System.debug('Issuer: ' + claims.get('iss'));

                // ここでユーザー情報に基づいたビジネスロジックを実装
                // 例: subjectをキーにカスタムオブジェクトからユーザー情報を検索する等

                res.statusCode = 200; // OK
                res.responseBody = Blob.valueOf('{"status":"success", "subject":"' + subject + '"}');

            } else {
                // 検証失敗
                System.debug('ID Token is invalid.');
                Auth.VerificationError error = result.getError();
                System.debug('Error: ' + error.name());
                System.debug('Error Description: ' + error.getDescription());
                
                res.statusCode = 401; // Unauthorized
                res.responseBody = Blob.valueOf('{"error":"' + error.name() + '", "error_description":"' + error.getDescription() + '"}');
            }

        } catch (Exception e) {
            res.statusCode = 500; // Internal Server Error
            res.responseBody = Blob.valueOf('{"error":"An unexpected error occurred", "details":"' + e.getMessage() + '"}');
        }
    }
}

注意事項

権限と設定

接続アプリケーション (Connected App): SalesforceをOPとして使用する場合、接続アプリケーションの作成が必須です。OAuth設定を有効にし、`openid` スコープを含める必要があります。また、`Callback URL` (リダイレクトURI) は、予期せぬリダイレクトによるトークン漏洩を防ぐため、可能な限り具体的に(ワイルドカードを避けて)指定してください。

認証プロバイダー (Auth. Provider): SalesforceをRPとして使用する場合、外部OPの情報を認証プロバイダーとして登録します。`Issuer URL`, `Client ID`, `Client Secret` などの情報を正確に設定する必要があります。

ユーザープロファイル/権限セット: 接続アプリケーションへのアクセスは、プロファイルまたは権限セット単位で制御できます。「管理者が承認したユーザは事前承認済み」設定を選択した場合、明示的にアクセスを許可されたユーザーのみがそのアプリケーションを利用できます。

API制限

OIDCフローに関連する操作(トークンエンドポイントやUserInfoエンドポイントへのコールなど)は、SalesforceのAPIコール制限を消費します。特に、多数のユーザーが頻繁に認証を行うような公開アプリケーションを構築する際は、API消費量に注意し、必要に応じて制限緩和を検討する必要があります。

エラー処理

認証フローでは様々なエラーが発生する可能性があります。例えば、ユーザーが同意を拒否した場合、OPは `error=access_denied` といったパラメータを付けてRPにリダイレクトします。RP側では、これらのエラーを適切にハンドリングし、ユーザーに分かりやすいメッセージを表示するべきです。また、トークン検証に失敗した場合は、いかなる理由であれ認証を拒否し、そのリクエストをログに記録することが重要です。不正アクセスの試みである可能性も考慮する必要があります。

セキュリティに関する考慮事項

state パラメータ: Cross-Site Request Forgery (CSRF) 攻撃を防ぐために不可欠です。RPは認証リクエストの直前にランダムで推測不可能な値を生成してユーザーセッションに保存し、`state` パラメータとしてOPに送信します。OPからリダイレクトされてきた際に、返却された `state` の値とセッションに保存した値を比較し、一致しない場合はリクエストを破棄しなければなりません。

nonce パラメータ: リプレイ攻撃を防ぐために使用されます。`state` と同様にリクエスト時に生成され、OPから返されるIDトークン内の `nonce` クレームに含まれます。RPは、この値が自身が送信したものと一致することを検証する必要があります。

PKCE (Proof Key for Code Exchange): 特にモバイルアプリやSingle Page Application (SPA)のような、クライアントシークレットを安全に保持できない「パブリッククライアント」にとって極めて重要です。認可コードの横取り攻撃を防ぐための仕組みで、現代のOIDC実装では必須のセキュリティ対策とされています。


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

OpenID Connectは、Salesforceを中心としたエコシステムにおいて、セキュアでモダンな認証連携を実現するための強力な標準プロトコルです。SalesforceをIDプロバイダーとしても、またサービスプロバイダー(リライング・パーティ)としても活用できる柔軟性を持っており、様々なビジネス要件に対応できます。

SalesforceでOIDCを実装する際のベストプラクティスは以下の通りです。

  • 適切なフローの選択: Webサーバーを持つ従来のアプリケーションでは「認可コードフロー」を選択します。クライアントシークレットを安全に保管できないSPAやモバイルアプリでは、必ず「認可コードフロー + PKCE」を利用してください。セキュリティ上の脆弱性から、旧来の「インプリシットフロー」は推奨されません。
  • 厳格なトークン検証: IDトークンを受け取ったら、必ずサーバーサイドで署名、発行者(iss)、対象者(aud)、有効期限(exp)、nonceを検証してください。SalesforceがRPの場合は、`Auth.OidcTokenValidator` クラスの利用を強く推奨します。
  • セキュリティパラメータの徹底: `state` パラメータによるCSRF対策と、`nonce` パラメータによるリプレイ攻撃対策は常に実装してください。
  • 最小権限の原則: 接続アプリケーションのスコープは、アプリケーションが必要とする最小限の権限(例: `openid`, `email`, `profile`)のみを要求するように設定します。過剰な権限要求は避けてください。
  • シークレットの厳重な管理: 接続アプリケーションの `Consumer Secret` (Client Secret) は、パスワードと同等の機密情報です。決してクライアントサイドのコード(JavaScriptなど)に埋め込んだり、バージョン管理システムにコミットしたりしないでください。

これらの原則とベストプラクティスに従うことで、SalesforceのID機能を最大限に活用し、安全で信頼性の高い認証基盤を構築することが可能になります。

コメント