1. 序章

このチュートリアルでは、人気のあるオープンソースのID管理ソリューションである Keycloak にカスタムプロバイダーを追加して、既存または非標準のユーザーストアで使用できるようにする方法を示します。

2. Keycloakを使用したカスタムプロバイダーの概要

すぐに使用できるKeycloakは、SAML、OpenID Connect、OAuth2などのプロトコルに基づいたさまざまな標準ベースの統合を提供します。 この組み込み機能は非常に強力ですが、それだけでは不十分な場合もあります。 特にレガシーシステムが関係している場合の一般的な要件は、それらのシステムのユーザーをKeycloakに統合することです。 これと同様の統合シナリオに対応するために、Keycloakはカスタムプロバイダーの概念をサポートしています。

カスタムプロバイダーは、Keycloakのアーキテクチャで重要な役割を果たします。 ログインフロー、認証、承認などのすべての主要な機能には、対応するサービスプロバイダーインターフェイスがあります。 このアプローチにより、これらのサービスのカスタム実装をプラグインすることができ、Keycloakはそれを独自のサービスの1つとして使用します。

2.1. カスタムプロバイダーの展開と検出

最も単純な形式では、カスタムプロバイダーは1つ以上のサービス実装を含む単なる標準のjarファイルです。起動時に、Keycloakはクラスパスをスキャンし、標準のjava.utilを使用して利用可能なすべてのプロバイダーを選択します。 ServiceLoaderメカニズム。 つまり、提供する特定のサービスインターフェイスにちなんで名付けられたファイルをjarの META-INF / services フォルダーに作成し、その中に実装の完全修飾名を入れるだけです。 。

しかし、Keycloakにどのようなサービスを追加できますか? Keycloakの管理コンソールで利用できるサーバー情報ページにアクセスすると、かなりの数のページが表示されます。

この図では、左側の列は特定のサービスプロバイダーインターフェイス(略してSPI)に対応し、右側の列はその特定のSPIで使用可能なプロバイダーを示しています。

2.2. 利用可能なSPI

Keycloakのメインドキュメントには、次のSPIがリストされています。

  • org.keycloak.authentication.AuthenticatorFactory :ユーザーまたはクライアントアプリケーションを認証するために必要なアクションとインタラクションフローを定義します
  • org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory / auth / realms / master / login-actions /action-tokenエンドポイントに到達したときにKeycloakが実行するカスタムアクションを作成できます。 例として、このメカニズムは標準のパスワードリセットフローの背後にあります。 電子メールに含まれるリンクには、そのようなアクショントークンが含まれています
  • org.keycloak.events.EventListenerProviderFactory :Keycloakイベントをリッスンするプロバイダーを作成します。 EventType Javadocページには、プロバイダーが処理できるカスタムの使用可能なイベントのリストが含まれています。 このSPIを使用する一般的な使用法は、監査データベースの作成です。
  • org.keycloak.adapters.saml.RoleMappingsProvider :外部IDプロバイダーから受け取ったSAMLロールをKeycloakのロールにマップします。 このマッピングは非常に柔軟であり、特定のレルムのコンテキストで役割の名前を変更、削除、および/または追加することができます
  • org.keycloak.storage.UserStorageProviderFactory :Keycloakがカスタムユーザーストアにアクセスできるようにします
  • org.keycloak.vault.VaultProviderFactory :カスタムボールトを使用してレルム固有のシークレットを保存できるようにします。 これらには、暗号化キー、データベースクレデンシャルなどの情報を含めることができます。

現在、このリストは、利用可能なすべてのSPIを網羅しているわけではありません。これらは、最もよく文書化されており、実際には、カスタマイズが必要になる可能性が最も高くなります。

3. カスタムプロバイダーの実装

この記事の紹介で述べたように、プロバイダーの例では、読み取り専用のカスタムユーザーリポジトリでKeycloakを使用できます。 たとえば、この場合、このユーザーリポジトリは、いくつかの属性を持つ単なる通常のSQLテーブルです。

create table if not exists users(
    username varchar(64) not null primary key,
    password varchar(64) not null,
    email varchar(128),
    firstName varchar(128) not null,
    lastName varchar(128) not null,
    birthDate DATE not null
);

このカスタムユーザーストアをサポートするには、 UserStorageProviderFactory SPIを実装し、既存のKeycloakインスタンスにデプロイする必要があります。

ここで重要なのは読み取り専用部分です。つまり、ユーザーは自分の資格情報を使用してKeycloakにログインできますが、パスワードなどのカスタムストアの情報を変更することはできません。[ X214X]ただし、これは実際には双方向の更新をサポートしているため、Keycloakの制限ではありません。 組み込みのLDAPプロバイダーは、この機能をサポートするプロバイダーの良い例です。

3.1. プロジェクトの設定

カスタムプロバイダープロジェクトは、jarファイルを作成する通常のMavenプロジェクトです。プロバイダーが通常のKeycloakインスタンスにコンパイル、デプロイ、再起動するという時間のかかるサイクルを回避するために、優れたトリックを使用します。 :テスト時間の依存関係として、プロジェクトにKeycloakを埋め込みます。

SpringBootアプリケーションにKeycloackを埋め込む方法についてはすでに説明したので、ここではその方法について詳しく説明しません。 この手法を採用することで、開始時間が短縮され、ホットリロード機能が提供され、開発者のエクスペリエンスがよりスムーズになります。 ここでは、サンプルのSpringBootアプリケーションを再利用して、カスタムプロバイダーから直接テストを実行するため、テストの依存関係として追加します。

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-core</artifactId>
    <version>12.0.2</version>
</dependency>

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-server-spi</artifactId>
    <version>12.0.2</version>
</dependency>

<dependency>
    <groupId>com.baeldung</groupId>
    <artifactId>oauth-authorization-server</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <scope>test</scope>
</dependency>

keycloak-coreおよびkeycloak-server-spiKeycloakの依存関係には最新の11シリーズバージョンを使用しています。

ただし、 oauth-authorization-server の依存関係は、BaeldungのSpringSecurityOAuthリポジトリからローカルに構築する必要があります。

3.2. UserStorageProviderFactoryの実装

UserStorageProviderFactory 実装を作成してプロバイダーを開始し、Keycloakで検出できるようにします。

このインターフェイスには11個のメソッドが含まれていますが、実装する必要があるのはそのうちの2つだけです。

  • getId():Keycloakが管理ページに表示するこのプロバイダーの一意の識別子を返します。
  • create():実際のプロバイダー実装を返します。

Keycloakは、トランザクションごとにcreate()メソッドを呼び出し、KeycloakSessionとComponentModelを引数として渡します。 ここで、トランザクションとは、ユーザーストアへのアクセスを必要とするアクションを意味します。 代表的な例はログインフローです。ある時点で、Keycloakは特定のレルムに対して構成されたすべてのユーザーストレージを呼び出して、資格情報を検証します。 したがって、 create()メソッドは常に呼び出されるため、この時点ではコストのかかる初期化アクションを実行しないようにする必要があります。

とはいえ、実装は非常に簡単です。

public class CustomUserStorageProviderFactory
  implements UserStorageProviderFactory<CustomUserStorageProvider> {
    @Override
    public String getId() {
        return "custom-user-provider";
    }

    @Override
    public CustomUserStorageProvider create(KeycloakSession ksession, ComponentModel model) {
        return new CustomUserStorageProvider(ksession,model);
    }
}

プロバイダーIDに「custom-user-provider」を選択しました。create()実装は、UserStorageProvider実装の新しいインスタンスを返すだけです。 ここで、サービス定義ファイルを作成してプロジェクトに追加することを忘れてはなりません。 このファイルは、 org.keycloak.storage.UserStorageProviderFactory という名前で、最終的なjarの META-INF /servicesフォルダーに配置する必要があります。

標準のMavenプロジェクトを使用しているため、これは src / main / resources / META-INF /servicesフォルダーに追加することを意味します。

このファイルの内容は、SPI実装の完全修飾名です。

# SPI class implementation
com.baeldung.auth.provider.user.CustomUserStorageProviderFactory

3.3. UserStorageProviderの実装

一見すると、UserStorageProviderの実装は期待どおりに見えません。 コールバックメソッドがいくつか含まれていますが、実際のユーザーには関係ありません。 この理由は、Keycloakは、プロバイダーが特定のユーザー管理の側面をサポートする他のミックスインインターフェイスも実装することを期待しているためです。

利用可能なインターフェースの完全なリスト Keycloakのドキュメントで入手できます 、と呼ばれる場所プロバイダーの機能。 単純な読み取り専用プロバイダーの場合、実装する必要がある唯一のインターフェースはUserLookupProviderです。 ルックアップ機能のみを提供します。つまり、Keycloakは必要に応じてユーザーを内部データベースに自動的にインポートします。 ただし、元のユーザーのパスワードは認証に使用されません。 そのためには、CredentialInputValidatorも実装する必要があります。

最後に、一般的な要件は、Keycloakの管理インターフェースのカスタムストアに既存のユーザーを表示する機能です。 これには、さらに別のインターフェースUserQueryProviderを実装する必要があります。 これはいくつかのクエリメソッドを追加し、ストアのDAOとして機能します。

したがって、これらの要件を考慮すると、これが実装の外観です。

public class CustomUserStorageProvider implements UserStorageProvider, 
  UserLookupProvider,
  CredentialInputValidator, 
  UserQueryProvider {
  
    // ... private members omitted
    
    public CustomUserStorageProvider(KeycloakSession ksession, ComponentModel model) {
      this.ksession = ksession;
      this.model = model;
    }

    // ... implementation methods for each supported capability
}

コンストラクターに渡された値を保存していることに注意してください。 これらが実装でどのように重要な役割を果たすかについては、後で説明します。

3.4. UserLookupProviderの実装

Keycloakは、このインターフェイスのメソッドを使用して、 id 、ユーザー名、または電子メールを指定してUserModelインスタンスを回復します。 この場合、idはこのユーザーの一意の識別子であり、次のようにフォーマットされます。’f:’ unique_id ‘:’ external_id

  • 「f:」は、これがフェデレーションユーザーであることを示す固定プレフィックスです。
  • unique_id は、ユーザーのKeycloakのIDです。
  • external_id は、特定のユーザーストアで使用されるユーザー識別子です。 この場合、それはusername列の値になります

getUserByUsername()から始めて、このインターフェイスのメソッドを実装してみましょう。

@Override
public UserModel getUserByUsername(String username, RealmModel realm) {
    try ( Connection c = DbUtil.getConnection(this.model)) {
        PreparedStatement st = c.prepareStatement(
          "select " +
          "  username, firstName, lastName, email, birthDate " + 
          "from users " + 
          "where username = ?");
        st.setString(1, username);
        st.execute();
        ResultSet rs = st.getResultSet();
        if ( rs.next()) {
            return mapUser(realm,rs);
        }
        else {
            return null;
        }
    }
    catch(SQLException ex) {
        throw new RuntimeException("Database error:" + ex.getMessage(),ex);
    }
}

予想どおり、これは提供されたusernameを使用して情報を検索する単純なデータベースクエリです。 説明が必要な2つの興味深い点があります。DbUtil.getConnection() mapUser()です。

DbUtil は、コンストラクターで取得したComponentModelに含まれる情報からJDBCConnectionを返すヘルパークラスです。 詳細については後で説明します。

mapUser()の場合、その役割は、ユーザーデータを含むデータベースレコードをUserModelインスタンスにマップすることです。 UserModel は、Keycloakから見たユーザーエンティティを表し、読み取りメソッドがあります。その属性。 ここで利用できるこのインターフェースの実装は、Keycloakが提供するAbstractUserAdapterクラスを拡張します。 また、実装に Builder 内部クラスを追加したため、 mapUser()UserModelインスタンスを簡単に作成できます。

private UserModel mapUser(RealmModel realm, ResultSet rs) throws SQLException {
    CustomUser user = new CustomUser.Builder(ksession, realm, model, rs.getString("username"))
      .email(rs.getString("email"))
      .firstName(rs.getString("firstName"))
      .lastName(rs.getString("lastName"))
      .birthDate(rs.getDate("birthDate"))
      .build();
    return user;
}

同様に、他の方法は基本的に上記と同じパターンに従うため、詳細には説明しません。 プロバイダーのコードを参照し、すべてのgetUserByXXXおよびsearchForUserメソッドを確認してください。

3.5. 接続の取得

それでは、 DbUtil.getConnection()メソッドを見てみましょう。

public class DbUtil {

    public static Connection getConnection(ComponentModel config) throws SQLException{
        String driverClass = config.get(CONFIG_KEY_JDBC_DRIVER);
        try {
            Class.forName(driverClass);
        }
        catch(ClassNotFoundException nfe) {
           // ... error handling omitted
        }
        
        return DriverManager.getConnection(
          config.get(CONFIG_KEY_JDBC_URL),
          config.get(CONFIG_KEY_DB_USERNAME),
          config.get(CONFIG_KEY_DB_PASSWORD));
    }
}

ComponentModel は、作成に必要なすべてのパラメーターがある場所であることがわかります。 しかし、Keycloakは、カスタムプロバイダーが必要とするパラメーターをどのように知るのでしょうか。 この質問に答えるには、CustomUserStorageProviderFactoryに戻る必要があります。

3.6. 構成メタデータ

CustomUserStorageProviderFactoryの基本コントラクトであるUserStorageProviderFactoryには、Keycloakが構成プロパティのメタデータをクエリし、割り当てられた値を検証できるようにするメソッドが含まれています。 この例では、JDBC接続を確立するために必要ないくつかの構成パラメーターを定義します。 このメタデータは静的であるため、コンストラクターで作成し、 getConfigProperties()は単にそれを返します。

public class CustomUserStorageProviderFactory
  implements UserStorageProviderFactory<CustomUserStorageProvider> {
    protected final List<ProviderConfigProperty> configMetadata;
    
    public CustomUserStorageProviderFactory() {
        configMetadata = ProviderConfigurationBuilder.create()
          .property()
            .name(CONFIG_KEY_JDBC_DRIVER)
            .label("JDBC Driver Class")
            .type(ProviderConfigProperty.STRING_TYPE)
            .defaultValue("org.h2.Driver")
            .helpText("Fully qualified class name of the JDBC driver")
            .add()
          // ... repeat this for every property (omitted)
          .build();
    }
    // ... other methods omitted
    
    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return configMetadata;
    }

    @Override
    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config)
      throws ComponentValidationException {
       try (Connection c = DbUtil.getConnection(config)) {
           c.createStatement().execute(config.get(CONFIG_KEY_VALIDATION_QUERY));
       }
       catch(Exception ex) {
           throw new ComponentValidationException("Unable to validate database connection",ex);
       }
    }
}

validateConfiguration()では、提供されたものがRealmに追加されたときに渡されたパラメーターを検証するために必要なすべてのものを取得します。 この例では、この情報を使用してデータベース接続を確立し、検証クエリを実行します。 何か問題が発生した場合は、 ComponentValidationException をスローして、パラメーターが無効であることをKeycloakに通知します。

さらに、ここには示されていませんが、onCreated()メソッドを使用して、管理者がプロバイダーをRealmに追加するたびに実行されるロジックをアタッチすることもできます。 これにより、1回限りの初期化時間ロジックを実行して、ストアを使用できるように準備できます。これは、特定のシナリオで必要になる場合があります。 たとえば、このメソッドを使用してデータベースを変更し、特定のユーザーがすでにKeycloakを使用しているかどうかを記録する列を追加できます。

3.7. CredentialInputValidator実装

このインターフェースには、ユーザーの資格情報を検証するメソッドが含まれています。 Keycloakはさまざまなタイプのクレデンシャル(パスワード、OTPトークン、X.509証明書など)をサポートしているため、プロバイダーは supportsCredentialType()およびで特定のタイプをサポートするかどうかを通知する必要があります。 isConfiguredFor()の特定のレルムのコンテキストで構成されています。

この例では、パスワードをサポートしているだけで、追加の構成は必要ないため、後者のメソッドを前者に委任できます。

@Override
public boolean supportsCredentialType(String credentialType) {
    return PasswordCredentialModel.TYPE.endsWith(credentialType);
}

@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
    return supportsCredentialType(credentialType);
}

実際のパスワード検証は、 isValid()メソッドで行われます。

@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {
    if(!this.supportsCredentialType(credentialInput.getType())) {
        return false;
    }
    StorageId sid = new StorageId(user.getId());
    String username = sid.getExternalId();
    
    try (Connection c = DbUtil.getConnection(this.model)) {
        PreparedStatement st = c.prepareStatement("select password from users where username = ?");
        st.setString(1, username);
        st.execute();
        ResultSet rs = st.getResultSet();
        if ( rs.next()) {
            String pwd = rs.getString(1);
            return pwd.equals(credentialInput.getChallengeResponse());
        }
        else {
            return false;
        }
    }
    catch(SQLException ex) {
        throw new RuntimeException("Database error:" + ex.getMessage(),ex);
    }
}

ここで、議論する価値のあるいくつかのポイントがあります。 まず、KeycloakのIDから初期化された StorageId オブジェクトを使用して、UserModelから外部IDを抽出していることに注目してください。 このIDがよく知られた形式であるという事実を利用して、そこからユーザー名を抽出することもできますが、ここで安全にプレイし、この知識をKeycloakが提供するクラスにカプセル化することをお勧めします。

次に、実際のパスワード検証があります。 単純で、当然のことながら、非常に安全でないデータベースの場合、パスワードのチェックは簡単です。データベースの値を、 getChallengeResponse()から入手できるユーザー指定の値と比較するだけで、完了です。 もちろん、実際のプロバイダーでは、データベースからハッシュ情報に基づくパスワードとソルト値を生成し、ハッシュを比較するなど、さらにいくつかの手順が必要になります。

最後に、ユーザーストアには通常、パスワードに関連付けられたライフサイクルがあります。最大経過時間、ブロックされたステータス、非アクティブなステータスなどです。 とにかく、プロバイダーを実装する場合、 isValid()メソッドがこのロジックを追加する場所です。

3.8. UserQueryProviderの実装

UserQueryProvider 機能インターフェースは、プロバイダーがストア内のユーザーを検索できることをKeycloakに通知します。 この機能をサポートすることで、管理コンソールでユーザーを表示できるようになるため、これは便利です。

このインターフェースのメソッドには、 getUsersCount()、ストア内のユーザーの総数を取得する、およびいくつかの getXXX()および searchXXX()メソッドが含まれます。 このクエリインターフェイスは、ユーザーだけでなくグループの検索もサポートしますが、今回は取り上げません。

これらのメソッドの実装は非常に似ているので、そのうちの1つ searchForUser()を見てみましょう。

@Override
public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
    try (Connection c = DbUtil.getConnection(this.model)) {
        PreparedStatement st = c.prepareStatement(
          "select " + 
          "  username, firstName, lastName, email, birthDate " +
          "from users " + 
          "where username like ? + 
          "order by username limit ? offset ?");
        st.setString(1, search);
        st.setInt(2, maxResults);
        st.setInt(3, firstResult);
        st.execute();
        ResultSet rs = st.getResultSet();
        List<UserModel> users = new ArrayList<>();
        while(rs.next()) {
            users.add(mapUser(realm,rs));
        }
        return users;
    }
    catch(SQLException ex) {
        throw new RuntimeException("Database error:" + ex.getMessage(),ex);
    }
}

ご覧のとおり、ここでは特別なことは何もありません。通常のJDBCコードだけです。 言及する価値のある実装上の注意: UserQueryProvider メソッドは通常、ページバージョンと非ページバージョンで提供されます。 ユーザーストアには多数のレコードが含まれる可能性があるため、ページングされていないバージョンは、適切なデフォルトを使用して、ページングされたバージョンに単純に委任する必要があります。 さらに良いことに、「適切なデフォルト」とは何かを定義する構成パラメーターを追加できます。

4. テスト

プロバイダーを実装したので、埋め込まれたKeycloakインスタンスを使用してローカルでテストします。 プロジェクトのコードには、Keycloakとカスタムユーザーデータベースをブートストラップし、1時間スリープする前にコンソールにアクセスURLを出力するために使用したライブテストクラスが含まれています。

この設定を使用すると、ブラウザで印刷されたURLを開くだけで、カスタムプロバイダーが意図したとおりに機能することを確認できます。

管理コンソールにアクセスするには、管理者の資格情報を使用します。管理者の資格情報は、application-test.ymlファイルを確認することで取得できます。 ログインしたら、「サーバー情報」ページに移動しましょう。

[プロバイダー]タブでは、カスタムプロバイダーが他の組み込みストレージプロバイダーと一緒に表示されます。

Baeldungレルムがすでにこのプロバイダーを使用していることも確認できます。 このために、左上のドロップダウンメニューでそれを選択し、ユーザーフェデレーションページに移動できます。

次に、このレルムへの実際のログインをテストしてみましょう。 ユーザーがデータを管理できるレルムのアカウント管理ページを使用します。 ライブテストでは、スリープ状態になる前にこのURLを印刷するため、コンソールからコピーしてブラウザのアドレスバーに貼り付けるだけです。

テストデータには、user1、user2、user3の3人のユーザーが含まれています。 それらすべてのパスワードは同じです:「changeit」。 ログインに成功すると、インポートされたユーザーのデータを表示するアカウント管理ページが表示されます。

ただし、データを変更しようとすると、エラーが発生します。 私たちのプロバイダーは読み取り専用であるため、これは予想されることであり、Keycloakはそれを変更することを許可していません。 双方向同期のサポートはこの記事の範囲を超えているため、現時点ではそのままにしておきます。

5. 結論

この記事では、具体的な例としてユーザーストレージプロバイダーを使用して、Keycloakのカスタムプロバイダーを作成する方法を示しました。 例の完全なソースコードは、GitHubにあります。