null 多要素認証機能のカスタマイズ

今回は、Liferay7.3から導入された多要素認証機能について、カスタマイズによる認証方式の追加方法をご紹介します。多要素認証機能の基本的な内容については過去のブログをご参照ください。

使用する環境

今回、LiferayのバージョンはLiferay DXP 7.3.10.1 sp1とし、IDEにはLiferay Developer Studio DXPを使用しました。

概要

以下の二段階に分けてカスタマイズ実装の方法を説明します。なお、本記事ではカスタマイズ認証の追加方法にフォーカスするため、認証方式自体は単純なパスワード方式とします。

  1. 多要素認証方式としてカスタマイズ要素を登録する(パスワードは全ユーザ共通)
  2. ユーザ毎の多要素認証用パラメータを追加する(ユーザ毎にパスワードを設定する)

なお、今回の内容のサンプルコードをBitbucketで公開しておりますので、是非お試しください

1. 多要素認証方式としてカスタマイズ要素を登録する

モジュール構成

今回作成するモジュールをmfa-test-webとします。モジュールの構成は以下のようになります。

mfa-test-web/
├── bnd.bnd
├── build.gradle
└── src/
    └── main/
        ├── java/mfa/test/web/internal/
        │   ├── checker/
        │   │   └── TestBrowserMFAChecker.java  ----------- 主要な処理を記述するクラス
        │   ├── configuration/
        │   │   └── MFATestConfiguration.java  ------------ 設定ページの内容を記述するインターフェース
        │   ├── constants/
        │   │   └── MFATestWebKeys.java  ------------------ 定数を管理するクラス
        │   └── settings/definition/
        │       └── MFATestCompanyConfigurationBeanDeclaration.java  -- 設定を登録するためのクラス
        └── resources/
             └── /META-INF/
                 └── resources/
                      ├── init.jsp  ----------------------- インポートやタグライブラリの宣言ファイル
                      └── mfa_test_checker/
                          └── verify_browser.jsp  --------- 多要素認証時の表示画面

モジュールの作成

  1. Liferay Developer Studio DXPのプロジェクトエクスプローラで、Liferay Workspaceを右クリックし、New → Liferay Module Projectと選択します。
  2. 表示されたウィザードで、以下の内容を記載しFinishボタンを押します。
    • Project name: mfa-test-web
    • Build type: Gradle
    • Project Template Name: mvc-portlet
  3. 上記の構成ツリーと一致するように、フォルダやパッケージ、ファイルの追加、削除を行います。

build.gradleの修正

以下の内容に修正します。

dependencies {
	compileOnly group: "biz.aQute.bnd", name: "biz.aQute.bndlib", version: "default"
	compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel"
	compileOnly group: "com.liferay.portal", name: "com.liferay.util.taglib"
	compileOnly group: "javax.portlet", name: "portlet-api", version: "default"
	compileOnly group: "org.apache.felix", name: "org.apache.felix.http.servlet-api", version: "default"
	compileOnly group: "org.osgi", name: "org.osgi.service.component.annotations", version: "1.3.0"
	compileOnly group: "org.osgi", name: "osgi.core", version: "6.0.0"
	
	compileOnly group: "com.liferay", name: "com.liferay.portal.configuration.metatype.api"
	compileOnly group: "com.liferay", name: "com.liferay.multi.factor.authentication.spi", version: "default"
	compileOnly group: "com.liferay", name: "com.liferay.multi.factor.authentication.web"
}

設定画面の作成

MFATestConfigurationインターフェースとMFATestCompanyConfigurationBeanDeclarationクラスを作成します。設定画面作成についての詳細は公式チュートリアルなどをご参照ください。

MFATestConfiguration

今回、設定画面では以下の3項目を設定できるようにします。

  • enabled: 多要素認証要素の有効/無効を設定するパラメータ
  • order: 多要素認証チェッカーが画面に表示される順序を決めるパラメータ
  • password: 今回のカスタマイズで用いるテストパスワード

実装内容は以下の通りです。

@ExtendedObjectClassDefinition(
    category = "multi-factor-authentication",
    scope = ExtendedObjectClassDefinition.Scope.COMPANY
)
@Meta.OCD(
    id = "mfa.test.web.internal.configuration.MFATestConfiguration",
    localization = "content/Language", name = "MFA_Test_Configuration"
)
public interface MFATestConfiguration {
  @Meta.AD(deflt = "false", name = "enabled", required = false)
  public boolean enabled();

  @Meta.AD(deflt = "400", id = "service.ranking", name = "order", required = false)
  public int order();

  @Meta.AD(deflt = "test", name = "test_password", required = false)
  public String password();
}

MFATestCompanyConfigurationBeanDeclaration

以下のように実装します。

@Component(
    property = "mfa.visibility.configuration.pid=mfa.test.web.internal.configuration.MFATestConfiguration",
    service = ConfigurationBeanDeclaration.class
)
public class MFATestCompanyConfigurationBeanDeclaration implements ConfigurationBeanDeclaration{
  @Override
  public Class getConfigurationBeanClass(){
    return MFATestConfiguration.class;
  }
}

TestBrowserMFACheckerクラスの作成

TestBrowserMFACheckerクラスを作成します。このクラスはcom.liferay.multi.factor.authentication.webバンドルで使用するサービスとして登録され、多要素認証の主要な機能を担当します。

インターフェースの継承

多要素認証チェッカー用のインターフェースcom.liferay.multi.factor.authentication.spi.checker.browser.BrowserMFACheckerを継承します。 このインターフェースには以下の3つのメソッドが含まれます。

  • includeBrowserVerification
    • 多要素認証画面で表示するjspファイルを設定するメソッドです。
  • verifyBrowserRequest
    • 多要素認証の成否を判定するメソッドです。
  • isBrowserVerified
    • 多要素認証が済んでいるかを判定するメソッドです。
    • このメソッドが呼ばれるタイミングでは多要素認証チェッカーの入力内容を直接読み込めないため、verifyBrowserRequestメソッドの成功時にSessionに記録する必要があります。

Componentプロパティの作成

クラス内でMFATestConfigurationの値を使用できるようにするため、以下のようにComponentプロパティを作成します。

@Component(
    configurationPid = "mfa.test.web.internal.configuration.MFATestConfiguration.scoped",
    configurationPolicy = ConfigurationPolicy.REQUIRE, immediate=true, service = {}
)
public class TestBrowserMFAChecker implements BrowserMFAChecker{
}

フィールド定義と依存性注入の実施

以下のように、本クラスで使用するフィールドの定義と依存性注入を行います。

private ServiceRegistration<?> _serviceRegistration;

protected volatile MFATestConfiguration mfaTestConfiguration;

@Reference
private UserLocalService _userLocalService;

@Reference
private Portal _portal;

@Reference(
  target = "(osgi.web.symbolicname=mfa.test.web)"
)
private ServletContext _servletContext;

includeBrowserVerificationの実装

このメソッドでは多要素認証画面で表示するjspファイルを設定します。 具体的には、指定したjspファイルをcom.liferay.multi.factor.authentication.webバンドルのmfa_verify/view.jsp内のaui:form要素の一部として埋め込みます。 今回の例では、/mfa_test_checker/verify_browser.jspというjspファイルを埋め込むこととします(jspファイルの詳細は後述します) 。

メソッドの内容は以下のようになります。

@Override
public void includeBrowserVerification(HttpServletRequest httpServletRequest,
    HttpServletResponse httpServletResponse, long userId) throws Exception {

  RequestDispatcher requestDispatcher = _servletContext.getRequestDispatcher("/mfa_test_checker/verify_browser.jsp");

  requestDispatcher.include(httpServletRequest, httpServletResponse);
}

verifyBrowserRequestの実装

このメソッドは、includeBrowserVerificationで埋め込んだjsp内のフォームから、ユーザからの入力が送信された際に実行され、送信内容の成否を判定します。 今回の例では、入力フォーム名をmfaTestとし、この内容が設定画面で入力したパスワードと一致した場合に認証成功とします。また、認証に成功した場合は、httpSessionに成功したuserIdを記録します。

メソッドの内容は以下のようになります。

@Override
public boolean verifyBrowserRequest(HttpServletRequest httpServletRequest,
    HttpServletResponse httpServletResponse, long userId) throws Exception {

  String mfaTest = ParamUtil.getString(httpServletRequest, "mfaTest");

  if (Validator.isBlank(mfaTest)) {
    return false;
  }
  if (!mfaTestConfiguration.password().equals(mfaTest)) {
    return false;
  }

  HttpServletRequest originalHttpServletRequest = _portal.getOriginalServletRequest(httpServletRequest);
  HttpSession httpSession = originalHttpServletRequest.getSession();
  httpSession.setAttribute(MFATestWebKeys.MFA_TEST_VALIDATED_USER_ID, userId);

  return true;
}

isBrowserVerifiedの実装

このメソッドでは、Sessionの内容によりverifyBrowserRequestが成功しているかを確認します。

メソッドの内容は以下のようになります。

@Override
public boolean isBrowserVerified(HttpServletRequest httpServletRequest, long userId) {

  HttpServletRequest originalHttpServletRequest = _portal.getOriginalServletRequest(httpServletRequest);

  HttpSession httpSession = originalHttpServletRequest.getSession(false);

  User user = _userLocalService.fetchUser(userId);

  if (user == null || httpSession == null) {
    return false;
  }

  Object mfaTestValidatedUserId = httpSession.getAttribute(MFATestWebKeys.MFA_TEST_VALIDATED_USER_ID);

  if(!Objects.equals(mfaTestValidatedUserId, userId)) {
    return false;
  }

  return true;
}

activate, deactivateの実装

コンポーネントの開始時に呼び出されるactivateメソッドと、終了時に呼び出されるdeactivateメソッドを以下のように実装します。

@Activate
protected void activate(BundleContext bundleContext, Map<String, Object> properties) {

  mfaTestConfiguration =
      ConfigurableUtil.createConfigurable(MFATestConfiguration.class, properties);

  if (!mfaTestConfiguration.enabled()) {
    return;
  }

  _serviceRegistration = bundleContext.registerService(
      BrowserMFAChecker.class.getName(), this, new HashMapDictionary<>(properties));
}

@Deactivate
protected void deactivate() {
  System.out.println("deactivate");

  if (_serviceRegistration == null) {
    return;
  }

  _serviceRegistration.unregister();
}

jspファイルの作成

verify_browser.jspを以下のように作成します(init.jspの修正は不要です)。

<%@ include file="/init.jsp" %>

<h3>
    MFA Test
</h3>

<aui:input label="MFA test form" name="mfaTest" showRequiredLabel="yes" />

<aui:button-row>
    <aui:button type="submit" value="submit" />
</aui:button-row>

動作確認

ここでは、既に多要素認証機能が有効化されているLiferayに対し、本例のモジュールをデプロイして動作確認を行います。 多要素認証機能の有効化方法については過去のブログをご参照ください。

  1. 作成したモジュールをデプロイし、Liferayを再起動します。
  2. 本例で実装した以外の多要素認証チェッカーを用いてLiferayにログインし、コントロールパネル→インスタンス設定→セキュリティ→多要素認証にアクセスし、「MFA_Test_Configuration」が存在することを確認します。
  3. MFA_Test_Configurationの「有効」チェックボックスにチェックを入れ、画面最下部の更新ボタンをクリックします。
  4. Liferayからログアウトし、再度ログインを行います。
  5. 多要素認証チェッカーとして、本例のものが表示されていることを確認します。
  6. フォームに「test」と入力し、送信ボタンを押し、ログインできることを確認します。

2. ユーザ毎の多要素認証用パラメータを追加する

事前準備

ユーザ毎のパラメータを設定するためには、パラメータ情報を管理するためのモデル、サービスレイヤー、永続性レイヤーが必要になります。今回はサービスビルダーを用いて以下のようなサービスが作成されているものとして解説を行います。作成方法の詳細は公式チュートリアルなどを参考にしてください。

  • サービスビルダーのプロジェクト名: mfa-test
  • モデル名: MFATestPassEntry
  • モデルのパラメータ
    • mfaTestPassEntryId (Primary Key)
    • companyId
    • userId
    • password
  • サービス名: MFATestPassEntryLocalService
  • サービスの機能
    • Entryの追加 (addTestPassEntry)
    • userIdでのEntryの検索 (fetchMFATestPassEntry)
    • Entryの削除 (deleteMFATestPassEntry)

追加・修正箇所

1.で作成したmfa-test-webに対し、以下の内容の追加・修正を行います。

mfa-test-web/
├── bnd.bnd
├── build.gradle  -------------------------------------------- 修正 (dependencyを追加する)
└── src/
    └── main/
        ├── java/mfa/test/web/internal/
        │   ├── checker/
        │   │   └── TestBrowserMFAChecker.java  -------------- 修正 (ユーザ毎の設定処理を追加する)
        │   ├── configuration/
        │   │   └── MFATestConfiguration.java  --------------- 修正 (使用しないパラメータを削除する)
        │   ├── constants/
        │   │   └── MFATestWebKeys.java
        │   └── settings/definition/
        │       └── MFATestCompanyConfigurationBeanDeclaration.java
        └── resources/
            ├── META-INF/
            │   └── resources/
            │       ├── init.jsp
            │       └── mfa_test_checker/
            │           ├── setup.jsp  ----------------------- 追加 (ユーザ毎の設定時画面)
            │           ├── setup_completed.jsp  ------------- 追加 (ユーザ毎の設定完了時画面)
            │           └── verify_browser.jsp
            └── content
                └── Language.properties ------------- 追加(言語キーの設定)

build.gradleの修正

作成したユーザ毎のパラメータを管理するサービスのapiを使用できるようにするため、dependencyに以下の内容を追記します。

compileOnly project(":modules:mfa-test:mfa-test-api")

TestBrowserMFACheckerの修正

フィールド定義と依存性注入の追加

作成したサービスをクラス内で使用するため、以下のフィールドを追加します

@Reference
private MFATestPassEntryLocalService _mfaTestPassEntryLocalService;

インターフェースの継承

多要素認証チェッカー用のインターフェースcom.liferay.multi.factor.authentication.spi.checker.setup.SetupMFAChecker`を継承します。 このインターフェースには、以下の4つのメソッドが含まれます。

  • includeSetup
    • 各ユーザのアカウント設定→多要素認証の項目に作成したjspファイルを表示するためのメソッドです。
  • isAvailable
    • 各ユーザが多要素認証チェッカーを使用できるかどうかを判定するメソッドです。
  • removeExistingSetup
    • 各ユーザの設定を削除するためのメソッドです。
  • setUp
    • 各ユーザの設定を登録するためのメソッドです。

includeSetupの実装

このメソッドはユーザ毎の設定画面で表示するjspファイルを指定します。具体的には、指定したjspファイルをcom.liferay.multi.factor.authentication.webバンドルの/my_account/setup_user_acount.jsp内のaui:form要素の一部として埋め込みます。 今回の例では、Setupを行っていないユーザー(対応するMFATestPassEntryが存在しない)場合には/mfa_test_checker/setup.jspというjspファイルを、Setupが済んでいるユーザーの場合には/mfa_test_checker/setup_completed.jspを埋め込むこととします(jspファイルの詳細は後述します)

メソッドの内容は以下のようにします。

@Override
public boolean isAvailable(long userId) {

  MFATestPassEntry mfaTestPassEntry = _mfaTestPassEntryLocalService.fetchMFATestPassEntryByUserId(userId);
  if (mfaTestPassEntry != null) {
    return true;
  }
  return false;
}

removeExistingSetupの実装

このメソッドでは各ユーザの設定の削除を行います。 メソッドの内容は以下のようにします。

@Override
public void removeExistingSetup(long userId) {

  MFATestPassEntry mfaTestPassEntry = _mfaTestPassEntryLocalService.fetchMFATestPassEntryByUserId(userId);

  if (mfaTestPassEntry != null) {
    _mfaTestPassEntryLocalService.deleteMFATestPassEntry(mfaTestPassEntry);
  }
}

setUpの実装

このメソッドでは各ユーザの設定の作成を行い、作成に成功した場合はtrueを返します。 メソッドの内容は以下のようにします。

@Override
public boolean setUp(HttpServletRequest httpServletRequest, long userId) {

  String mfaTest = ParamUtil.getString(httpServletRequest, "mfaTest");

  if (mfaTest!=null && !mfaTest.isEmpty()) {
    try {
      _mfaTestPassEntryLocalService.addTestPassEntry(userId, mfaTest);
      return true;
    }catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }
  return false;
}

verifyBrowserRequestの修正

多要素認証チェッカーでの入力内容との照合対象をユーザー毎に設定したパスワードとするため、本メソッドを以下のように修正します。

@Override
public boolean verifyBrowserRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
    long userId) throws Exception {

  String mfaTest = ParamUtil.getString(httpServletRequest, "mfaTest");

  if (Validator.isBlank(mfaTest)) {
    return false;
  }

  MFATestPassEntry mfaTestPassEntry = _mfaTestPassEntryLocalService.fetchMFATestPassEntryByUserId(userId); //修正箇所
  if (mfaTestPassEntry==null || !mfaTest.equals(mfaTestPassEntry.getPassword())) { //修正箇所
    return false;
  }

  HttpServletRequest originalHttpServletRequest = _portal.getOriginalServletRequest(httpServletRequest);
  HttpSession httpSession = originalHttpServletRequest.getSession();
  httpSession.setAttribute(MFATestWebKeys.MFA_TEST_VALIDATED_USER_ID, userId);

  return true;
}

activateの修正

本コンポーネントをSetupMFACheckerのサービスとして登録するため、本メソッドを以下のように修正します

@Activate
protected void activate(BundleContext bundleContext, Map<String, Object> properties) {

  mfaTestConfiguration =
      ConfigurableUtil.createConfigurable(MFATestConfiguration.class, properties);

  if (!mfaTestConfiguration.enabled()) {
    return;
  }

  _serviceRegistration = bundleContext.registerService(new String[] {    //修正箇所
      BrowserMFAChecker.class.getName(), SetupMFAChecker.class.getName() //修正箇所
  }, this, new HashMapDictionary<>(properties));                         //修正箇所
}

MFATestConfigurationの修正

こちらの設定画面のパスワードは不要になるため、以下の項目を削除します。

@Meta.AD(deflt = "test", name = "test_password", required = false)
public String password();

jspファイルの作成

setup.jspsetup_completed.jspを以下のように作成します。 各jspのsubmitボタンを押した際に、com.liferay.multi.factor.authentication.web.internal.portlet.action.SetupUserAccountMVCActionCommanddoProcessActionが実行され、フォームからremoveExistingSetupパラメータがtrueとして送信されている場合には本クラスのremoveExistingSetupメソッドが、そうでない場合にはsetUpメソッドが実行されます。

setup.jsp

<%@ include file="/init.jsp" %>

<aui:input label="mfa-test" name="mfaTest" showRequiredLabel="yes" />

<aui:button-row>
	<aui:button type="submit" value="submit" />
</aui:button-row>

setup_completed.jsp

<%@ include file="/init.jsp" %>

<aui:input name="removeExistingSetup" type="hidden" value="<%= true %>" />

<button class="btn btn-danger" type="submit">
  Remove Password
</button>

Language.propertiesの作成

以下のようにLanguage.propertiesを作成します。この値がユーザ毎の設定画面でのタイトルになります。

mfa.test.web.internal.checker.TestBrowserMFAChecker=MFA Test

動作確認

  1. 作成したモジュールをデプロイし、Liferayを再起動します。
  2. Liferayにログインします。その際、多要素認証画面で今回作成した多要素認証チェッカーが表示されないことを確認します(別の多要素認証チェッカーでのログインが必要になります)。
  3. 右側のユーザアイコンからアカウント設定を選択し、多要素認証タブを選択します。
  4. MFA Testのフォームにパスワード「testpassword」を入力し、申請ボタンを押します。
  5. 画面が切り替わり、Remove passwordのボタンが表示されることを確認します。
  6. Liferayからログアウトします。
  7. 再度ログインを行い、多要素認証画面に今回作成したものが表示されることを確認します。
  8. フォームに「testpassword」と入力して送信ボタンを押し、ログインできることを確認します。

まとめ

今回の内容は以上となります。様々な認証方式を使用できるようになりますので、ぜひ活用してみてください。

関連記事
feature

RANKING
2021.1.08
2020.12.28
2020.12.01
2020.10.30
2020.12.18