3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社ネオシステムAdvent Calendar 2024

Day 1

KeycloakでメールOTP認証SPIを実装

Last updated at Posted at 2024-12-01

この記事は「ネオシステム Advent Calendar 2024」の1日目の記事です。

はじめに

今年もAdvent Calendarにチャレンジさせてもらいました。
協力してくれる社員の方々、日々忙しい中にもかかわらず本当にありがとうございます。
最終日まであっという間だと思いますが、よろしくお願いします。

今年は一年を通じて「Keycloak」というID管理を実現するためのOSSに触る機会が多かったです。Keycloakに触る前は同じくID管理を実現する「OpenAM」というOSSを業務で扱ったことがあったのですが、個人的にはKeycloakの方が直観的に設定等が理解できる場面が多く扱いやすい印象です。

ただ、Keycloakには標準で「メールOTP認証」が搭載されておらず、Keycloakの拡張機能(SPI)として機能を実装する必要があります。本記事では同機能を実装した際の手順等を書いていきます。

また、本記事に出てくるソースコードはGitHubでも公開しています。ソースコードの内容詳細やディレクトリ構成はこちらで確認願います。

環境

  • Keycloak(v25.0.6)
  • Java(v17.0.13)
  • Apache Maven(v3.8.7)
  • docker(v27.3.1)
  • maildev

Java、Maven、dockerは事前にインストールされている前提で話を進めます。

メールOTP認証とは

認証方式の一つで、登録されたメールアドレス宛にワンタイムパスワード(OTP)が記されたメールを送信し、それを画面で入力して認証を行います。最近はMicrosoftやGoogleが提供する「Authenticator」アプリを利用したOTP認証の方がよく見かける印象ですが、Authenticatorアプリがインストールされたスマホを忘れた際などの予備策として備えられていたりします。

環境構築

Keycloakとmaildevはdockerイメージが提供されているので、docker-compose.ymlを用意して構築します。複数のコンテナを利用されたい方は、適宜ポート番号やコンテナ名を調整して下さい。

docker-compose.yml
services:
  keycloak:
    image: quay.io/keycloak/keycloak:25.0
    command: start-dev
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KC_LOG_LEVEL: info
    ports:
      - 8080:8080

  maildev:
    image: maildev/maildev
    ports:
      - 1080:1080
      - 1025:1025

ファイルを準備したら、以下コマンドでコンテナを生成します。

sudo docker compose up -d

Keycloak初期設定

テスト用レルム作成

レルムとは、Keycloakが対象とする認証情報やセキュリティ設定などが適用される範囲を指します。今回はまずテスト用のレルムを作成します。

ブラウザを開き、以下URLからKeycloakの管理コンソールにアクセスします。

http://localhost:8080

アクセスすると以下のようなログイン画面が表示されますので、コンテナ生成時に指定した「KEYCLOAK_ADMIN」「KEYCLOAK_ADMIN_PASSWORD」の値を指定してログインします。
image.png

左メニューの上部にレルムを選択するプルダウンがあるので、プルダウンをクリックし、表示された「Create realm」ボタンを押下します。
image.png

レルム作成画面に遷移するので、「Realm name」に任意の名称(今回はtest-realm)を入れて「Create」ボタンを押下します。
image.png

ローカライズ設定

作成したテスト用レルムの表示言語を日本語に変更します。この設定は必須ではありませんが、SPIの言語設定ファイルがこの設定に準じるため変更しています。

左メニューの上部から作成したテスト用レルムを選択すると、レルムのウェルカム画面に遷移します。
image.png

左メニューから「Realm Settings」を選択し、レルムの設定画面に遷移します。
image.png

右画面上部のタブから「Localization」を選択し、ローカライズ設定画面に遷移します。
image.png

「Internationalization」トグルをオンにすると「Support locales」「Default locale」が表示されるので、それぞれ以下のように設定し「Save」ボタンを押下します。
image.png

SMTP設定

Keycloakと合わせてコンテナを生成した「mailDev」をSMTPサーバとして設定します。mailDevはSMTPサーバのモックを提供してくれるアプリケーションで、メール送信を伴う開発時に重宝します。mailDevの説明は以下の記事がとても分かりやすいです。

レルムの設定画面から、上部のタブ「Email」を選択し、SMTP設定画面に遷移します。
image.png

Templateの項より「From」の値に差出人を設定します。ここでは管理用レルム(master)の管理者のアドレスを設定します。
image.png

Connection & Authenticationの項より「Host」「Port」を以下のように設定し「Save」ボタンを押下します。Hostの値には「localhostではなくmaildevのdockerコンテナに割り当てられたIPアドレス」を指定して下さい。
image.png

保存後、「Test connection」を押下して疎通確認を行います。問題無ければ画面の右上に以下のようなメッセージが表示されます。
image.png

SPI実装

プロバイダークラス

KeycloakのSPIを実装する場合、ビジネスロジックを実装する「プロバイダークラス」と、そのプロバイダークラスのインスタンスを生成する「プロバイダーファクトリークラス」を実装する必要があります。

プロバイダークラスはKeycloakの「org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator」を継承する必要があり、その上でいくつかの抽象メソッドの具象化が必要です。修正が必須なのは「authenticate」「action」メソッドです。それ以外のメソッドは必要なければNOPでOKです。

authenticateメソッド

本メソッドには、ワンタイムパスワード(認証コード)の入力画面に遷移する前の処理を実装します。認証コードを生成し、生成したコードをメールで送信しています。生成したコードはセッションに格納し、後のactionメソッドで画面から入力された値との比較チェックを行います。

    @Override
    public void authenticate(AuthenticationFlowContext context) {
        // 認証コードを生成
        String code = SecretGenerator.getInstance().randomString(MAIL_OTP_CODE_LENGTH, SecretGenerator.DIGITS);
        String codeTTL = Long.toString(System.currentTimeMillis() + (MAIL_OTP_CODE_TTL_VALUE * 1000L));

        // 生成した認証コードをセッションに格納
        AuthenticationSessionModel session = context.getAuthenticationSession();
        session.setAuthNote(MAIL_OTP_SESSION_KEY_CODE, code);
        session.setAuthNote(MAIL_OTP_SESSION_KEY_CODE_TTL, codeTTL);

        // メール送信準備
        EmailTemplateProvider emailTemplateProvider = context.getSession().getProvider(EmailTemplateProvider.class);
        emailTemplateProvider.setRealm(context.getRealm());
        emailTemplateProvider.setUser(context.getUser());

        // メール件名と本文への設定値を準備
        List<Object> subjectParams = List.of(context.getRealm().getName());
        Map<String, Object> mailBodyAttributes = new HashMap<>();
        mailBodyAttributes.put("username", context.getUser().getUsername());
        mailBodyAttributes.put("code", code);
        mailBodyAttributes.put("ttl", codeTTL);

        // メール送信
        try {
            emailTemplateProvider.send("emailCodeSubject", subjectParams, "code-email.ftl", mailBodyAttributes);
        } catch (EmailException e) {
            log.error("failed to send email.");
            throw new RuntimeException(e);
        }

        // メールOTP認証画面に遷移
        Response response = context.form().createForm("email-authenticator.ftl");
        context.challenge(response);
    }

actionメソッド

ワンタイムパスワード(認証コード)の入力画面から渡された認証コードのチェックを行い、OKであれば認証成功、NGであれば認証失敗としてエラー画面に遷移させています。

@Override
    public void action(AuthenticationFlowContext context) {
        UserModel userModel = context.getUser();
        if (!enabledUser(context, userModel)) {
            return;
        }

        // 認証コード検証
        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
        AuthenticationSessionModel session = context.getAuthenticationSession();
        String code = session.getAuthNote(MAIL_OTP_SESSION_KEY_CODE);
        String codeTTL = session.getAuthNote(MAIL_OTP_SESSION_KEY_CODE_TTL);
        String enteredCode = formData.getFirst(MAIL_OTP_SESSION_KEY_CODE);

        // 認証コードチェック
        if (!code.equals(enteredCode)) {
            context.attempted();
            return;
        }

        context.getAuthenticationSession().removeAuthNote(MAIL_OTP_SESSION_KEY_CODE);
        context.success();
    }

プロバイダーファクトリークラス

先述したプロバイダークラスを生成するためのファクトリークラスです。Keycloakの「org.keycloak.authentication.AuthenticatorFactory」インターフェースを実装し、抽象メソッドを具象化します。修正が必須なのは「create」「getId」「getDisplayType」メソッドです。それ以外のメソッドは必要なければNOPでOKです。

createメソッド

プロバイダークラスのインスタンスを生成して返却するためのメソッドです。インスタンスは予めシングルトンで生成しておき、それを返却するようにしています。

    /**
     * シングルトンインスタンス
     */
    private static final EmailAuthenticator SINGLETON = new EmailAuthenticator();

    
    
    @Override
    public Authenticator create(KeycloakSession arg0) {
        return SINGLETON;
    }

getIdメソッド

Keycloak内でプロバイダーを扱う際の識別子を返却するためのメソッドです。UUID等の形式にする必要はなく、ただの文字列でも良いようです。

    /**
     * プロバイダID
     */
    private static final String PROVIDER_ID = "email-authenticator";

    
        
    @Override
    public String getId() {
        return PROVIDER_ID;
    }

getDisplayTypeメソッド

この後にも出てきますが、Keycloakの設定画面で表示される名称を返却するためのメソッドです。

    /**
     * 表示名
     */
    private static final String DISPLAY_NAME = "Email Authentication";

    

    @Override
    public String getDisplayType() {
        return DISPLAY_NAME;
    }

プロバイダーファクトリー定義ファイル

名称が正しいかはわかりませんが、KeycloakがSPIのプロバイダーファクトリークラスを認識するための設定ファイルが必要です。SPIでファイル名が決まっており、ファイル内にプロバイダーファクトリークラスのFQCNを記載します。

org.keycloak.authentication.AuthenticatorFactory
jp.co.neosystem.keycloak.spi.email_authenticator.EmailAuthenticatorFactory

SPIビルド&デプロイ

実装したSPIは、Mavenでjar形式にします。生成したjarはKeycloakコンテナ内の「/opt/keycloak/providers」ディレクトリ配下にデプロイします。

# ビルド(pom.xmlがあるディレクトリに移動後)
mvn clean package

# KeycloakコンテナのコンテナIDを調べる
docker ps | grep "{コンテナ名}"
0b38b4edaa59   quay.io/keycloak/keycloak:25.0   "/opt/keycloak/bin/k…"   About an hour ago   Up 47 minutes                  8443/tcp, 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 9000/tcp                          keycloak_spi_email_authenticator-keycloak-1

# デプロイ
docker cp target/email-authenticator-25.0.6.jar 0b38b4edaa59:/opt/keycloak/providers

デプロイまで完了したら、Keycloak管理コンソールにアクセスします。左メニューの上部プルダウンから「master」レルムを選択するとウェルカム画面が表示されます。
image.png

右画面のタブから「Provider Info」タブをクリックします。authenticator欄に今回実装したSPIの名称「email-authenticator」が表示されていればデプロイ成功です。
image.png

Keycloak追加設定(認証フローへのSPI適用)

再びテスト用レルム「test-realm」を選択し、左メニューから「Autehntication」を選択すると、認証フローの設定画面に遷移します。
image.png

一覧の一番上に表示されている「browser」というのが、ユーザがブラウザからアクセスした際に適用される認証フローになります。リンクをクリックすると認証フローの設定画面が表示されます。
image.png

今回はユーザ名とパスワードによるフォーム認証を行った後、メールOTP認証が適用されるように設定します。画面上部の「Add Step」ボタンを押下すると認証フローのStep設定用のダイアログが開くので、今回実装したメールOTP認証SPIを選択して「Add」ボタンを押下します。
image.png

メールOTP認証が追加されたら、ドラッグ&ドロップでユーザ名とパスワードによるフォーム認証の後に位置を移動しRequiredにします。
image.png

動作確認

以下テスト用レルムのマイページに遷移して、ログイン時の動作を確認します。

http://localhost:8080/realms/test-realm/account

フォーム認証が表示されるので、レルム内に作成したユーザで認証を行います。
image.png

認証するとmailDevに認証コードメールが届き、画面上には認証コードの入力画面が表示されます。
image.png

認証コードを入力し、問題無ければマイページが表示されます。
image.png

終わりに

今回はKeycloakのメールOTP認証SPIの説明をさせてもらいました。SPIはこの他にもアカウント登録や削除などのイベント契機で動くものや、RESTAPIなど多種多様なものが実装できます。今後も何か別のSPIを実装したら、Qiitaに投稿できればと考えています。

あと、本記事にも説明をかなり端折っている部分などがあるので、Advent Calebndarを走りきる前にしれっと補完していきたいと考えています…

参考文献

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?