【必読】なぜ今、Aura ではなく LWR を選ぶべきなのか?

React×BFFで実現するExperience Cloud Headless IDログイン:実装手順と3つの壁

A conceptual architectural visualization of a modern headless web authentication system. In the foreground, a sleek, translucent glass pane displays a minimalist, glowing app interface. From this pane, intricate lines of golden light stream through a central glowing crystal prism—the backend proxy—before connecting to a massive, intricate digital cloud structure in the background. The scene is set in a dark, atmospheric cyberspace with floating holographic fragments of code and encryption keys. The lighting is dominated by shades of deep blue and vibrant cyan, with a cinematic depth of field. The image conveys a sense of high-speed data flow, security, and the seamless decoupling of frontend and backend technologies. 基礎・アーキテクチャ

この記事はバージョン Summer ’26 において執筆しています。
現在の動作と異なる場合がありますので、ご認識おきください。

SalesforceのHeadless 360やHeadless IDの概念は、Experience Cloudの標準UIに依存せず、任意のフロントエンドからAPI経由で認証・データ連携を行うための強力なアーキテクチャです。

しかし、概念を理解したフロントエンドエンジニアがReactなどのSPA(Single Page Application)から直接APIを呼び出そうとすると、CORS(Cross-Origin Resource Sharing)のプレフライトエラーや、PKCE(Proof Key for Code Exchange)特有のエンコード仕様といった技術的な壁に直面し、ログインフローを確立するだけで多大な時間を消費することが少なくありません。

本記事では、外部ホスティング環境(Xserver上のReactを想定)からSalesforceの外部ユーザーとして認証を完了させるための、具体的な実装手順とトラブルシューティングを解説します。

DXforce Point for Developers
LWRって何?どんなメリットがあるの?
そんな疑問を解決するにはまずは以下の記事をご覧ください。
Salesforce LWRとは? Experience Cloudの次世代ランタイムを徹底解説

1. 実装アーキテクチャの全体像と処理フロー

実装に入る前に、認証フローの全体像を整理します。ここで重要なのは、公式ドキュメントで定義されている仕様(事実)と、それを現実のWebアプリケーションに落とし込む際の構成(見解)を切り分けて理解することです。

事実:認証コードとログイン情報フロー

SalesforceのHeadless IDログインは、「認証コードとログイン情報フロー(Authorization Code and Credentials Flow)」を採用しています。これは、クライアントがユーザーの認証情報(ID/パスワード)とPKCEパラメーターを認可エンドポイントに送信して「認可コード」を取得し、それをトークンエンドポイントで「アクセストークン」に交換する2段階の仕様です。

見解:BFF(Backend For Frontend)の必要性

公式仕様上はクライアント(ブラウザ)から直接エンドポイントを呼び出すことも想定されていますが、実際のSPA開発においては、ブラウザからのPOSTリクエスト時に発生するOPTIONSリクエスト(プレフライト)をSalesforceの認可エンドポイントがサポートしていないため、CORSエラーで通信がブロックされます。

この壁を越え、かつアクセストークンをブラウザ側に露出させないセキュアな構成をとるため、本記事ではフロントエンドと同じドメインにBFF(今回は軽量なPHPプロキシ)を配置し、サーバー間通信でSalesforceとやり取りするアーキテクチャを採用します。

以下のシーケンス図は、今回実装する処理フローを示しています。

2. 外部サイトと連携する具体的な6つのステップ

ここからは、実際に環境を構築するための6つのステップを解説します。
前半のSalesforce側設定は公式ドキュメントに基づく事実であり、後半の実装は上述のアーキテクチャに基づく構成サンプル(見解)です。

ステップ1: Salesforce側の初期設定(プロファイルとLWRサイト)

まずはAPIゲートウェイとなるサイトと、テストユーザーを準備します。

  1. デジタルエクスペリエンスを有効化し、LWRサイトを作成・有効化します(※このサイトをゲスト公開設定にする必要はありません)。
  2. 検証用の取引先責任者と、それに紐づくカスタマーポータルユーザー(Customer Community Plus User等)を作成します。
  3. 組織の「OAuth および OpenID Connect 設定」で、「認証コードおよびログイン情報フローを許可」を有効にします。

ステップ2: 外部クライアントアプリケーションの設定

フロントエンドからのAPIリクエストを受け付ける関所を設定します。

  1. 「外部クライアントアプリケーションマネージャー」から新規アプリを作成します。
  2. OAuth設定を有効にし、以下のスコープを付与します。
    • API を使用してユーザーデータを管理 (api)
    • Web ブラウザーを使用してユーザーデータを管理 (web)
    • いつでも要求を実行 (refresh_token, offline_access)
  3. 以下にチェックを入れます。
    • 認証コードおよびログイン情報フローを有効化
    • サポートされる認証フローに Proof Key for Code Exchange (PKCE) 拡張を要求
  4. 「Web サーバーフローの秘密の要求」と「更新トークンフローの秘密が必要」のチェックは外します。
  5. 発行されたコンシューマー鍵(Client ID)を控えます。

ステップ3: ヘッドレスID設定とApex実装

送信されたユーザー名がシステムに存在するかを判定するロジックを組み込みます。

  1. Auth.HeadlessUserDiscoveryHandler インターフェースを実装したApexクラスを作成し、SOQLでユーザーを特定する処理を記述します。
  2. Experience Cloudワークスペースの「ログイン & 登録」画面にある「ヘッドレスユーザー名-パスワードログイン」にて、作成したApexクラスをハンドラーとして指定します。
  3. Bot攻撃を防ぐため、同画面でGoogle reCAPTCHA v3の秘密鍵を設定し、認証フローに対するreCAPTCHA要件を有効化します。

▼サンプルApexクラス

/**
 * @description ヘッドレスパスワードレスログイン時にユーザーを特定するためのハンドラー実装です。
 * API Version: 67.0
 */
global class SimpleHeadlessUserDiscoveryHandler implements Auth.HeadlessUserDiscoveryHandler {

    /**
     * @description クライアントから送信された識別子(メールアドレス等)を基に該当する有効なSalesforceユーザーを検索します。
     * @param networkId リクエスト送信元のExperience CloudサイトID
     * @param loginHint クライアント側で入力されたユーザー識別子(メールアドレスや電話番号等)
     * @param verificationAction 認証方法(EMAIL または SMS)
     * @param customDataJson リクエストに含まれるカスタムJSONデータ
     * @param requestAttributes IPアドレスやユーザーエージェントなどのリクエストメタデータ
     * @return 検出結果(ユーザーIDのセット、またはエラーメッセージ)
     */
    global Auth.HeadlessUserDiscoveryResponse discoverUserFromLoginHint(
        Id networkId, 
        String loginHint, 
        Auth.VerificationAction verificationAction, 
        String customDataJson, 
        Map<String, String> requestAttributes
    ) {
        Set<Id> userIds = new Set<Id>();
        String errorMessage = 'ユーザーが見つからないか、有効なアカウントではありません。';

        // 入力値が空の場合はSOQLを発行せずに即時エラーメッセージを返却します
        if (String.isBlank(loginHint)) {
            return new Auth.HeadlessUserDiscoveryResponse(userIds, '識別子(メールアドレス等)が入力されていません。');
        }

        try {
            // パスワードレス認証ではユーザー名だけでなくメールアドレスで入力されるケースが多いため、
            // EmailとUsernameの両方を条件に含めてユーザーの検索漏れを防ぎます。
            // 重複チェックのためにLIMIT 2で取得します。
            List<User> users = [
                SELECT Id 
                FROM User 
                WHERE (Username = :loginHint OR Email = :loginHint) 
                  AND IsActive = true 
                LIMIT 2
            ];

            // 単一の有効なユーザーが一意に特定できた場合のみ成功レスポンスを組み立てます
            if (users.size() == 1) {
                userIds.add(users[0].Id);
                errorMessage = null; // 成功時はエラーメッセージをnullに指定します
            } else if (users.size() > 1) {
                // 同一識別子のユーザーが複数存在する場合は一意に特定できないため、セキュリティ保護のためエラーにします
                errorMessage = '該当するユーザーが一意に特定できませんでした。管理者にお問い合わせください。';
            }

        } catch (Exception e) {
            // 予期せぬ例外発生時はログを出力しつつ、フロントエンドへは汎用メッセージを返却します
            System.debug(LoggingLevel.ERROR, 'User Discovery Error: ' + e.getMessage());
        }

        // 公式仕様のコンストラクタ (Set<Id>, String) に従ってレスポンスを返却します
        return new Auth.HeadlessUserDiscoveryResponse(userIds, errorMessage);
    }
}

ステップ4: Postmanによる認証APIの疎通確認

Reactコードを書く前に、Postmanを使用してエンドポイントの疎通を確認します。まずは認可エンドポイント(/services/oauth2/authorize)に対して、以下の要領でPOSTリクエストを投げます。

  • 認可: 認可タイプ: Basic 認証 [Base64(ユーザー名:パスワード)]
  • ヘッダーAuth-Request-Type: Named-User
  • ボディ (x-www-form-urlencoded)
    • response_type: code_credentials
    • client_id: ステップ2で取得したコンシューマー鍵
    • redirect_url: https://[YOUR_POSTMAN_ID].postman.co/callback
    • code_challenge: {{code_challenge}}
    • recaptcha:  (※reCAPTCHAトークンはテストサイト等で手動生成して指定)
  • スクリプト: 下記参照

ここでレスポンスが 302 Found となり、Location ヘッダーに ?code=xxx が返ってくれば第一関門突破です。

  • 認可
  • ヘッダー
  • ボディ
  • スクリプト
Experience Cloud サイトへのログインID・パスワード
// PKCE用のベリファイアとチャレンジを動的に生成します。
function base64URLEncode(wordArray) {
    var base64 = CryptoJS.enc.Base64.stringify(wordArray);
    return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

// Generate a random 32-byte verifier using CryptoJS
var randomWords = CryptoJS.lib.WordArray.random(32);
var verifier = base64URLEncode(randomWords);
pm.environment.set("code_verifier", verifier);

// Generate the SHA-256 challenge from the verifier
var challenge = base64URLEncode(CryptoJS.SHA256(verifier));
pm.environment.set("code_challenge", challenge);

続いて、取得した code を用いてトークンエンドポイント(/services/oauth2/token)へリクエストし、正常に access_token が返却されることを確認してください。

ステップ5: XserverへのBFF(proxy.php)配置

ここからは実装レイヤーです。CORSを回避するため、Xserver上にBFFとして機能する proxy.php を配置しました。 このBFFは単なる中継ではなく、以下の3つの役割(アクション)を担う万能なルーターとして実装します。

  1. 認可(authorize): Reactからの要求を受け、cURLでSalesforceへサーバー間通信を行います。自動リダイレクトを停止させた上で、302 Found レスポンスをフックし、Location ヘッダーから code を抽出してReactへJSONで返します。
  2. トークン交換(token): 取得した code をアクセストークンに交換する際も、CORS回避のためにこのプロキシを経由させます。
  3. API連携(api): ログイン後にSalesforceの各種APIを叩く際、アクセストークンを Authorization: Bearer ヘッダーに安全に付与して通信を中継します。

ステップ6: Reactアプリの実装と検証

React側では以下の処理を実装します。

  1. ログインボタン押下時に、window.crypto.subtle を用いてPKCEの code_verifier と code_challenge を生成します。
  2. Google reCAPTCHA v3のAPIを呼び出し、最新のワンタイムトークンを取得します。
  3. ID、パスワード、チャレンジ、reCAPTCHAトークンをまとめ、同一ドメインのBFF(proxy.php)へ送信します。
  4. BFFから認証成功(codeの取得)とトークン交換完了のレスポンスを受け取ります。アクセストークン取得時のレスポンスには id というIdentity URLが含まれています。このURLに対し、BFF経由でGETリクエストを送ることで、ログインユーザーの姓名やユーザー名などのプロファイル情報をシームレスに取得・描画できれば検証完了です。

3. 実装時に直面する3つの壁と解決策

概要の手順通りに進めても、細かな仕様の罠にはまることがあります。ここでは、筆者が実際に直面した3つの壁と、その技術的な解決策を共有します。

壁1: CORSプレフライトと自動リダイレクトの制御

  • 事象: ブラウザの fetch APIでSalesforceの認可エンドポイントに直接Basic認証ヘッダーを送ると、プレフライトリクエストが拒否されます。また、302リダイレクトが発生した際、JavaScript側でリダイレクト先のURLパラメーター(認可コード)を透過的に取得することが困難です。
  • 解決策: ステップ5で述べたBFFパターンの採用です。PHPのcURLオプションで CURLOPT_FOLLOWLOCATION => false を指定し、自動リダイレクトを停止させた上で、レスポンスヘッダー群から正規表現等を用いて Location ヘッダーの値を自前で抽出します。

壁2: invalid code verifier エラー(S256の指定漏れ)

  • 事象: 認可コードは取得できたものの、トークンエンドポイント(/token)にリクエストした際に invalid code verifier エラーで弾かれるケースです。
  • 解決策: 認可リクエスト(/authorize)を送信する際、Bodyパラメーターに code_challenge_method=S256 を明示的に含める必要があります。これを省略すると、Salesforce側は送られてきた code_challenge がハッシュ化されていないプレーンテキストであると誤認し、後の突き合わせで検証に失敗します。

壁3: 言語間のURLエンコード解釈の不一致(RFC3986)

  • 事象: S256を指定してもなお invalid code verifier が発生することがあります。
  • 解決策: PKCEの乱数生成アルゴリズムに依存する問題です。PKCEの仕様では ~(チルダ)などの記号が許可されていますが、PHPの http_build_query 等を通す際に、特定の記号が意図せず %7E のようにURLエンコードされることがあります。 Salesforce側でハッシュ計算を行う際、このエンコードされた状態の文字列と元の文字列で不一致が起こります。 これを防ぐには、BFF側でのエンコード処理を厳格に RFC3986 仕様へ統一することに加え、フロントエンド側の自衛策として、window.crypto でPKCEの乱数を生成する際の文字セットから ~ や . などの記号をあらかじめ除外し、A-Za-z0-9-_ だけを使用するよう実装するのが最も安全で確実なアプローチです。

4. 完全なヘッドレスIDの実現に向けて

本記事で解説した「ユーザー名とパスワードを使用したヘッドレスログイン」は、Headless IDアーキテクチャの強力さを示す第一歩(基礎)に過ぎません。

実際のカスタマー向けWebサービスにおいては、ユーザー自身によるアカウント作成やパスワード忘れの対応が不可欠です。SalesforceのヘッドレスID APIは、これらのライフサイクルイベントもカバーしております。
したがって本ブログの今後の課題として以下機能の執筆を視野に入れています。

  • ヘッドレス登録(Headless Registration): 外部フォームからのユーザー作成。
  • ヘッドレスパスワードリセット: 外部UIでのパスワードリセットフローの完結。
  • パスワードなしのヘッドレスログイン(Passwordless Login): ワンタイムパスワード(OTP)を利用した、よりシームレスでモダンな認証体験の提供。

ID・パスワードによる認証だけでなく、これらすべてのアイデンティティ管理機能をExperience CloudのUIを介さずに外部システムで統合して初めて、「完全なHeadless Identityの実現」と呼べるでしょう。

まとめ

Experience CloudをAPIゲートウェイとして割り切り、BFF層を間に挟むことで、SPAからのシームレスなSalesforce認証フローを実現できます。公式仕様の理解と、PKCEやCORSといったWeb標準の仕様を正しくつなぎ合わせることが、次世代アーキテクチャ構築の鍵となります。本記事が、実装に立ち向かう開発者の一助となれば幸いです。

参考URL

What Is Headless Identity? | Headless Identity Implementation Guide

ヘッドレス ID の前提条件の完了

ヘッドレス ID API: 認証コードとログイン情報フローの外部クライアントアプリケーションの設定

DXforceの管理人

福島 瑛二

2013年にJavaエンジニアとしてのキャリアをスタート。2019年にSalesforceと出会い、Salesforceエンジニアの道へ。

デザインや UI/UX の観点からもシステムを捉え、ユーザーにとって心地よい体験を実装することにやりがいを感じています。

CRM(顧客データ)や Data Cloud と連携した高度なサイトを目に見える形で表現できる Experience Cloud に大きな可能性を見出しており、バックエンドのデータ構造とフロントエンドの表現力を極めることがこれからの Salesforce エンジニアに求められるスキルだと確信しています。

Trailblazer: efukushima

福島 瑛二をフォローする

読者の声

タイトルとURLをコピーしました
DXforce 案内エージェント