Keycloakの認証処理にSMS認証(Twilio利用)を追加してみる

# はじめに

5日目の今日はKeycloakのカスタマイズです。Keycloakについての説明は 「[Keycloakとは](https://qiita.com/daian183/items/30f01e162e03567ff21b)」を参照してください。去年の [Keycloak by OpenStandia Advent Calendar 2017](https://qiita.com/advent-calendar/2017/keycloak-by-openstandia) でKeycloakを紹介する記事です。去年のKeycloak Advent Calendarには私も参加しまして、[Keycloakのカスタマイズポイント](https://qiita.com/yoonis/items/298d580f7e93e56ddf6a)について記事を書きました。今回は、このカスタマイズポイントの中で認証機能（[認証SPI](https://keycloak-documentation.openstandia.jp/master/ja_JP/server_development/index.html#_auth_spi)）をピックアップして認証処理にSMS認証（本人確認）機能を追加するカスタマイズをやってみます。

## SMS？SMS認証？

SMS（ショートメッセージサービス）とは電話番号でショートメッセージを送受信できるサービスです。SMS認証とはSMSを利用して認証(本人確認)を行うことです。絵で表現すると以下のイメージです。

![SMS認証イメージ.png](https://qiita-image-store.s3.amazonaws.com/0/206135/9c335703-7557-389a-6e1c-9f51bbddd8f6.png)

ID/PWDのログインとは別にAさんの登録済み電話番号に認証コードを送信＆確認することで、より確実な本人確認が出来るため、第三者からの不正ログインを防ぐ確率を上げることが出来ます。

SMS送受信サービスを提供しているのは [Twilio](https://twilio.kddi-web.com/), [nexmo](https://www.nexmo.com/jp), [Amazon SNS](https://aws.amazon.com/jp/sns/) などいくつか存在しますが、今回のSMS送受信サービスには<font color="DeepPink"> **Twilio** </font>を利用します。

## Twilioとは？

![20171206203748.png](https://qiita-image-store.s3.amazonaws.com/0/206135/909194c5-8490-45c1-a80a-03ba5aecaac7.png)

Twilioとはサンフランシスコにあるクラウド通信企業で、WEBサービスAPIを使って電話やテキストメッセージを送受信するサービスを提供しています。日本ではKDDIがその代理店となっていて[日本語ドキュメント](https://jp.twilio.com/docs/)も公開していますので、始めて使う時も入りやすいのではないかと思います。

余談ですが、Twilioの[アドベントカレンダー](https://qiita.com/advent-calendar/2018/twilio) も今年登録されていました。（2015年から毎年登録されています）TwilioはSMS送受信以外にも色々なコミュニケーションAPIを提供していますので、興味がある方は参照してください。

# 事前準備

## Twilioのアカウント作成

TwilioのSMS認証を使うにはTwilioのアカウントが必要です（正確にはSMS送受信用のAPI-KEYが必要です）
[無料トライアルのアカウント](https://jp.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account)が登録できてTwilio内の一定の金額までは無料で使うことが可能です。一定の金額に達するとアカウントは自動で停止状態になり、有料アカウントへアップグレード案内通知が来ます。

（2018年12月時点では）有料アカウントへアップグレードしない限り料金が発生することはないですが、今後変わる可能性もありますので登録時には利用規約を確認するようにしてください。登録自体は簡単ですので、普通に進められると思います。登録途中に電話番号検証(SMS)が発生しますのでSMS受信可能な電話を用意しましょう。（SMS通信料は受信者負担です。。）

## TwilioのAPI-KEY取得

アカウント作成終了後、TOP画面が表示されたら「トップ画面」 ⇒「電話番号認証」⇒「電話番号認証 Project ダッシュボード」⇒「Verify」まで進むと以下の画面が表示されます。番号の順番通りに進めましょう。

![verify.png](https://qiita-image-store.s3.amazonaws.com/0/206135/bb8f7adf-79ee-08c1-0be4-bacf7c5f2db5.png)

* 「1. Let’s verify a phone number on your Twilio account」
    * ここでもう一回、電話番号検証します。

* 「2. Create an application and get your API credentials」
    * applicationとAPIを作成。applicationを作成すると、SMS送受信履歴やAPI-KEY確認・更新などが出来るようになります。

* 「3. Send your first Verify code」
    * 上記で作成したapplicationのAPI-KEYを利用してSMS認証コードをcURLで送信します。下記のようにリクエストの中身が表示されますのでAPI-KEYもここで確認できます。（設定画面からも確認できます）

![verify2.png](https://qiita-image-store.s3.amazonaws.com/0/206135/83bdf1d2-a060-7a13-2108-49d41625c1f8.png)

* 「4. Get verified」
    * 受信した認証コードが正しいかcURLで確認します。

以上、Twilio側の事前準備は完了です。

## カスタマイズ前のログイン確認

カスタマイズする前のログイン処理を確認しておきます。ログイン確認はアカウント管理画面にアクセスすることで確認できます。

* アカウント管理画面を表示
    * http://localhost:8080/auth/realms/レルム名/account

![Login.png](https://qiita-image-store.s3.amazonaws.com/0/206135/cf3446f1-ba92-ca2e-943d-478ca9e38652.png)

* アカウント管理画面にログイン成功

![Login_OK.png](https://qiita-image-store.s3.amazonaws.com/0/206135/6b0bf652-6644-6459-a7c6-bd5a829cf333.png)

Username / Password の入力のみでログインできることを確認しました。

# 認証機能のカスタマイズ

認証機能をカスタマイズします。作業順番は以下の通りです。

1. 画面(Themes)作成
2. SMS認証モジュール作成
3. カスタマイズモジュールをKeycloakに反映
4. Keycloak起動
5. 設定反映(管理コンソール)

## 画面(Themes)作成

二つの画面(`.ftl`)とメッセージファイル(`.properties`)を作成するため、「openstandia」という新しいThemesを作成しました。ソースコードは [こちら](https://github.com/sangyoon-lee/keycloak-sms-authenticator/tree/master/themes) です。反映するにはソースコードの `openstandia` フォルダをそのまま `_KEYCLOAK_HOME_/themes/` 配下に配置すれば反映されます。

* 認証コード入力画面(sms-validation.ftl)

![verifyinput.png](https://qiita-image-store.s3.amazonaws.com/0/206135/96994a62-792e-72a4-faea-b5a05d7b7212.png)

* 認証コードエラー画面(sms-validation-error.ftl)

![verifyerror.png](https://qiita-image-store.s3.amazonaws.com/0/206135/643a303e-19dd-82ad-ddf2-409d5dc2ed63.png)

* メッセージファイル(*.properties)
    * messages_ja.properties （日本語メッセージ）
    * messages_en.properties （英語メッセージ）

メッセージファイルは日本語（jp）対応と英語（en）対応の２つ用意しました。対応する言語は [theme.properties](https://github.com/sangyoon-lee/keycloak-sms-authenticator/blob/master/themes/openstandia/login/theme.properties) の`locales`で設定します。

```_KEYCLOAK_HOME_/themes/テーマ名/login/theme.properties
parent=keycloak
import=common/keycloak
locales=en,ja
```

Themesのカスタマイズ方法は去年の「[Keycloakのカスタマイズポイントを整理してみる - Themesについて](https://qiita.com/yoonis/items/298d580f7e93e56ddf6a#themes%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)」で説明していますので、参照してください。

## SMS認証モジュール作成

SMS認証モジュールを作成します。先ほど「TwilioのAPI-KEY取得」時にcURLで認証コードの発行、認証コードの確認を行いましたが、これをSMS認証モジュールの中で実行するようにします。

* SMS認証モジュールに該当するSPIは、**認証SPI**
* `implements` する `interface` は、 `org.keycloak.authentication.Authenticator`

まず基本となる以下２つのクラスを作成します。

* SMS認証モジュールクラス
    * SMSAuthenticator implements Authenticator

* SMS認証モジュールクラスのファクトリークラス
    * SMSAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory

### SMSAuthenticatorFactory実装

* `getId()` メソッド
    * プロバイダーIDを返します。（一意）

```java
	public static final String PROVIDER_ID = "sms-authenticator-with-twilio";

	public String getId() {
		return PROVIDER_ID;
	}
```

* `getDisplayType()` メソッド
    * 画面表示名を返します。

```java
	public String getDisplayType() {
		return "Twilio SMS Authentication";
	}
```

* `isConfigurable()` メソッド
    * 画面（管理コンソール）で設定可能なAuthenticatorかどうかを返す。画面から入力させる項目がある場合は `true` を返します。今回はAPI-KEYなどを入力させるので `true`

* `getConfigProperties()` メソッド
    * 画面（管理コンソール）入力項目の定義です。API-KEY、認証コード桁数、プロキシ設定などを管理コンソールから入力させたいのでここで設定します。

```java
	private static final List<ProviderConfigProperty> configProperties;
	static {
        configProperties = ProviderConfigurationBuilder
                .create()
                .property()
                .name(SMSAuthContstants.CONFIG_SMS_API_KEY)
                .label("API-KEY")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("")
                .add()

                ......

                .property()
                .name(SMSAuthContstants.CONFIG_CODE_LENGTH)
                .label("Code Length")
                .type(ProviderConfigProperty.STRING_TYPE)
                .helpText("")
                .defaultValue(4)
                .add()

                .build();
	}

    ......

	public List<ProviderConfigProperty> getConfigProperties() {
		return configProperties;
	}

```

* `getRequirementChoices()` メソッド
    * サポートするRequirementタイプを返します。今回は必須(REQUIRED)、無効(DISABLED)のみ設定します。他にはALTERNATIVE(代替)、OPTIONAL(任意)があります。

```java
	private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
			AuthenticationExecutionModel.Requirement.REQUIRED,
			AuthenticationExecutionModel.Requirement.DISABLED
	};

	public Requirement[] getRequirementChoices() {
		return REQUIREMENT_CHOICES;
	}
```

* SMSAuthenticatorFactoryクラスの登録するために、META-INF配下に、以下のようにファクトリークラス名を記述した定義ファイルを配置します。

```META-INF/services/org.keycloak.authentication.AuthenticatorFactory
jp.openstandia.keycloak.authenticator.SMSAuthenticatorFactory
```

### SMSAuthenticator実装

* `authenticate(AuthenticationFlowContext context)` メソッド
    * 認証処理、このメソッドでSMS認証コード送信処理を実装

```java
public void authenticate(AuthenticationFlowContext context) {

	......
	
	if (sendVerify.sendSMS(phoneNumber)) { // SMS送信

		Response challenge = context.form().createForm("sms-validation.ftl");
		context.challenge(challenge);

	} else {

        // SMS失敗時はエラー画面を返します
		Response challenge = context.form().setError(new FormMessage("sendSMSCodeErrorMessage"))
				.createForm("sms-validation-error.ftl");
		context.challenge(challenge);
	}

	......
}
```

ここで注目すべきなのは結果の戻し方です。Response(Challenge)情報に戻す画面を設定(createForm)して `context` に設定します。エラーが発生した場合はエラーメッセージを設定(`setError`)することもできます。`setError` の設定値 `new FormMessage("sendSMSCodeErrorMessage")` の `sendSMSCodeErrorMessage`は、Themesの [messages](https://github.com/sangyoon-lee/keycloak-sms-authenticator/tree/master/themes/openstandia/login/messages) ファイル `messages_xx_properties`に定義されたメッセージKeyになります。

* `action(AuthenticationFlowContext context)` メソッド
    * このメソッドで画面から入力された認証コードをチェックする処理を実装
  
```java
public void action(AuthenticationFlowContext context) {

	MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
	String enteredCode = inputData.getFirst("smsCode");

	......
	
	if (sendVerify.verifySMS(phoneNumber, enteredCode)) { // 認証コード確認
		logger.info("verify code check : OK");
        // 認証コードOK時
		context.success();

	} else {
        // 認証失敗時はエラー画面を返す
		Response challenge = context.form()
				.setAttribute("username", context.getAuthenticationSession().getAuthenticatedUser().getUsername())
				.setError(new FormMessage("invalidSMSCodeMessage")).createForm("sms-validation-error.ftl");
		context.challenge(challenge);
		}

	......
}
```

ここも同じく結果の戻し方に注目です。`verifySMS` で認証コードのチェックが正常だった場合は、`context.success()`でこの認証結果はOKになります。エラーの場合は、`authenticate` メソッドと同じく、Response(Challenge)に表示画面やエラーメッセージを設定して `context` に設定します。

### SMS送信/確認処理

上記`SMSAuthenticator`で実装するSMS送信/コード確認処理は、`HttpsURLConnection` を使ってTwilioのAPIを利用して実装しました。SMS送信(TwilioのAPI呼出)処理は [こちら] (https://github.com/sangyoon-lee/keycloak-sms-authenticator/blob/master/src/main/java/jp/openstandia/keycloak/authenticator/api/SMSSendVerify.java) 

モジュール作成は以上です。ここでは重要なメソッドのみを紹介しました。サンプルモジュールのソースコードをGitHub公開していますので、細かい設定はソースコードを確認してください。 ソースコードは [こちら](https://github.com/sangyoon-lee/keycloak-sms-authenticator)

## カスタマイズモジュールをKeycloakに反映

作成したファイルをKeycloakに反映します。

### Themesの配置
作成したThemesファイル(Openstandiaフォルダごと) `_KEYCLOAK_HOME_/themes/` に配置します。「画面(Themes)作成」時に配置済みであればスキップしてください。

### 認証モジュール(.jar)の配置
作成した認証モジュールは`mvn package`でパッケージングして、jarファイルを `_KEYCLOAK_HOME_/standalone/deployments/` に配置します。

## Keycloak起動

カスタマイズモジュールを全て配置したら、Keycloakを起動します。

## 設定反映(管理コンソール)

### Themesの反映

Themesを反映します。「レルムの設定」ー「テーマ」タブから行います。今回はログインテーマのみ作成していますので「ログインテーマ」を「openstandia」に変更します。その他のテーマは変更しません。

![themes.png](https://qiita-image-store.s3.amazonaws.com/0/206135/73763485-e7bf-8b63-59cf-8e16b8d88025.png)

### 認証フローの修正

SMS認証モジュールを認証処理に追加する為に、管理コンソールから認証フローを修正します。

「管理コンソール」ー「認証」ー「Browser」の認証フロー画面

![console1.png](https://qiita-image-store.s3.amazonaws.com/0/206135/d9e04b52-e123-a7e7-321e-6d9ffbeb5be5.png)

（デフォルトの「Browser」認証フローは直接修正できないため）「Browser」の認証フローをコピーします。認証フロー名は任意。

![console2.png](https://qiita-image-store.s3.amazonaws.com/0/206135/dadefe06-0eb8-973a-d6b4-af72f8564072.png)

コピーした「Openstandia Browser」認証フロー

![console3.png](https://qiita-image-store.s3.amazonaws.com/0/206135/ec0fb5ea-6f2d-e458-72f3-742838e8c5c0.png)

「Openstandia Browser Forms」の「アクション」をクリックして「Execution を追加」をクリック。

![console4.png](https://qiita-image-store.s3.amazonaws.com/0/206135/77331f10-1dc2-e096-6500-b423f756d19d.png)

今回作成したモジュールを選択。ここの表示名は `SMSAuthenticatorFactory` の `getDisplayType` で設定した名称。

![console5.png](https://qiita-image-store.s3.amazonaws.com/0/206135/f0e9d2d0-8c04-0bc6-3b67-ac365613e23b.png)

「Twilio SMS Authentication」を追加して、実行順序を「OTP Form」の上に移動。「OTP Form」は無効(DISABLED)に設定（任意）

![console6.png](https://qiita-image-store.s3.amazonaws.com/0/206135/17c164fa-d1bd-76bd-708b-a6170a820a06.png)

「Twilio SMS Authentication」の「アクション」をクリックして「設定」をクリック。`SMSAuthenticatorFactory` の `getConfigProperties` で設定した画面項目がここで表示されます。

![console7.png](https://qiita-image-store.s3.amazonaws.com/0/206135/c9ad4719-796a-9078-4bf3-53d42d61189e.png)

* 「API-KEY」はTwilioで取得したAPI-KEYを設定します。
* プロキシが必要であれば、「Proxy Enabled」を「オン」にしてプロキシ情報を入力します。
* 「SMS Code Length」は送信する認証コードの桁数です。

認証フローに戻り、「バインディング」タブをクリック。ブラウザーフローをコピーした「Openstandia browser」に変更します。

![console8.png](https://qiita-image-store.s3.amazonaws.com/0/206135/dc0602e5-d75a-0540-53df-9e2591d11943.png)

これで、管理コンソールの変更は完了です。

# 動作確認

事前にログインするユーザー属性に電話番号情報を追加します。電話番号属性の「Key」は認証モジュール内の属性名と合わせます。

![user_telnum.png](https://qiita-image-store.s3.amazonaws.com/0/206135/789fe5cc-ab25-4be8-b592-9c046d72940b.png)

アカウント管理画面（http://localhost:8080/auth/realms/レルム名/account） にログインします

![Login.png](https://qiita-image-store.s3.amazonaws.com/0/206135/a4e6dcf2-d759-7108-cda5-7a47cf0b402b.png)

認証コード入力画面が表示されます。

![verifyinput.png](https://qiita-image-store.s3.amazonaws.com/0/206135/e8c55709-4c00-2a6e-7d8b-2e5decf833e0.png)

入力した電話番号にSMSで認証コードが通知されます。

![認証コード.png](https://qiita-image-store.s3.amazonaws.com/0/206135/b0e2ed9c-2236-7e30-9347-a2131c5d3eeb.png)

認証コードを入力します

![verify1.png](https://qiita-image-store.s3.amazonaws.com/0/206135/4aa59a89-d17e-393f-e273-41c64238816f.png)

ログイン完了です

![verifyok.png](https://qiita-image-store.s3.amazonaws.com/0/206135/cf27031c-c2c9-e66f-a95e-c9acea0e7ba0.png)

※ 以下は間違ったコードを入力した場合のエラー画面。「認証コードが一致しません。」というエラーメッセージは Response(Challenge) の setError で設定したメッセージです。

![verifyerror.png](https://qiita-image-store.s3.amazonaws.com/0/206135/7e9c5b25-5f2d-6ae8-a245-7722f57ff90a.png)

動作確認は以上です。

# 最後に

認証SPIをカスタマイズして認証処理にSMS認証を追加してみました。Keycloakのカスタマイズは認証SPIに限らず全SPIのカスタマイズ方法は基本的に同じです。

簡単に言えば、

* カスタマイズする機能に該当するSPIを確認する。（今回は認証SPI）

* カスタマイズする機能が `implements` すべき `interface` を確認する。（今回はAuthenticator）

* 対象 `interface` を  `implements` して用意されているメソッドを実装する。

これだけです。

3つ目のメソッドを実装する際に各メソッドがどういう役割なのかを理解しながら実装していきます。（ここがメインなので一番時間かかりますが、、）

どのSPIのカスタマイズでも一度経験すれば、次からは別のSPIをカスタマイズすることになっても、スムーズに実装できると思います。ぜひ、Keycloakのカスタマイズをチャレンジしてみてください :+1:

# 参照資料

* [Twilio Docs](https://jp.twilio.com/docs/)
* [Keycloak Docs - 認証SPI](https://keycloak-documentation.openstandia.jp/master/ja_JP/server_development/index.html#_auth_spi)
* 今回のソースコード(GitHub)
    *  [keycloak-sms-authenticator](https://github.com/sangyoon-lee/keycloak-sms-authenticator)

[![mark.png](https://github.com/k-tamura/cocha-icons/raw/master/mark.png)](https://github.com/k-tamura/cocha-icons)