6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SalesforceAdvent Calendar 2023

Day 22

ヘッドレスID APIを使ってみよう(ユーザ登録編)

Last updated at Posted at 2023-12-21

はじめに

Summer'23で、ヘッドレスID APIがリリースされました。( Summer'23リリースノート )

これまで、Salesforceに外部ユーザを登録しようとする場合には、Experience Cloudサイトからユーザ登録する必要がありましたが、このAPIを使用することで、外部アプリケーションからユーザ登録/ログイン/パスワードリセットが可能になります。

本記事は、ヘッドレスID APIを利用して外部ユーザを新規登録するまでの設定と流れについて、公式ドキュメントを補足するものになります。
ヘッドレスID APIを試す際の一助になれば幸いです。

公式ドキュメントはこちらです

ヘッドレスID APIを使うまでの設定について

以下が主な設定作業になります。公式ドキュメントでは、このページが該当します。

  • プロファイル, ロール
  • Experience Cloudサイト
  • 接続アプリケーション(外部ユーザ用と、管理者用)
  • 組織の設定

公式ドキュメントのガイドに従って設定すれば良いですが、今回は「Experience Cloudサイト」と「接続アプリケーション」の設定について補足します。

Experience Cloudサイトの作成

ヘッドレスID APIを使ったユーザ登録は、主にログイン&登録メニュー > ヘッドレスID設定セクションで、ポチポチ設定します。
ヘッドレス登録 API を介したセルフ登録の許可にチェックを入れると、その他の設定が表示されます。
本記事では、ヘッドレスID APIを使ったユーザ登録でreCAPTCHAを使用しませんので、このAPIへのアクセスに認証を要求にチェックを入れます。
reCAPTCHAを使用する場合には、このチェックは不要です。代わりにreCAPTCHAオプションを設定ください。

ここで、登録ハンドラーは必須となりますので、事前に作成しておいてください。登録ハンドラーのサンプルは後ほど紹介します。

screenshot_003.png

登録ハンドラー(Apex)の作成

HeadlessSelfRegistrationHandlerインタフェースを実装したApexクラスを用意します。
このApexクラス内でユーザオブジェクトの生成や、取引先/取引先責任者の生成を実行します。ただし、ユーザオブジェクトはInsertせずに返すようにしてください。(Insertして返すとエラーになります)
自前で実装することもあり、取引先と取引先責任者の生成には組織固有のロジックを組み込めます。

/**
 * ヘッドレス 登録ハンドラー のサンプル
 */
public with sharing class CreateSiteUserHandler implements Auth.HeadlessSelfRegistrationHandler {
  private final Long CURRENT_TIME = Datetime.now().getTime();

  public CreateSiteUserHandler() {}

  /**
   * ユーザ生成処理
   * ※insertしない状態でUserオブジェクトを返す
   */
  public User createUser(Id profileId, Auth.UserData data, String customUserDataMap, String experienceId, String password) {

    // Userフィールドに値をセットして、Userオブジェクトを生成する
    User u = new User(
      ProfileId = profileId
    );

    // (追加データ用)
    CustomUserData customData = (CustomUserData)JSON.deserialize(customUserDataMap, CustomUserData.class);

    Map<Schema.SObjectField, String> registrationAttributes = new Map<Schema.SObjectField, String>{
      Schema.User.FirstName => data.firstname,
      Schema.User.LastName => data.lastname,
      Schema.User.Email => data.email,
      Schema.User.Username => data.username,
      Schema.User.MobilePhone => customData.mobile_phone
    };
    for (Schema.SObjectField field : registrationAttributes.keySet()) {
      String value = registrationAttributes.get(field);
      u.put(field, value);
    }

    //
    if (String.isBlank(u.Username)) {
      u.Username = u.Email;
    }
    // 別名とかニックネームを適当に生成しているだけです.本来はもう少しロジックを入れるべきかな、とは思います
    if (String.isBlank(u.Alias)) {
      u.Alias = generateAlias();
    }
    if (String.isBlank(u.CommunityNickname)) {
      u.CommunityNickname = generateCommunityNickname();
    }

    Account acc = createAccount(u);
    Contact c = createContact(u, acc.Id);
    u.ContactId = c.Id;

    u.languagelocalekey = UserInfo.getLanguage();
    u.localesidkey = UserInfo.getLocale();
    u.emailEncodingKey = 'UTF-8';
    u.timeZoneSidKey = UserInfo.getTimezone().getID();

    // (注) ユーザはInsertせずに、返却してください.
    return u;
  }


  private Account createAccount(User u) {
    Account acc = new Account(
      Name = u.LastName + ' ' + u.FirstName
    );
    insert acc;
    return acc;
  }

  private Contact createContact(User u, Id accountId) {
    Contact c = new Contact(
      AccountId = accountId,
      LastName = u.LastName,
      FirstName = u.FirstName,
      Email = u.Email
    );
    insert c;
    return c;
  }

  private String generateAlias() {
    String timeString = String.valueOf(CURRENT_TIME);
    return timeString.substring(timeString.length() - 8);
  }

  private String generateCommunityNickname() {
    // ニックネームは組織内でユニークであることが必要
    // ユニークとなるよう、何かしら工夫してください
    return 'SiteUser/' + CURRENT_TIME;
  }


  /**
   * (カスタム)ユーザデータ
   *  - ヘッドレスID APIから受け取ったデータをデシリアライズするためのApexクラスになります.
   *  - 必要に応じて定義すればokです.
   */
  private class CustomUserData {
    public String mobile_phone {get; private set;}

  }
}

メールテンプレート

ヘッドレスID APIを使ったユーザ登録では、検証用のワンタイムパスワードをメールで通知することが可能です。
その際のメールテンプレートは、メールメニュー > メールテンプレートセクションで設定します。
ちなみに、ユーザ登録時のワンタイムパスワードを通知するメールテンプレートはヘッドレス登録のワンタイムパスワードで設定します。

screenshot_006.png

接続アプリケーションの作成(管理者用)

3rdパーティのアプリケーションから、Salesforceにユーザ登録する際には、ユーザ登録を実行できるトークンが必要となります。(Experience Cloudの設定でこのAPIへのアクセスに認証を要求を有効にしているので)
そのため、外部ユーザが使用するものとは別に管理者用として接続アプリケーションが必要になります。

この場合の接続アプリケーションの作成方法としては、以下の記事が参考になると思います。

ここで1点注意が必要になります。OAuth範囲として、以下を追加ください。

  • ヘッドレス登録APIにアクセス (user_registration_api)
  • パスワードを忘れた場合のヘッドレスAPIにアクセス (forgot_password)

screenshot_001.png

※「パスワードを忘れた場合のヘッドレスAPIにアクセス」はユーザ登録時には不要ですが、パスワード忘れ時の対応として必要になります。

接続アプリケーションの作成(外部ユーザ用)

ヘッドレスID APIを使うための接続アプリケーションを用意します。
接続アプリケーションの作成ではOAuth範囲として、以下は追加しておいてください。

  • ヘッドレス登録APIにアクセス (user_registration_api)
  • パスワードを忘れた場合のヘッドレスAPIにアクセス (forgot_password)

また、認証コードおよびログイン情報フローを有効化と、認証コードおよびログイン情報フローの POST 本文でユーザーログイン情報を要求にチェックを入れておきます。
その他、今回は非公開クライアントによる実装ですので、Webサーバーフローの秘密が必要, 更新トークンフローの秘密が必要 にもチェックを入れます。

PKCEについてはデフォルトでチェックが入った状態になります。ログインフローでPKCEを使う場合にはチェックを入れたままにしてください。(ここでは外してます)

IDトークンを要求ではIDトークンの要否に合わせてチェックを入れてください。IDトークンを含める場合は、OAuth範囲に一意のユーザー識別子にアクセスも追加ください。

screnshot_005.png

ヘッドレスID APIを使ってみる

(ようやく本題です)

ドキュメントでは「公開クライアント」「非公開クライアント」という2つの言葉が出てきます。
「公開クライアント」とは機密情報を保持できないアプリケーションを指します。
例えば、SPAがこれにあたります。
機密情報(クライアントシークレットなど)をクライアント側で使用すると、誰でも参照できてしまう(≒公開)のでNGですよね。

逆に、「非公開クライアント」とは機密情報を(比較的安全に)保持できるアプリケーションを指します。
例えば、クライアント/サーバ構成のWebアプリケーションがこれにあたります。

お使いのシステム構成に従って、参照するドキュメントを選んでください。本記事では、非公開クライアントの場合を取り上げます。
公式ドキュメントは、こちらになります。

登録フロー(概要)

非公開クライアントのヘッドレス登録フローより引用)

ff8b0652b231b3ec66869a7229aeb6ef.png

この中で、(4), (8), (11) についてもう少し掘り下げます。

実装ポイント1: ヘッドレス登録APIへのリクエスト(4)

フローの「3」で、ユーザはクライアントで用意したフォームにメールアドレスやユーザ名などの情報を入力します。
この入力内容を、Experience Cloudサイトのヘッドレス登録エンドポイントにPOSTする、という具合になります。

エンドポイント

/services/auth/headless/init/registration

HTTPメソッド

POST

ヘッダ

キー 備考
Authorization Bearer (トークン) Experience Cloudの[ログイン&登録]ページで、このAPIへのアクセスに認証を要求を有効にした場合は必須です。管理者用の接続アプリケーション経由で取得したアクセストークンを使います。
※公式からは、非公開クライアントの場合、有効にすることを強く推奨しています
Content-Type application/json 固定値です
Auth-Verification-Type email or sms ユーザの検証に使用する方法としてEmail or SMS のいずれかを指定します。emailの場合はユーザのEmailに検証用の通知が届きます。
Accept-Language ja 通知内容を日本語にする場合、指定が必要です。(※)

※クライアント(ブラウザ側)から、上記のエンドポイントにPOSTする場合、Accept-Languageは明示的に指定しなくても自動で付与されると思います。
もし、クライアント -> (何らかのサーバアプリケーション) -> (ヘッドレス登録API) という具合に、間に挟まるものがある場合にはAccept-Languageの指定が必要になります。

リクエストボディ

キー 備考
password (フォームで入力した値) 必須です
userdata (姓,名,Email,ユーザ名) 必須です
customdata (そのほか追加情報) 任意です

リクエストボディのサンプルです。

{
  "userdata": {
    "firstName": "Taro",
    "lastName": "TAO",
    "email": "test_email@example.com",
    "username": "test_username@example.com"
  },
  "customdata": {
    "xxx": "xxx"
  },
  "password": "********"
}

customdata は、ヘッドレス登録用ApexハンドラでJSON形式の文字列として受け取ります。
ドキュメントでは、ヘッドレス登録用Apexハンドラ内で使用する変数を定義したApexクラスを用意して、そのApexクラスでデシリアライズしましょう、とありましたし、実際そうすべきだと思います。
例えば、紐つけしたい取引先責任者(or取引先)のキー情報などがあれば、それを定義しておいて、Apexハンドラ内で使用すると良いでしょう。

CustoUserData customData = (CustomUserData)JSON.deserialize(customUserDataMap, CustomUserData.class);
// contact_keyで該当のレコードをクエリする、など
Contact c = [Select Name From Contact Where key__c = :customData.contact_key];

...

private class CustomUserData {
  public String contact_key {get; private set;}
}

レスポンス

リクエストが成功すると、以下のようなレスポンスを受け取ります

{
  "status": "success",
  "email": "test_email@example.com",
  "identifier": "xxxxxxxxxxxxxxxx"
}

このidentifierは、後続処理で使用します。
また、ユーザにはメールが送信されます。このメールにワンタイムパスワード(OTP1)が記載されています。

クライアント側では、このOTPを入力するためのフォームを用意して、ユーザに入力を促してください。(フローの(6), (7)にあたります)

実装ポイント1(+α): ユーザーが検証フォームに OTP を入力する(6)

自前のサイトで、OTPを入力させるための画面をご用意ください。入力したOTPを後続の処理で使用します。

実装ポイント2: 認証コードとログイン情報フローを初期化(8)

ユーザが入力したOTPと、identifierを使って、ヘッドレスログインAPIを使ったログイン処理を開始します。

エンドポイント

/services/oauth2/authorize

HTTPメソッド

POST

ヘッダ

キー 備考
Auth-Request-Type user-registration 固定値です。
Authorization Basic (要求識別子) ※1
Content-Type application/x-www-form-urlencoded 固定値です
Auth-Verification-Type email or sms ユーザの検証に使用する方法としてEmail or SMS のいずれかを指定します。emailの場合はユーザのEmailに検証用の通知が届きます。
ヘッドレス登録APIへのリクエストで使用したものと同じ値を指定ください。

※1
identifierと、OTPの文字列を : で連結して、Base64でエンコードした文字列を指定します。
例えばjsで記述するとこんな感じです。

const user_request = `${identifier}:${otp}`;
const base64_encoded = Buffer.from(user_request).toString('base64');
...
const headers = {
  'Auth-Request-Type': 'user-registration',
  'Auth-Verification-Type': 'email',
  'Content-Type': 'application/x-www-form-urlencoded',
  'Authorization': `Basic ${base64_encoded}`,
};

リクエストボディ

キー 備考
client_id (外部ユーザ用 接続アプリケーションのクライアントID) 必須です
response_type code_credentials 固定値です
redirect_uri (認証に成功した後のリダイレクト先URL) 接続アプリケーションのコールバックURLに含まれるもの

認証に成功すると、Salesforceがredirect_uriに302リダイレクトします。以下のサンプルのようにクエリパラメータをつけてリダイレクトします。
リダイレクト先で、クエリパラメータcodeを取得してください。

(サンプル)
(リダイレクト先URL)?code=aPrxC1xxxxx&sfdc_community_url=https%3A%2F%2FMyExperienceCloudSiteName.my.site.com&sfdc_community_id=0DBxxxxxxxxxxx

実装ポイント3: 認証コードを使ってアクセストークンをリクエスト(11)

上記の「リダイレクト先」での処理になります。クエリパラメータに含まれているcodeを抽出して、アクセストークンをリクエストします。

エンドポイント

/services/oauth2/token

HTTPメソッド

POST

ヘッダ

キー 備考
Content-Type application/x-www-form-urlencoded 固定値です

リクエストボディ

キー 備考
client_id (外部ユーザ用 接続アプリケーションのクライアントID) 必須です
client_secret 外部ユーザ用 接続アプリケーションのクライアントシークレット) 必須です
code (抽出したcode) 必須です
grant_type authorization_code 固定値です
redirect_uri (認証に成功した後のリダイレクト先URL) 接続アプリケーションのコールバックURLに含まれるもの

レスポンス

Salesforce側で認証コードの検証が成功すると、以下のようなレスポンスが返ってきます。
以降は、この中に含まれるaccess_tokenを使って、salesforceのAPIにリクエストを投げてください。

{
  "access_token":"*******************",
  "sfdc_community_url":"https://MyExperienceCloudSiteName.my.site.com",
  "sfdc_community_id":"0DBxxxxxxxxxxxx",
  "signature":"ts6wm/svX3jXlCGR4uu+SbA04M6qhD1SAgVTEwZ59P4=",
  "scope":"openid api",
  "id_token":"XXXXXX",
  "instance_url":"https://yourInstance.salesforce.com",
  "id":"https://yourInstance.salesforce.com/id/00Dxxxxxxxxxxxx/005xxxxxxxxxxxx",
  "token_type":"Bearer",
  "issued_at":"1667600739962"
}

さいごに

思ったより長くなってしまいました。すみません。最後まで読んで頂いた方には感謝です。

ヘッドレスID APIを使うと、Experience Cloudサイトを使わずにユーザ登録機能やログイン機能を提供できるので、UIの自由度が上がります。(逆にUIを実装する必要が出てきますが)
UIを細かく調整したい場合には、ヘッドレスID APIを使う、という選択肢も検討に入れてみてください。

本記事はSalesforce Advent Calendar 2023の22日目の記事になります。

  1. OTPの有効期限についてドキュメントで見つけられず分かりません。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?