Salesforce Developers Japan Blog

[SolutionDev] Open ID Connect を使った SSO の実現例 (gBizID 編)

Salesforce Lightning Platform (Sales Cloud / Service Cloud を含む) はシングルサインオン (以下 SSO) の連携機能を備えていて、自身が Identity Provider (以下 IdP) となることもできれば、他 IdP と連携して Lightning Platform へログインすることも可能です。この記事では、経済産業省が提供している gBizID を使って Lightning Platform / Experience Cloud (旧 Community Cloud) へログインする、Open ID Connect による SSO を実現する仕組みと、その実装ポイントについて説明します。

実現イメージ動画

*動画をご覧になるには、画像をクリックしてください。 (画像をクリックするとVidyard(動画配信ページ)へ遷移します)

対象読者

Open ID Connect での SSO について参考資料を探しているSalesforce 開発者、また、gBizID を使った SSO を実装しようとしている Salesforce 開発者を想定しています。このため、実装手順についてひとつひとつ細かく(設定画面内での操作パスや、開発者コンソールの使い方など)は説明していませんので、あらかじめご了承ください。

注意事項

この記事の内容は、2020年10月時点の仕様・情報に基づき設定・動作確認を行ったものになります。gBizID と Salesforce Lightning Platform の今後のバージョンアップ・仕様変更などにより、記事の内容と相違が出てくるかもしれないこと、あらかじめご了承ください。また、この記事の内容は、あくまでこの時点での動作検証を行った結果を共有するものであり、Salesforce が gBizID との連携を機能提供・サポートする意味では無いこと(Open ID Connect 連携機能自体はもちろんサポートします)、また、構築パートナー様におかれましては必ず動作検証を行った上でシステム構築を行っていただく必要があることをご理解ください。

なお、gBizID の開発・検証環境については、原則として連携するサービス提供アプリケーションの開発・検証時においてのみ利用可能になります。このため、Lightning Platform / Experience Cloud の Open ID Connect による SSO を実際に試してみたい場合は、他 IdP サービスや、Keycloak などのオープンソース利用をご検討ください。Open ID Connect は標準化されたプロトコルなので、本投稿の大部分は参考になるはずです。

記事の内容

1.Lightning Platform / Experience Cloud の SSO

2.前提の整理

3.実装の流れ

 a.gBizID 関連の申請手続き、アカウント作成

 b.Lightning Platform のユーザ作成、設定

 c.ハンドラーの作成

 d.認証プロバイダの定義

 e.ログイン設定の変更

 f.動作確認

 g.サービス利用可否の判定

 h.委任情報の取得

4.参考情報

1.Lightning Platform / Experience Cloud の SSO

Single Sign On – シングルサインオン – SSO についての詳しいことについては専門書・記事に譲るとして、本投稿では次のような動きについての説明をします。

  • Lightning Platform / Experience Cloud にログインしようとする
  • 別の IdP により認証を行う
  • 問題なければ、Lightning Platform / Experience Cloud にログインできる

Lightning Platform / Experience Cloud は、SAML、もしくは Open ID Connect による SSO を標準機能として提供しています。SAML や Open ID Connect に対応した ID Provider であれば、画面からのいくつかの設定と、ログイン時の処理を記述するハンドラーを開発・利用することで、簡単に SSO を実現できます。

本投稿では、gBizID を ID プロバイダー Open ID Provider (以下 OP) とし、Lightning Platform / Experience Cloud をサービス提供アプリケーション Relaying Party (以下 RP)とする構成で、実現に必要な Lightning Platform 側の実装の流れを説明します。

gBizID は、標準規格である Open ID Connect に対応した OP です。一部に固有の用語があるかもしれませんが、基本的には他の OP でも本投稿と同様の設定を行うことで、Lightning Platform / Experience Cloud との SSO を実現することが可能です。

ところで、gBizID との連携には SSO 以外にもうひとつ恩恵があります。UserInfo として渡されてくる事業者情報を RP 側で活用することで、RP 利用者が入力しなければいけない項目を少なくし、省力化・利便性の向上を提供できるようになります。加えて gBizID には“委任”の情報を管理する機能もあります。この情報の取り扱いについても後述します。

2.前提の整理

ハンドラーの実装にも影響することなので、サービス提供アプリケーション側(RP)の要件を決めておく必要があります。

  1. SSO によって RP 側のアカウントを自動的に作成する必要があるか?
  2. OP から提供される情報で、RP 側で利用できる機能・サービスを変える必要があるか?

Lightning Platform / Experience Cloud を RP とする場合、SSO ができても RP 側のアカウント作成が不要と言うことではありません。

例えば、OP 側にアカウント opA が、RP 側にアカウント rpA があったとします。RP 側には“opA は rpA です”と設定します。これによって、RP 側へのログイン時に、OP 側で opA として認証されれば、以降 RP 側の操作は、rpA の振る舞いとしてみなされることになります。RP 側でのデータアクセス制御をどうやるかなど考えてみると、確かにこうなるかなと思えるのではないでしょうか。

もしかしたら、アカウントを作成しなくても良いアーキテクチャもあるかもしれませんが Lightning Platform / Experience Cloud を利用するする場合は、前提としてご承知おきください。

さて、改めて要件 1 についてですが、この RP 側のアカウント作成を、初回 SSO 時に自動的に作成するか?あるいは RP 側のアカウント作成はあらかじめ行っておくか?です。利用者やサービス提供者の利便性を考えると自動的に作成された方が良いでしょう。ただ、弊社製品のライセンス体系(Experience Cloud はユーザ数/ログイン数単位での課金)をご存知の方であれば、好ましく無い事態になる可能性も容易に想像できるかと思います。また、実際にこの組み合わせが利用される場面を思い浮かべると、事業者が何らかの申請やサービス利用を行うとして、サービス提供アプリケーションを利用できるか(してもよいのか)は、別途判断すべきでは無いだろうか?と考え、本投稿ではアカウントの自動作成はしない前提としました。とはいえ、ハンドラーの作り方が変わるだけですので、自動作成する場合においてどうすれば良いかは、できる範囲でなるべく補記します。

次に要件 2 についてですが、gBizID の持つ“委任”に関連する話です。例を見てみましょう。

  • RP は何らかの手続き申請を受理するサービス提供アプリケーション
  • あらかじめ gBizID 側で、株式会社稲葉製作所のアカウント稲葉から、松井行政書士事務所のアカウント松井に“委任する”手続きを行っておく
  • RP にログインしたアカウント松井は、アカウント稲葉に代わって稲葉製作所の手続き申請を行える

これが、事業者間での委任です。もう一つ、事業者内での委任もあります。

  • 松井行政書士事務所には、プライム(代表)アカウントとしてアカウント松井、メンバーアカウントとしてアカウント伊藤がある
  • アカウント松井は、アカウント伊藤が操作を行える RP を選択できる

事業者内での委任については、RP 側にはあくまで“そのアカウントが使えるサービス(RP)のリスト”情報が渡されるだけなので、RP 側でログインさせる・させない、あるいはログインはさせるが機能は使えないようにするのかなどの制御が必要です。 本投稿ではこの委任についても行う前提として、実装手順・内容を説明します。

事業者名、アカウント名はもちろん架空のものです!(今回共同で検証した松井さん、伊藤さんの名前を拝借しました)

3.実装の流れ

実際の利用場面を想定して、コミュニティユーザで特定のコミュニティへの SSO を試してみます。

3-a.gBizID 関連の申請手続き、アカウント作成

詳しくは gBizID の公式サイトで公開されている開発者向けマニュアルをご参照ください。申請を行い RP とする Lightning Platform に設定する次の情報を取得します。

  • SSO 関連: Client ID、Client Secret
  • 委任関連: 対象サービスコード、Client Key、Client Token

また、検証用の gBizID アカウントを作成します。本投稿では事業者間の委任とメンバーへの委任を試すので、次の通り 3 アカウントを作成します。

  • 事業者A: プライムアカウント
  • 事業者B: プライムアカウント、メンバーアカウント

ところで、申請を行うにあたって提供しなければならない情報の一つに、リダイレクト URL があります。これは手順3-d.認証プロバイダの定義を行うと作成されるのですが、その認証プロバイダの定義には申請後に受領する情報に含まれる Client ID と Client Secret が必要です。あれ、デッドロックですね? 諦めてはいけません、どちらかをダミーとして登録し、後から修正すれば良いのです。まずは認証プロバイダの Client Key と Client Secret をダミー文字列で登録しておきリダイレクト URL を入手、申請を行った上で、後ほど提供された Client ID と Client Secret に書き換えましょう。

3-b.Lightning Platform のコミュニティの作成、ユーザ作成・設定

SSO を試すコミュニティの作成、また、SSO で紐付けられるユーザの作成と、gBizID 固有の情報を格納しておく項目追加を行います。

コミュニティの作成
この段階では特殊な設定は必要ありません。いつもと同じようにコミュニティを作成します。今回は“共通ポータル”というコミュニティを作成しました。

アカウントを作成・設定
コミュニティユーザを作成します。取引先責任者レコードを作成し、そこから“カスタマーユーザを有効化”するのが標準的な手順ですね。レコードができたら、“統合 ID”の項目に、gBizID のアカウント(メールアドレス)を設定します。

今回は“統合 ID”の項目を使いましたが、他の項目(それこそメールアドレスなど)やカスタム項目でも構いません。その場合は3-c.で実装するハンドラーの SOQL 文を適切なものに書き換えてください

ユーザオブジェクトに項目追加
今回は、認証成功時に UserInfo として渡されてくる情報を格納するカスタム項目をユーザオブジェクトに追加するとしました。アカウントの種類や事業者の種類、利用可能サービス一覧などの情報を格納する項目を追加しています。データ型は基本的にはテキストで良いのですが、mandate_info (利用できるサービス一覧)は少し長めの文字列が渡される場合もあるため、ロングテキストエリアにしています。

コミュニティユーザなので、本来であれば各種情報を取引先、取引先責任者、ユーザのどのオブジェクトに登録するのかも要件に合わせてきちんと設計・定義し、ハンドラーもそれにあわせて実装する必要があります。

UserInfo でどのような情報が渡されてくるかは、gBizID 開発者向けマニュアルを参照してください。

後になってから気がついたのですが、FederationIdentifier の項目表示目が“SAML 統合 ID”となってますね… Open ID Connect で使ってダメと言うことはないのですが少々複雑な気分です。

3-c.ハンドラー(Apex クラス)の作成

ログイン処理時に実行されるハンドラーを作成します。ハンドラーそのものの詳細については help をご参照ください。今回の前提に合わせたサンプルコードを示しますが、利用にあたっては最新の仕様の確認・必要な修正と、エラーハンドリングや要件に合わせた処理の追加をなど行ってください。

https://github.com/hinabasfdc/gBizID-Salesforce-SampleCode/blob/main/SSO_gBizIDLoginHandler.cls

/**
 * gBizID との SSO を行うための Registration Handler
 * 認証プロバイダの登録ハンドラーに設定して使用
 * https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_auth_plugin.htm
 * ---- ---- ---- ----
 * 2020/11/03 @Hiroyuki Inaba
 */
public class SSO_gBizIDLoginHandler implements Auth.RegistrationHandler {

  // User オブジェクトの定型的属性情報
  private static final String COUNTRY = 'JP';
  private static final String TIME_ZONE = 'Asia/Tokyo';
  private static final String LANGUAGE = 'ja';
  private static final String LOCALE = 'ja_JP';
  private static final String EMAIL_ENCODE = 'UTF-8';

  class RegHandlerException extends Exception {}

  /**
   * @description      : 初回 SSO 時に呼び出されるメソッド。gBizID 側のアカウントと Lightning Platform のユーザのマッピングが行われる
   * @param portalId   : 認証プロバイダのポータルに設定されたポータルの値
   * @param data       : UserInfo で渡されてくる情報
   * @return User      : 紐付けを作成するユーザレコード
   **/
  public User createUser(Id portalId, Auth.UserData data){

    //渡されてきたデータに電子メールアドレスが含まれていない場合はエラーを返す
    if(String.isBlank(data.attributeMap.get('user_email'))) throw new RegHandlerException('Cannot find attribute user_email from UserInfo Attributes.');

    //統合 ID の項目値でユーザレコードを検索し、存在する場合は渡されてきたその他属性値をユーザレコードに設定する
    List<User> userList = [SELECT Id FROM User WHERE FederationIdentifier = :data.attributeMap.get('user_email')];
    if(userList.size() == 1) {
      userList[0] = updateUserAttributes(userList[0], data);
      update userList[0];
    }else{
      // ユーザレコードが存在しない、もしくは 1 以外の場合にはエラーを返す
      throw new RegHandlerException('Cannot find User record with identifier provided from User Object. Or found duplicated user records.');
    }

    return userList[0];
  }

  /**
   * @description      : 2 回目以降のログイン時に呼びださsれるメソッド
   * @param userId     : 紐付けられたユーザレコードの ID
   * @param portalId   : 認証プロバイダのポータルに設定されたポータルの値
   * @param data       : UserInfo で渡されてくる情報
   **/
  public void updateUser(Id userId, Id portalId, Auth.UserData data){

    //ユーザレコードのオブジェクト ID でユーザレコードを検索し、存在する場合は渡されてきたその他属性値をユーザレコードに設定する
    List<User> userList = [SELECT Id FROM User WHERE Id = :userId AND IsActive = true];
    if(userList.size() == 1) {
      User u = updateUserAttributes(userList[0], data);
      update u;
    }else{
      // ユーザレコードが存在しない、もしくは 1 以外の場合にはエラーを返す
      throw new RegHandlerException('Cannot find User record with identifier provided from User Object. Or found duplicated user records.');
    }
  }


  /**
   * @description  : 内部的に使用するユーザレコードへの情報格納処理
   * @param u      : ユーザレコード
   * @param data   : UserInfo で渡されてきた情報
   * @return User  : 項目の値更新後のユーザレコード
   **/
  private static User updateUserAttributes(User u, Auth.UserData data) {

    // gBizID 固有データ
    u.gbiz_sub__c = data.identifier;
    u.gbiz_parent_id__c = data.attributeMap.get('parent_id');
    u.gbiz_account_type__c = data.attributeMap.get('account_type');
    u.gbiz_corp_type__c = data.attributeMap.get('corp_type');
    u.gbiz_corporate_number__c = data.attributeMap.get('corporate_number');
    u.gbiz_mandate_info__c = data.attributeMap.get('mandate_info');

    // ユーザデータ
    u.LastName  = data.attributeMap.get('user_last_nm');
    u.FirstName  = data.attributeMap.get('user_first_nm');
    u.Email = data.attributeMap.get('user_email');
    u.Phone = data.attributeMap.get('user_tel_no_contact');
    u.Department = data.attributeMap.get('user_department');
    u.CompanyName = data.attributeMap.get('name');
    String alias = data.attributeMap.get('user_email');
    if(alias.length() > 8) alias = alias.substring(0,8);
    u.Alias = alias;

    // 住所データ
    u.Country = COUNTRY;
    u.PostalCode = data.attributeMap.get('user_post_code');
    u.City = data.attributeMap.get('user_address1');
    u.Street = data.attributeMap.get('user_address2') + ' ' + data.attributeMap.get('user_address3');
    u.State = data.attributeMap.get('user_prefecture_name');

    // (都道府県はコードが渡されてくるので都道府県名とのマッピングテーブルをカスタムメタデータとして作成し使用)
    String code = data.attributeMap.get('user_prefecture_name');
    List<StateListJisX0401__mdt> states = [SELECT MasterLabel, Code__c FROM StateListJisX0401__mdt WHERE Code__c =:code];
    if(states.size() == 1) u.State = states[0].MasterLabel;

    // その他属性データ
    u.TimeZoneSidKey    = TIME_ZONE;
    u.LanguageLocaleKey = LANGUAGE;
    u.LocaleSidKey      = LOCALE;
    u.EmailEncodingKey  = EMAIL_ENCODE;

    return u;
  }
}

いくつかポイントとなるところを解説します。

初回ログイン(アカウントの紐付け)

  • 初回 SSO ログイン時には createUser メソッドが呼び出され、Lightning Platform / Experience Cloud 側の紐付けするアカウント(ユーザオブジェクトのレコード)を返す
  • 今回は FederationIdentifier 項目(画面での表示名は“統合 ID ”)の値と、OP 側から渡されてくる情報にあるメールアドレス(gBizID ではアカウント名として使用)が一致した場合に、そのユーザレコードを返す
  • 内部処理としては、ThirdPartyAccountLink オブジェクトにレコードが作成され、紐付けされたことが画面でも確認できる

もし、初回 SSO 成功時にユーザも作成する場合は、createUser メソッドで、既存のユーザを検索して返すのではなく、新規にユーザを作成(User u = new User() のように)して、そのレコードを返すようにします。

gBizID の場合、Auth.UserData の identifier にマッピングされてくるのは“アカウント管理番号”となり、内部的に扱う番号になります。もちろん、これを紐付け用の識別子として使っても良いのですが、gBizID 利用者側からは知る術が無いようなので、今回は attributeMap に入ってくる user_email を識別子として利用することにしました。とはいえ、後述の委任情報を扱う場合には、“アカウント管理番号”が必要になるので、どこかの項目に格納しておく必要があります。

二回目以降のログイン

  • updateUser メソッドが呼び出され、引数にユーザレコードの ID が渡されてくるので、その ID を使ってユーザレコードを特定
  • OP 側から渡されてくる情報を上書きする(本来であれば差分があった場合にのみ更新すべきかもしれない)

なお、紐付けされたアカウントについては、ユーザレコードの詳細画面から確認できます。余談ですが、“サードパーティ取引先のリンク”と言う表示名は、Account を取引先と訳してしまった誤りですね。

このコードに関しての考慮事項
今回はログイン時に、問答無用で gBizID から渡された情報でユーザレコードのデータを上書きするようにしています。もし、 RP 側で情報の修正を行ったとしても、次回ログイン時にまた gBizID 側の値で上書きされます。サービス提供アプリケーションの要件で気にすべき所ですね。要件によっては差異がある場合は上書きするかどうかを確認する画面を出すなど必要になるでしょう。

3-d.認証プロバイダの定義

続いては SSO 設定の要、認証プロバイダの定義です。とはいえ、たいして項目も多くありません。提供されてきた情報も使って gBizID 連携用の定義を作成します。

申請により提供されてくる Client ID と Client Secret の値を、それぞれコンシューマ鍵とコンシューマの秘密に入力します。その他は gBizID 開発者マニュアルに従い適切な値を入力します。登録ハンドラは先ほど作成した Apex クラス を指定します。

リダイレクト URL (画面上の表示はコールバック URL)もここで確認できます。コミュニティの各種 URL は、あらかじめコミュニティを作成しておかないと出てこないので気をつけましょう。

3-e.ログイン設定の変更

ログイン画面に gBizID でのログインを行うためのボタンを表示する設定をします。該当コミュニティの管理にある“ログイン&登録”でログインオプションとして gBizID を使えるようにチェックを入れます。

3-f.動作確認

では、gBizID での SSO を試してみます。コミュニティのページでログインを試行すると、次のようにログインフォームの下側に、ボタンが出現しています。(このログイン画面は“内部ユーザにコミュニティへの直接ログインを許可”している場合のものです)

ボタンをクリックすることで、gBizID のログイン画面に移動しました。

アカウントの種類によっては追加認証が必要です。SMS によるワンタイムパスワードの入力、あるいはアプリ操作で追加認証を行います。

無事ログイン後の画面に遷移しました。画面右上にユーザのニックネームや会社名が表示されているのが見えます。

ログインしたユーザが自らの事業体についての何らかの申請を行うのであれば、用意は整いました。RP 側は、渡されてきた UserInfo の事業者に関する各種情報を活用することで、申請者の利便性を向上させることができるでしょう。

3-g.アカウント種別に応じた利用機能の判別

gBizID では、エントリー、プライム、メンバーのアカウント種類別に RP 側でふるまい・利用できる機能の制御を変えることを求めています。UserInfo で渡されてくる account_type がその種別にあたります。今回の例では、ハンドラーでユーザオブジェクトの項目に値を格納していますので、その値を参照し処理を分けることで実現可能です。例えば、Lightning Web Component の読み込み時にこの値を取得して、ボタンの表示を変えたり、あるいはコンポーネントそのものの表示を変えたりでしょうか。

また、今回は実装していませんが、プロファイルを 3 つ用意しておき、ハンドラーで割り当てるプロファイルを変えても良いかもしれません。アクセスできるオブジェクトや Apex クラスを制御できます。

他にも、コミュニティのページごとに設定できる“利用者”の機能を使うこともできるでしょう。

3-h.委任情報の取得

さて、ここからは gBizID 特有の“委任”の情報を管理する機能の取り扱いについでです。

別事業者から委任を受けた情報の取得
今回は、委任情報を取得する API に対して http コールアウトを行う Apex クラスを作成し利用することとしました。

作成する Apex クラスも、ごく一般的な http コールアウトを行う例にならって開発すれば良いです。サンプルを示しますが、例によって必要なエラーハンドリングや追加処理をお願いします!

https://github.com/hinabasfdc/gBizID-Salesforce-SampleCode/blob/main/SSO_gBizIDLoginHandler.cls

/*
 * gBizID から委任情報を取得する処理を行う
 * ---- ---- ---- ----
 * 2020/11/3 @Hiroyuki Inaba
 */
public with sharing class GID_RemoteAccessApexController {

  // カスタムメタデータから設定値を取得する際の検索値
  private static final string DEVELOPERNAME  = 'gBizIDServiceAccount';

  /**
   * @description      : 委任情報を取得する API を呼び出し、委任された事業体の情報を受け取る
   * @return string    : status (success / error) と message (エラーの場合のメッセージ) もしくは body (受け取った委任情報の JSON)
   **/
  @AuraEnabled
  public static string getDelegation(){
    Map<String,Object> retvals = new Map<String,Object>();
    String meti_id = '';

    // ユーザオブジェクトのレコードからアカウント管理番号を取得。ユーザがない場合や管理番号が空の場合はエラーとして終了
    List<User> users = [SELECT gbiz_sub__c FROM User WHERE Id=:UserInfo.getUserId()];
    if(users.size() > 0) {
      meti_id = users[0].gbiz_sub__c;
    }else{
      retvals.put('status', 'error');
      retvals.put('message', 'no user record');
      return System.JSON.serialize(retvals);
    }

    if(String.isBlank(meti_id)) {
      retvals.put('status', 'error');
      retvals.put('message', 'no user record with valid sub.');
      return System.JSON.serialize(retvals);
    }

    // アクセスに必要な値をカスタムメタデータから取得し、リクエストボディを組み立て
    gBizIDServiceAccount__mdt meta = [SELECT ClientKey__c, ClientToken__c FROM gBizIDServiceAccount__mdt WHERE DeveloperName=:DEVELOPERNAME LIMIT 1];
    Map<String, String> m = new Map<String, String> {
      'client_key' => meta.ClientKey__c,
      'client_token' => meta.ClientToken__c,
      'meti_id' => meti_id
    };

    HttpRequest req = new HttpRequest();
    req.setMethod('POST');
    // API の URL は指定ログイン情報の設定を取得
    req.setEndpoint('callout:gbizid_delegation_request');
    req.setHeader('Content-Type', 'application/json');
    req.setBody(JSON.serialize(m));

    try{
      HttpResponse res = new Http().send(req);
      if(res.getStatusCode() == 200) {

        // 内容のチェックを Apex 側で行う場合、JSON を分解して値にアクセスできるようにする
        Map<String,Object> objDelegation = (Map<String,Object>) System.JSON.deserializeUntyped(res.getBody());
        List<Object> arrayDelegations = (List<Object>) objDelegation.get('delegation_info');
        if(arrayDelegations.size() > 0) {
          for(Integer i = 0; i < arrayDelegations.size(); i++) {
            Map<String,Object> objCompanyInfo = (Map<String,Object>) arrayDelegations[i];

            // 今回はログに出力するだけだが、何らかのチェック処理を入れても良いだろう
            System.debug(objCompanyInfo.get('system_cd'));
            System.debug(objCompanyInfo.get('delegation_start'));
            System.debug(objCompanyInfo.get('delegation_end'));
          }
        }

        retvals.put('status', 'success');
        retvals.put('body', objDelegation);
        return System.JSON.serialize(retvals);
      }else{
        retvals.put('status', 'error');
        retvals.put('message', 'http request status: ' + res.getStatusCode() + ' ' + res.getStatus());
        return System.JSON.serialize(retvals);
      }
    }catch(Exception e) {
      retvals.put('status', 'error');
      retvals.put('message', e.getMessage());
      return System.JSON.serialize(retvals);
    }
  }
}

次の情報は、Lightning Platform の機能を使って定義し、Apex クラスにはハードコーディングせず呼び出して値を取得するようにしています。

  • API の URL: 指定ログイン情報
  • 認証で使う Client Key と Client Token: カスタムメタデータ

今回 API のリクエストの認証は直接コード内で書いているので、この場合は API の URL もカスタムメタデータを使っても良かったかもしれません。その場合はリモートサイトの設定を忘れずに。

Lightning Web Component などからこの Apex クラスの getDelegation 関数を呼び出すことで、操作実行ユーザの gBizID アカウントに委任されている事業者の情報を JSON 形式で取得できます。事業者ごとに情報を整理して、委任を受けているどの事業者の情報を扱うか、選択するわかりやすい UI を開発する必要がありますね。

また、今回は取得できたデータをそのまま返していますが、実際にその情報を利用しても良いのかどうか、渡されてくる情報を元に RP 側のアプリケーションで判別する必要もありそうです。

  • system_cd: どの RP に対して委任されたものか
  • delegation_start と delegation_end: 委任の開始日と終了日

エラーの扱いについて、このサンプルコードでは LWC から呼び出されることを想定して JSON 形式の返り値の中に状態とメッセージを埋め込んでみています。もちろん Exception を投げるべき処理エラーもあるかもしれませんので、このクラスやメソッドの使われ方に応じて適切なコーディングをするようにしましょう。

 

メンバーへの委任情報の取得と制御
もうひとつ、プライムアカウントがメンバーアカウントに対して、どのサービス(RP)を利用できるのか設定した委任情報の扱いについて解説します。この情報は、委任情報取得 API ではなく、UserInfo の mandate_info に含まれています。ハンドラーで mandate_info の値を取得し、カスタム項目に格納した文字列の例を見てみましょう。(API の返り値は JSON 形式)

[{client_id=salesforceapplication01},{client_id=salesforceapplication02}]

このように利用できる RP の Client ID が列挙されています。3-g.で示した内容と同じように、この値をみてユーザが利用できる機能を制御する実装が、RP 側のアプリケーションには求められます。プライムアカウントの場合は、列挙される数も多くなってしまうのですが、メンバーの場合にはアクセスが許可された Client ID だけが列挙されるので、全体を文字列として contains メソッドや正規表現などで値が含まれるかを判定しても良いかもしれませんし、いったん値のみを配列化して、indexOf で該当するかを判定する方がスマートでしょうか。ハンドラーでこの判定処理を実装、ユーザオブジェクトに別途定義するチェックボックス項目へ判定結果を入れておけば、様々な場面で容易に活用できそうです。

なお、このメンバーへの委任の設定は、gBizID 側で変更できるものの、RP 側への反映は今回のような実装方式だとあくまでログイン操作時になります。つまり、プライムアカウントがメンバーアカウントから ある RP の利用可能権限の剥奪操作をしたとしても、メンバーアカウントがすでにその RP にログイン済みだった場合は、ログアウトするまではメンバーアカウントは引き続きその RP での操作が許可されたままになります。システム間連携を行うにあたっては通常許容される範囲の仕様だと考えますが、要件によっては都度 UserInfo の値を取得しにいって、内容を確認する処理を実装する必要があるでしょう。その場合には、指定ログイン情報をうまく使ってもらえれば、コールアウトに関わる認証周りの実装負荷を減らすことができますね。

4.おわりに、と参考情報

ここまで解説した通り、サービス提供アプリケーションとしてきちんと動作するためには、少しコーディングをしなければいけないことは確かですが、gBizID と SSO で連携すること自体は、Open ID Connect に対応していることだけあって、認証情報やトークン、UserInfo のやりとりなどをコーディングすることなく画面からの設定だけで済ませることができました。もし、この処理もコーディングが必要だとしたら?もちろん様々なパターンを考慮したテストもやらなければいけないですし、少しげんなりしそうです。

使える標準機能はうまく使っていただき、アプリケーション、ひいてはサービス自体をより良くするための時間捻出に貢献できることを願っています。

参考情報

gBizID 関連

Trailhead

Salesforce Help

開発者向けドキュメント

著者について

稲葉 洋幸(いなば・ひろゆき)は、2016 年に株式会社セールスフォース・ドットコムに入社後プリセールス部門でプラットフォーム・スペシャリストとして、システム全体・データ連携のアーキテクチャデザインや、Lightning Platform / Heroku 上でのアプリ開発に関する提案支援・啓蒙活動に従事。現在は公共分野の業界担当ソリューションエンジニアとして、中央省庁・地方自治体への提案活動・課題解決支援を担当

コメント

[SolutionDev] Open ID Connect を使った SSO の実現例 (gBizID 編)