Keycloakで認証SPiを実装する方法

  •  
 
おじさん2023年9月27日 - 10:52 に投稿

概要:

 この記事は、Keycloakで認証SPIの実装をおこなったので、その手順をまとめたものです
 作成した認証は、フォームから aaa を入力すると通るものになります

使用したもの:

 Keycloak:22.0.1(ソースコードも22.0.1)
 Open JDK:18.0.2
 Maven:3.9.4
 OS:Mac(Apple M1) 13.4

前提条件:

 Keycloakとそのソースコードが必要になります
 また、Keycloakを動かすにはJavaが必要です
 認証SPIのビルドにはMavenを使います
 それぞれ準備できてない場合は、インストールやダウンロードをお願いします

 Keycloakはここからダウンロードできます
 Keycloakのソースコードはここからダウンロードできます

目次:

  • 認証SPIについて
  • 作成したファイル
  • 実装手順
  • 実装している時に起きた問題

認証SPIについて:

 Keycloakでは、パスワード、OTP等の認証方法が提供されています
 それ以外に独自に作成した認証方法も追加できるようになっています

 認証SPIの作成には、Keycloakのソースコードの方を使います
 必要なファイルを作成して、Mavenを使ってビルド、jarファイルにすることで、
 Keycloakの認証として追加することができるようになります

 認証SPIの実装には最低限以下の2つのインターフェースを実装する必要があります
- Authenticator - AuthenticatorFactory

Authenticator:

 認証の処理を実装するためのインターフェースです
 いくつかメソッドが宣言されているので、それらを実装する必要があります

実装する必要のあるメソッド:

  • public void authenticate(AuthenticationFlowContext context);
    認証の時に最初に呼ばれるメソッド
    この中でページを作成して、 context.challenge(Response)を呼び出すことで
    actionメソッドが呼び出される

  • public void action(AuthenticationFlowContext context);
    認証の処理を書くメソッド
    context.successを呼び出すと認証成功になる

  • public boolean requiresUser();
    この認証の前にユーザ認証が済んでいる必要があるかどうかを指定する
    認証フローの設定でUser Password Form等の前に持ってくる場合は、falseを指定

  • public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user);
    認証方法がユーザ用かどうかを返すメソッド
    requiresUserがtrueを返す場合は、trueを返さないと認証が飛ばされるみたいでした

  • public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user);
    秘密の質問やメールアドレス更新等、必要な操作を要求する場合はここに処理を書かないといけないようです
    Required actionsを特に要求しない場合は何も書かなくて大丈夫でした

  • public void close();
    アプリ終了時に呼びだされる終了処理
    特に何も書かなくて大丈夫でした

AuthenticatorFactory:

 実装したAuthenticatorのインスタンスを作成するためのインターフェース
 AuthenticatorFactoryインターフェースでも、宣言されたメソッドをそれぞれ実装する必要があります

実装する必要があるメソッド:

  • public String getId();
    認証のIDを返す
    管理コンソールのプロバイダ一覧(master realm -> Provider info)にはこのIDで表示される

  • public Authenticator create(KeycloakSession session);
    Authenticatorを実装したクラスのインスタンスを作成するメソッド

  • public AuthenticationExecutionModel.Requirement[] getRequirementChoices();
    認証に設定できるの実行要件を配列で返す(REQUIRED、ALTERNATIVE等)

  • public boolean isUserSetupAllowed();
    Authenticatorを実装するクラスのsetRequiredActions()メソッドを呼び出すかどうか

  • public boolean isConfigurable();
    管理コンソールで認証の設定をできるかどうか

  • public List getConfigProperties();
    設定したProviderConfigPropertyの配列を返す
    isConfigurableメソッドでtrueを返す場合、ProviderConfigPropertyで指定したものは、管理コンソールの設定できるようになる
    設定は、管理コンソールの認証フローの設定(Authentication -> Flows)でSettings(歯車のマーク)を押すと表示される

  • public String getHelpText();
    管理コンソールの認証フローの設定で表示される時の説明文を返す

  • public String getDisplayType();
    管理コンソールの認証フローの設定で表示される名前を返す

  • public String getReferenceCategory();
    管理コンソールでのカテゴリーを返す

  • public void init(Config.Scope config);
    初期化処理
    特に何も書かなくて大丈夫でした

  • public void postInit(KeycloakSessionFactory factory);
    ファクトリクラスが全部初期化された後に呼び出されるメソッド
    特に何も書かなくて大丈夫でした

  • public void close();
    アプリ終了時に呼びだされる終了処理
    特に何も書かなくて大丈夫でした

作成したファイル:

 今回の認証SPIを実装する時に作成したファイルです
 作成する必要があるファイルは全部で5つになります

  • プログラム
    • Authenticatorを実装したクラスファイル
    • AuthenticatorFactoryを実装したクラスファイル
  • テンプレートファイル
    • 認証画面のテンプレートファイル(ftl)
  • その他のファイル
    • pom.xml
    • 使用するクラスを定義するファイル

プログラム:

 今回実際に実装したプログラムは以下になります

  • TestAuthenticator.javaファイル:

    package org.keycloak.examples.authenticator;
    
    import org.keycloak.http.HttpCookie;
    import org.keycloak.http.HttpResponse;
    import org.keycloak.authentication.AuthenticationFlowContext;
    import org.keycloak.authentication.AuthenticationFlowError;
    import org.keycloak.authentication.Authenticator;
    import org.keycloak.models.AuthenticatorConfigModel;
    import org.keycloak.models.KeycloakSession;
    import org.keycloak.models.RealmModel;
    import org.keycloak.models.UserModel;
    
    import jakarta.ws.rs.core.MultivaluedMap;
    import jakarta.ws.rs.core.Response;
    import java.net.URI;
    
    
    public class TestAuthenticator implements Authenticator {
    
        @Override
        public void authenticate(AuthenticationFlowContext context) {
            //ページ作成
            Response challenge = context.form()
                    .createForm("test.ftl");
            //認証をおこなう
            context.challenge(challenge);
        }
        @Override
        public void action(AuthenticationFlowContext context) {
            MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
            String secret = formData.getFirst("answer");
    
            //入力値を確認
            boolean validated = secret.equals("aaa");
            if (!validated) {
                //間違ってたらもう一度認証
                Response challenge =  context.form()
                        .setError("badSecret")
                        .createForm("test.ftl");
                context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
                return;
            }
            //認証が通ったらクッキーを取得
            setCookie(context);
            context.success();
        }
    
        //クッキー取得
        protected void setCookie(AuthenticationFlowContext context) {
            AuthenticatorConfigModel config = context.getAuthenticatorConfig();
            int maxCookieAge = 60 * 60 * 24 * 30; // 最長有効期限の設定(秒)
            URI uri = context.getUriInfo().getBaseUriBuilder().path("realms").path(context.getRealm().getName()).build();
            HttpResponse response = context.getSession().getContext().getHttpResponse();
            response.setCookieIfAbsent(
                    new HttpCookie(
                            1, "COOKIE_AAA", "AAAAA",
                        uri.getRawPath(),
                        null, null,
                        maxCookieAge,
                        false, true,  null));
        }
        @Override
        public boolean requiresUser() {
            return true;
        }
        @Override
        public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
            return true;
        }
        @Override
        public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
            //オーバーライドしないとコンパイルが通らないので定義
        }
        @Override
        public void close() {
            //オーバーライドしないとコンパイルが通らないので定義
        }
    }
    
  • TestAuthenticatorFactory.javaファイル:

    package org.keycloak.examples.authenticator;
    
    import org.keycloak.Config;
    import org.keycloak.authentication.Authenticator;
    import org.keycloak.authentication.AuthenticatorFactory;
    import org.keycloak.models.AuthenticationExecutionModel;
    import org.keycloak.models.KeycloakSession;
    import org.keycloak.models.KeycloakSessionFactory;
    import org.keycloak.provider.ProviderConfigProperty;
    
    import java.util.ArrayList;
    import java.util.List;      
    
    public class TestAuthenticatorFactory implements AuthenticatorFactory {
    private static final TestAuthenticator SINGLETON = new TestAuthenticator();
    
        @Override
        public String getId() {
            System.out.println("getId foo");
            return "test-authenticator";
        }
    
        @Override
        public Authenticator create(KeycloakSession session) {
            //認証をするクラスを作成
            return SINGLETON;
        }
    
        private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
                AuthenticationExecutionModel.Requirement.REQUIRED,
                AuthenticationExecutionModel.Requirement.ALTERNATIVE,
                AuthenticationExecutionModel.Requirement.DISABLED
        };
        @Override
        public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
            return REQUIREMENT_CHOICES;
        }
    
        @Override
        public boolean isUserSetupAllowed() {
            return true;
        }
    
        @Override
        public boolean isConfigurable() {
            return true;
        }
    
        @Override
        public List<ProviderConfigProperty> getConfigProperties() {
            return configProperties;
        }
        private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
    
        @Override
        public String getHelpText() {
            return "aaa";
        }
    
        @Override
        public String getDisplayType() {
            return "Test";
        }
    
        @Override
        public String getReferenceCategory() {
            return "Test";
        }
    
        @Override
        public void init(Config.Scope config) {
    
        }
    
        @Override
        public void postInit(KeycloakSessionFactory factory) {
    
        }
    
        @Override
        public void close() {
    
        }
    }
    

テンプレートファイル:

 認証画面のテンプレートです

  • test.ftlファイル

    <!DOCTYPE html>
    
    <html>
    <head>
        <title>Welcome</title>
    
        <meta charset="utf-8">
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <meta name="robots" content="noindex, nofollow">
    
    </head>
    
    <body>
        <form id="kc-totp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
            <div class="${properties.kcFormGroupClass!}">
                <div class="${properties.kcLabelWrapperClass!}">
                    <label for="totp" class="${properties.kcLabelClass!}">input: aaa</label>
                </div>
    
                <div class="${properties.kcInputWrapperClass!}">
                    <input id="totp" name="answer" type="text" class="${properties.kcInputClass!}" />
                </div>
            </div>
            <button>push</button>
            <div class="${properties.kcFormGroupClass!}">
                <div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
                    <div class="${properties.kcFormOptionsWrapperClass!}">
                    </div>
                </div>
                <div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
                </div>
            </div>
        </form>
    </body>
    

その他のファイル:

  • pom.xmlファイル

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
        <parent>
            <artifactId>keycloak-examples-providers-parent</artifactId>
            <groupId>org.keycloak</groupId>
            <version>22.0.1</version>
        </parent>
    
        <name>Authenticator Example</name>
        <description/>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>test</artifactId>
        <packaging>jar</packaging>
    
        <dependencies>
            <dependency>
                <groupId>org.keycloak</groupId>
                <artifactId>keycloak-core</artifactId>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.keycloak</groupId>
                <artifactId>keycloak-server-spi</artifactId>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.keycloak</groupId>
                <artifactId>keycloak-server-spi-private</artifactId>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.jboss.logging</groupId>
                <artifactId>jboss-logging</artifactId>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.keycloak</groupId>
                <artifactId>keycloak-services</artifactId>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    
        <build>
            <finalName>test</finalName>
            <plugins>
                <plugin>
                    <groupId>org.wildfly.plugins</groupId>
                    <artifactId>wildfly-maven-plugin</artifactId>
                    <configuration>
                        <skip>false</skip>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    
  • org.keycloak.authentication.AuthenticatorFactoryファイル

    org.keycloak.examples.authenticator.TestAuthenticatorFactory  
    

実装手順:

 認証SPIの作成からKeycloakで動かすまでの手順です

実装の流れ:

  1. インターフェース実装
  2. 認証SPIをビルド
  3. Keycloakに追加
  4. サーバの設定

手順:

  1. インターフェース実装
     今回は、Keycloakの認証SPIのサンプルプログラム(SecretQuestion)があるディレクトリを使っています
     サンプルプログラムに関係しているファイルは削除しているので、それがまだある場合は移動させるか、
     別のディレクトリで作業するといいと思います

    1. ターミナルを開いて、Gitから持ってきたKeycloak(ソースコードの方)のディレクトリに移動
       $KEYCLOAK_HOME/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator
       ( $KEYCLOAK_HOME はKeycloakのソースコードまでパスに置き換えてほしいです)

    2. 各インターフェースを実装したクラスファイルを作成
      (今回作成したのはTestAuthenticator, TestAuthenticatorFactoryです)

    • Authenticator
    • AuthenticatorFactory
  2. 認証SPIをビルド

    1. ディレクトリに移動
       $KEYCLOAK_HOME/examples/providers/authenticator/src/main/resources/META-INF/service/

    2. クラスを定義するファイルを作成
      org.keycloak.authentication.AuthenticatorFactory
       中身は作成したFactoryクラスの名前を指定する
      中身:
        org.keycloak.examples.authenticator.TestAuthenticatorFactory

    3. ディレクトリに移動
       $KEYCLOAK_HOME/examples/providers/authenticator/src/main/resources/theme-resources/

    4. 使用するテンプレートファイルを作成
       test.ftl

    5. ディレクトリに移動
       $KEYCLOAK_HOME/examples/providers/authenticator

    6. 作成した認証SPIをビルドするのに使うpom.xmlを作成
      (今回はKeycloakのサンプルプログラムにあったもの編集して使いました)
       編集箇所:
       <finalName>test</finalName>

    7. コマンドを実行して認証SPIを作成
       mvn clean install
       ファイルの配置はこんな感じになる

    • $KEYCLOAK_HOME/examples/providers/authenticator
      • pom.xml
      • src
      • (target)
    • $KEYCLOAK_HOME/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/
      • TestAuthenticator.java
      • TestAuthenticatorFactory.java
    • $KEYCLOAK_HOME/examples/providers/authenticator/src/main/java/resources/META-INF/services/
      • org.keycloak.authentication.AuthenticatorFactory
    • $KEYCLOAK_HOME/examples/providers/authenticator/src/main/java/resources/theme-resources/templates/
      • test.tfl
  3. Keycloakに追加

    1. 以下のディレクトリに 〜.jar、〜-source.jarが作成される
       $KEYCLOAK_HOME/examples/providers/authenticator/target

    2. できたファイルを、Keycloak(ソースコードじゃない方)のprovidersディレクトリに移す
       $KEYCLOAK_HOME/keycloak/providers

    3. Keycloakをビルドする
       $KEYCLOAK_HOME/bin/kc.sh build

    4. Keycloakサーバを起動する
       kc.sh start

  4. サーバの設定

    1. ブラウザからKeycloakにアクセス、管理コンソールにログイン
      'http://IPアドレス:8080'
    2. Provider infoに作成した認証のIDがあるか確認
    3. Create realmから新しくレルムを作成
    4. メニューのUsersを開き、Add userを押してユーザを作成

    5. メニューのAuthenticationからFlowsタブを開く

    6. 名前がbrowserのものを探し、右側の縦の3点を押し、Duplicateを押して複製
    7. 複製したものの右側の縦の3点を押し、Bind flowを押す
    8. 複製したものを開く
    9. Add stepから作成したものを探して追加する
    10. 追加したら実行要件をRequiredにする
    11. 設定したらブラウザでアカウント管理画面を開く
      http://IPアドレス:8080/realms/レルム名/account
    12. 作成した認証画面が表示される

実装している時に起きた問題:

 実装している中で起きた問題と解決した方法についてです
 解決方法を試すとうまく動くことがあるかもしれないです

  • 認証SPIがKeycloakに追加されない
     作成した認証SPIをKeycloakサーバのprovidersディレクトリに配置しても
     管理コンソールのプロバイダ一覧に表示されない
    解決方法:
     認証SPIのサンプルプログラム(SecretQuestion)がprovidersディレクトリに置いてある場合は、
     それを削除してからKeycloakをビルドすると、認識されるようになるかもしれないです

  • 認証の処理が飛ばされる
     Keycloakの認証フローにAdd stepから認証を追加しても、
     アカウント管理画面からログインした時に、追加したものが飛ばされて次の認証にすぐに切り替わる
    解決方法:
     Authenticatorインターフェースを実装するクラスで、requiresUserメソッドがtrueを返す場合は、
     メソッドがtrueを返さないと認証が飛ばされるみたいでした

  • 認証の時に「Invalid username or password.」の画面が表示される
     認証フローの設定で、ユーザ認証(User Password Form等)の前に作成した認証を配置した時、
     アカウント画面からログインしようとするとエラー画面が表示される
    解決方法:
     Authenticatorを実装するクラスのrequiresUserメソッドで
     falseを返すようにするとユーザ認証の前に配置しても動くようになりました

まとめ:

 Keycloakの認証SPIを実装した手順についてまとめました
 インターフェースを実装するだけで簡単に作ることができます
 また、今回は単純な認証方法でしたが、他にもユーザに必要な操作を要求する(パスワードリセット等)認証も、
 他のインターフェースを実装することで作れるみたいでした

参考にしたところ:

コメントを追加

プレーンテキスト

  • HTMLタグは利用できません。
  • 行と段落は自動的に折り返されます。
  • ウェブページのアドレスとメールアドレスは自動的にリンクに変換されます。
CAPTCHA
この質問はあなたが人間の訪問者であるかどうかをテストし、自動化されたスパム送信を防ぐためのものです。
画像
1
2
3
4
5