SSO
Keycloak

Keycloakのカスタマイズポイントを整理してみる

今日やること

Keycloak by OpenStandia Advent Calendar 2017 も22日目の今日は、Keycloakのカスタマイズポイントを整理してみます。:thumbsup:

説明のために、Keycloakのカスタマイズポイントを大きく以下3つに分けてみました。

  • 画面のカスタマイズ
  • 既存サービスのカスタマイズ
  • 独自のカスタマイズ

各カスタマイズポイントの説明と(できるところは)実装例なども紹介できればと思っていますので、宜しくお願い致します。:smile:

画面のカスタマイズ

Keycloakでは「Themes」機能により、Keycloak本体のファイルを直接修正せずに画面のカスタマイズをすることが可能です。

Themesについて

「Themes」機能の特徴をみてみます。

  • Keycloak本体のファイルを直接修正することなく、UIを構成する各種ファイルの上書きすることが可能です。(上書き可能なファイルは、HTMLテンプレートとなる*.ftlファイル、CSSファイル、メッセージのプロパティファイル)
  • 作成したカスタムテーマは、レルム単位で適用することが可能です。
  • テーマはKeycloakのUIタイプごとに作成し、管理コンソールによりUIタイプ別に適用可否を選択可能です。(UIタイプは、admin, account, login, email, welcomeの5つ存在します。「welcome」は未ログイン状態でアクセス可能なページであり全レルムで共通のため、管理コンソールからは設定不可で $KEYCLOAK_HOME/standalone/configuration/keycloak-server.json または $KEYCLOAK_HOME/domain/servers/{server name}/configuration/keycloak-server.json で設定する必要があります。)
  • カスタムテーマは $KEYCLOAK_HOME/themes ディレクトリにコピーすることでデプロイが可能です。JARにパッケージングしWildflyのモジュールとしてデプロイすることも可能です。

カスタムテーマのディレクトリ構成

{Mavenプロジェクト名}/src/main/resources/theme/{テーマ名}/{UIタイプ} でUIタイプごとにカスタムテーマを作成します。

※ ディレクトリコピー方式のデプロイであれば、theme 配下の {テーマ名}/{UIタイプ} だけでも良いですが、モジュールとしてデプロイするためにはJARにパッケージングする必要があるため、ここではMavenプロジェクト方式を取りました。

themes/src/main/resources
├─META-INF
│  └─keycloak-themes.json
└─theme
    └─openstandia
        ├─account
        │  ├─ (省略)
        │  └─theme.properties
        ├─admin
        │  ├─ (省略)
        │  └─theme.properties
        ├─email
        │  ├─ (省略)
        │  └─theme.properties
        ├─login
        │  ├─ (省略)
        │  └─theme.properties
        └─welcome
           ├─ (省略)
           └─theme.properties
  • keycloak-themes.json : モジュールとしてデプロイする際に必要になります。
  • theme.properties : 各UIタイプのディレクトリ直下に配置します。ベースとする親テーマやスタイル(CSS)の読込みなどを設定します。
theme.properties
parent=keycloak
import=common/keycloak
locales=en,ja
styles=lib/patternfly/css/patternfly.css lib/select2-3.4.1/select2.css css/styles.css css/main.css
  • parent : 親テーマ名を設定します。Keycloakまたはbaseが親として指定できます。(Keycloakテーマ自体はbaseを親としています)
    • Keycloakの通常のスタイルをベースにカスタマイズするのであれば、親にはkeycloakを指定。
    • keycloakのスタイルをベースにせず完全にCSSを一から定義するのであれば、親にはbaseを指定。
  • import : 共通リソース(JavascriptライブラリやCSSフレームワークのスタイル)を読み込むのに使用します。通常はcommon/keycloakとKeycloak標準のものを使えば良いです。
  • locales : サポートする言語を設定します。
  • styles : 読み込むCSSファイルを設定します。スペース区切りで複数指定が可能です。

UIタイプのディレクトリ構成

各UIタイプのディレクトリには、上書き元のテーマに合わせてディレクトリと上書きファイルを配置します。例えば、admin タイプの場合は、多言語メッセージ用にmessagesディレクトリとCSS、画像、Freemaker部分テンプレートなどを格納する resources ディレクトリを持ちます。

themes/src/main/resources/theme/openstandia/admin
├─messages
│  ├─admin-messages_ja.properties
│  ├─admin-messages_ja.properties.native
│  ├─messages_ja.properties
│  └─messages_ja.properties.native
├─resources
│  ├─css
│  │  └─main.css
│  ├─img
│  └─partials
│      └─user-detail.html
└─theme.properties

なお、上書き元がどのようなファイルを持っているかは、$KEYCLOAK_HOME/themes ディレクトリ配下に basekeycloak テーマのファイルが格納されているため、こちらを確認します。

基本的な画面カスタマイズ手順

カスタマイズする場合の基本的な手順は下記の通りです。

・テーマを新たに作成する場合

  1. themes/src/main/resources/theme/keycloak ディレクトリなどの、既存テーマ全体をコピーして任意の名前(openstandia)で新テーマディレクトリを作成すればOKです。コピー後、不要なUIタイプがあれば削除します。
  2. themes/src/main/resources/META-INF/keycloak-themes.json に新テーマを追加します。

・UIタイプを新たに作成する場合

UIタイプのディレクトリを作成するだけでOKです。UIタイプは、admin, account, login, email, welcome の5つであるため、これ以外の名前で作成しても認識されないです。

・各種リソースを上書きカスタマイズする場合

  1. 上書き元のオリジナルのリソースファイルを $KEYCLOAK_HOME/themes ディレクトリ配下から探します。
  2. 見つけたオリジナルのリソースファイルを、自身のテーマディレクトリにコピーしてカスタマイズしたい箇所を変更します。

・メッセージを差分カスタマイズする場合

  1. {UIタイプ}/messages ディレクトリに親テーマの同名のメッセージファイルを作成する。
  2. カスタマイズしたいメッセージキーのみを定義する。

メッセージ差分をカスタマイズしたテーマを適用すると、カスタマイズしたテーマ内のメッセージが優先されます。定義していないメッセージキーが読み込まれる場合は、親テーマのメッセージが自動的に参照されます。

ログイン画面のカスタマイズ

ここでは、openstandiaというカスタムテーマを作成して、その中にUIタイプ login を作成してログイン画面をカスタマイズします。

・UIタイプloginのディレクトリ構成

$KEYCLOAK_HOME/themes/openstandia/login
├─login.ftl
├─messages
│  ├─messages_en.properties
│  ├─messages_ja.properties
├─resources
│  ├─css
│  │  └─main.css
│  └─img
│      ├─favicon.ico
│      ├─logo.png
│      └─ssoidm_logo.png
└─theme.properties
  • テンプレートファイル(*.ftl)
    • login.ftl: ログイン画面
  • メッセージプロパティファイル
    • messages_{locale}.properties : ログイン画面に表示するメッセージを変更したい場合に追加します。ファイル名の{locale}は対応する言語(en:英語、ja:日本語) を表します。メッセージを変更する必要がない場合は不要です。
  • スタイルファイル
    • main.css : theme.properties で読込み設定をしたCSSファイル。CSSレベルでカスタマイズが済むレベルであれば、このファイルを作成しスタイルをカスタマイズすればOKです。
    • favicon.ico : 独自の favicon.ico を配置します。

今回作成した theme.properties(Keycloakテーマを親としています)

$KEYCLOCK_HOME/themes/openstandia/login/theme.properties
parent=keycloak
import=common/keycloak
locales=en,ja
styles=lib/patternfly/css/patternfly.css lib/zocial/zocial.css css/main.css

・テーマの反映方法

Keycloakのデフォルトでは作成したテーマを反映するにはKeycloakの再起動が必要ですが、テーマキャッシュを無効にすることで再起動なしで即時反映させることもできます。設定を変更するには、standalone.xml または standalone-ha.xmlを修正します。

            <theme>
-                <staticMaxAge>2592000</staticMaxAge>
-                <cacheThemes>true</cacheThemes>
-                <cacheTemplates>true</cacheTemplates>
+                <staticMaxAge>-1</staticMaxAge>
+                <cacheThemes>false</cacheThemes>
+                <cacheTemplates>false</cacheTemplates>
                <dir>${jboss.home.dir}/themes</dir>
            </theme>

・テーマ設定の変更&カスタムログイン画面の確認

変更前はデフォルトのログイン画面が表示されます。

Keycloakログイン_def.png

管理コンソールの「レルムの設定」ー「テーマ」設定から、ログインテーマを「openstandia」に変更します。

themeset.png

ログアウトして再度ログイン画面を表示するとカスタマイズしたログイン画面が表示されます。

login_new.png

既存サービスのカスタマイズ

Service Provider Interfasesについて

「Keycloakのカスタマイズポイントを整理する = Service Provider Interfaces(以下SPI)を整理する」 に言い換えても差し支えないくらい、カスタマイズを説明する中で欠かせない存在です。上で説明した「Themes」機能も画面を変更するという意味で分けて説明しましたが、実は「Themes」もSPIの一部です。

Keycloakにはカスタマイズしなくてもサービスが利用できるように標準のSPIが用意されています。ですが、どうしても標準のSPIだけでは実現できないこともあります。この場合、SPIを拡張して独自のプロバイダーを作成することができます。 SPIのカスタマイズはアドオン形式でできるので、Keycloak本体のファイルを直接修正する必要はありません。 ここでは、「SPIのカスタマイズ方法とデプロイ方法」と簡単に説明してから、重要な(よく使いそうな)SPIをいくつかピックアップして紹介します。ちなみに、全てのSPIを確認したい場合は「管理コンソール」の右上のメニュー「サーバ情報」ー「プロバイダー」から確認することができます。

サーバ情報_プロバイダー.png

認証機能のカスタマイズ (Authentication SPI)

管理コンソールに「認証」設定画面があります。ここでは「認証フロー」や「認証後の処理(Requiredアクション)」などを設定することができます。

認証フローの設定画面

認証.png

Requiredアクション設定画面

requiredAction.png

「認証」関わる機能を拡張したい場合は、「Authentication SPI」を利用することでカスタマイズできます。例えば、IPアドレスチェックを追加したり、初回ログイン時に秘密の質問を設定させたりすることが可能です。

カスタマイズの詳細は、Keycloak DocumentationのServer Development - Authentication SPIに記載されています。

外部認証ストア機能のカスタマイズ(User Storage SPI)

管理画面に「ユーザーフェデレーション」の設定画面があります。外部のユーザー認証ストアへ接続にすることで、Keycloak内部のユーザー認証ストアにユーザー情報がなくても、Keycloak認証を可能とする機能です。

ユーザーフェデレーション.png

標準機能で「LDAP」「Kerberos」をサポートしていますが、「User Storage SPI」カスタマイズポイントを利用することで、フェデレーション機能を拡張することができます。例えば、スクラッチアプリケーションが利用するRDBの認証データストアへのフェデレーション機能を追加することができます。

カスタマイズの詳細は、Keycloak DocumentationのServer Development - User Storage SPIに記載されています。

ユーザーフェデレーション機能の使い方については、Keycloak by OpenStandia Advent Calendar 2017 の13日(LDAP)、14日(Kerberos)目で紹介していますので、参考にしてみてください。

アイデンティティプロバイダー機能のカスタマイズ(Identity Provier SPI)

管理画面に「アイデンティティ プロバイダー」の設定画面があります。Facebook, Twitterなどを外部IdPとし、Keycloakの認証を委譲することができる機能です。(同じ機能がこのQiitaのログイン画面にも「GitHub」「Twitter」「Google」で新規登録/ログインができるボタンがあります。)

IPS.png

上記画面の通り、「GitHub」「Twitter」「Facebook」「Google」などメジャーなソーシャルプロバイダーは標準で備わっていますが、それ以外の外部IdPを利用したい場合、「Identity Provier SPI」を利用してカスタマイズすることができます。

カスタマイズの詳細は、Keycloak DocumentationのServer Development - Identity Brokering APIsに記載されています。

イベントリスナー機能のカスタマイズ(Event Listener SPI)

ユーザがログインしたり、アカウントを変更したり、トークンを取得したりすると、その操作に対するイベントが発生します。イベントの詳細は管理コンソール上で確認することもできます。

イベント.png

イベントリスナーには、Event Listener SPI というカスタマイズポイントがあり、これをカスタマイズすることで、監査ログとしてデータをプッシュしたり、登録したユーザ数に関する統計を更新したり、ログイン通知メールを送信したりすることができます。

カスタマイズの詳細は、Keycloak DocumentationのServer Development - Service Provider Interfaces(SPI)に記載されています。

SPIのカスタマイズ方法とデプロイ方法

Mavenプロジェクトを作成して、標準のSPIを継承したクラスを作成することでSPIのカスタマイズが可能です。例えば、EventListenerを拡張したい場合、以下の2クラスを作成する必要があります。

  • org.keycloak.events.EventListenerProvider インタフェースを実装したクラス : MyEventListenerProvider
  • org.keycloak.events.EventListenerProviderFactory インタフェースを実装したクラス : MyEventListenerProviderFactory

また、上記Factoryクラスを登録するためのMETA-INF/services/org.keycloak.events.EventListenerProviderFactory ファイルを作成する必要があります。中には、作成したファクトリークラス名を記述します。

META-INF/services/org.keycloak.events.EventListenerProviderFactory
org.acme.provider.MyEventListenerProviderFactory

SPIのカスタマイズ方法の詳細は、Keycloak DocumentationのServer Development - Service Provider Interfaces(SPI)に記載されています。

デプロイ方法は、これらは拡張モジュールをJARファイルにパッケージングし、$KEYCLOAK_HOME/providers にJAR(依存JARがあればそれも)を配置してKeycloakを再起動すればデプロイできます。

独自カスタマイズ

Keycloakではコア機能を拡張してカスタマイズすることで標準にはないオリジナル機能を作成することもできます。ここでは独自のカスタマイズの例として「CSVでユーザ一括登録」のカスタマイズについて説明します。

独自カスタマイズについての詳細は、Keycloak DocumentationのServer Development - Extending the Serverに記載されています。

独自カスタマイズの「CSVでユーザ一括登録」

・パッケージ構成

「CSVでユーザ一括登録」のプロジェクトのパッケージ構成です。

bulk-control-package.png

・RESTのリクエストを処理する機能

Keycloakでは、HTTPリクエストを、リソースプロバイダとリソースプロバイダファクトリーで管理しています。

BulkResourceProviderFactory : org.keycloak.services.resource.RealmResourceProviderFactory を継承した Factory クラス。このクラスのgetId()メソッドで返される文字列が、エンドポイントのパスになります。当サンプルでは、"bulk-control" の部分です。(auth/realms/{レルム名}/bulk-control/...)

BulkResourceProviderFactory
public class BulkResourceProviderFactory implements RealmResourceProviderFactory {

    public static final String ID = "bulk-control";

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

    @Override
    public RealmResourceProvider create(KeycloakSession session) {
        return new BulkResourceProvider(session);
    }
    ・・・
}

BulkResourceProvider : org.keycloak.services.resource.RealmResourceProviderを継承した Provider クラス。 getResource() メソッドで、HTTPリクエストに対応する JAX-RS リソースクラスを呼び出します。

BulkResourceProvider
public class BulkResourceProvider implements RealmResourceProvider {

    private KeycloakSession session;

    public BulkResourceProvider(KeycloakSession session) {
        this.session = session;
    }

    @Override
    public Object getResource() {
        return new BulkResource(session);
    }
    ・・・
}

BulkResource, BulkUserResource : JAX-RS リソースクラス。アノテーションにより、リクエストの仕様を設定します。当サンプルでは、アカウント登録以外のバルク処理も拡張できることを想定して、2種類のリソースクラスを用意しました。

BulkResourceで、パスにあわせて、さらにリソースクラスを振り分けられるようにしました(今回はアカウント情報のみ)

BulkResource
public class BulkResource {
・・・
/**
* ユーザ情報のCSVアップロード処理を受付ます。
*
* @return BulkUserResource からの戻り値
*/
@Path("users")
public BulkUserResource getBulkUserResourceAuthenticated() {
    return new BulkUserResource(session);
}
・・・
}

BulkUserResource : アカウント情報に対するリクエストを処理します。パスやHTTPメソッドにあわせて処理内容を振り分けることを想定しています。(今回は登録のみ)

BulkUserResource
public class BulkUserResource {
・・・
/**
* CSVアップロードによるユーザ登録処理を行います。
* リクエスト情報からアップロードされたCSV情報を取得し、DB登録処理を行います。
*
* @param uriInfo UriInfo
* @param input   MultipartFormDataInput
* @return
* @throws IOException
*/
@SuppressWarnings("unchecked")
@POST
@Path("registration")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public Response createUsers(@Context UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
    // アカウント一括登録リクエストの処理を記載
}
・・・
}

UserCsvRepresentation : アップロードされたCSVの1行分の情報を格納するためのクラス。今回、CSVをパースするためにcom.opencsvライブラリを使用しているため、ヘッダ名にあわせた getter と setter を用意しています。CSVのレイアウトにあわせてカスタマイズします。

UserCsvRepresentation
/**
* @return username
*/
public String getUserName() {
    return username;
}

/**
* @param username
*/
public void setUserName(String username) {
    this.username = username;
}

今回のサンプルのCSVレイアウトは、以下の通りです。ロール1つ、Attribute 1つが設定できるようになっていて、複数登録できるようにしたい場合、このクラスのヘッダ名定義とヘッダにあわせた getter と setter を追加修正します。

username lastName firstName email role option1 password

・DB登録処理を行う機能

DB登録処理を行うプロバイダを用意します。ここではorg.keycloak.provider.Spiorg.keycloak.provider.ProviderFactoryorg.keycloak.provider.Provider の3つを継承したクラスで構成されます。

  • BulkUserSpi : プロバイダの構成を定義するクラス。getName() メソッドで返される文字列がプロバイダ名になります。
BulkUserSpi
public class BulkUserSpi implements Spi {
    ・・・
    @Override
    public String getName() {
        return "bulkuser";
    }

    @Override
    public Class<? extends Provider> getProviderClass() {
        return BulkUserService.class;
    }

    @Override
    @SuppressWarnings("rawtypes")
    public Class<? extends ProviderFactory> getProviderFactoryClass() {
        return BulkUserServiceProviderFactory.class;
    }
}
  • BulkUserServiceProviderFactory、BulkUserServiceProviderFactoryImpl : プロバイダクラスを呼び出す。

  • BulkUserService、BulkUserServiceImpl : DB登録処理を行う実装クラス。オーバーライドが必要なメソッドはないので、必要な処理のみ実装します。

    • DB接続には、javax.persistence.EntityManager を利用します。EntityManagerはプロバイダ内のセッション情報から初期化されたクラスが取得できます。

      private EntityManager getEntityManager() {
      return session.getProvider(JpaConnectionProvider.class).getEntityManager();
      }
      
    • 各テーブルの定義や、固定のSQL文定義には、エンティティクラスを用意する必要があり、当サンプルでは、Keycloak本体の org.keycloak.models.jpa.entities.UserEntity を利用しています。

・トランザクションの制御

Keycloakでは、複数のプロバイダのトランザクションは、デフォルトではそれぞれコミットが別になるので、コミット/ロールバックをまとめて制御したい場合はorg.keycloak.models.KeycloakTransactionManagerを利用します。KeycloakTransactionManagerはセッションから取得できます。

session.getTransactionManager()

処理の最後に、コミット、例外発生時等にロールバックを明記することで、複数のトランザクションをまとめることができます。

try {
    // DB登録
    users = session.getProvider(BulkUserService.class).addUsers(users);

    // イベントログ出力
    adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(eventInfo(users)).success();

    // commit
    if (session.getTransactionManager().isActive()) {
        session.getTransactionManager().commit();
    }

    return ...
} catch(Exception e) {
    // rollback
    if (session.getTransactionManager().isActive()) {
        session.getTransactionManager().setRollbackOnly();
    }
    return ...
}

・イベントログの出力について

管理者による操作のイベントログ出力は、org.keycloak.services.resources.admin.AdminEventBuilder を利用します。/admin で呼び出される管理コンソール用の処理では、共通処理で AdminEventBuilder のインスタンスを宣言しているが、カスタムエンドポイントでは、必要な場合、独自に宣言することになります。その場合、管理者の認証情報 org.keycloak.services.resources.admin.AdminAuth が必要になります。ログイン認証処理とは異なり、ログイン中の管理者の認証情報は、アクセストークンから取得できます。

AdminAuth auth = authenticateRealmAdminRequest(session.getContext().getRequestHeaders());
this.adminEvent = new AdminEventBuilder(auth.getRealm(), auth, session, session.getContext().getConnection());
・・・
// イベントログ出力
adminEvent.operation(OperationType.CREATE)
          .resourcePath(uriInfo)
          .representation(eventInfo(users))
          .realm(realm)
          .success();

AdminEventBuilderの各メソッドについての概要

  • operation(OperationType):操作内容を設定します。用意されているOperationTypeは、CREATE, UPDATE, DELETE, ACTIONの4つ。
  • resourcePath():現在のエンドポイントのパスを文字列で設定します。引数に合わせて、3種類メソッドが用意されています。(UriInfoクラス、String等)
  • representation(Object):更新内容等のObjectを渡すと、JSON形式に変換してログに出力します。出力内容にパスワードなど、セキュリティ上出力しない情報や、ログとして不要な情報が含まれないようにObjectの内容には注意が必要です。
  • realm(realm):カスタムエンドポイントのレルム情報を設定します。
  • success():正常系のログを出力します。
  • error():エラーログを出力します。

・デプロイ方法

  • パッケージングしたJARファイル名を openstandia-keycloak-endpoint-1.0.0-SNAPSHOT.jar とし、以下のコマンドを実行してデプロイします。
$KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=jp.openstandia.keycloak.endpoint --resources=openstandia-keycloak-endpoint-1.0.0-SNAPSHOT.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-common,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services,org.keycloak.keycloak-model-jpa,org.jboss.logging,javax.ws.rs.api,javax.persistence.api,org.jboss.resteasy.resteasy-multipart-provider,org.hibernate"
  • standalone.xml、または、standalone-ha.xmlを編集して、ModuleをProviderに追加します。
    <providers>
        <provider>classpath:${jboss.home.dir}/providers/*</provider>
        <provider>module:jp.openstandia.keycloak.endpoint</provider>
    </providers>

Keycloakサーバを再起動するとモジュールが適用されます。

  • 確認方法

事前にCSVファイルを用意して、CURLを利用して以下のリクエストを送信する。

curl -i --header "Content-type: multipart/form-data" --header "Authorization: bearer eyJhbGciOiJSUz..." --request POST --form "file=@[ファイルパス]" "http://localhost:8080/auth/realms/master/bulk-control/users/registration";

※ 再デプロイ時は、一度削除してください。

$KEYCLOAK_HOME/bin/jboss-cli.sh --command="module remove --name=jp.openstandia.keycloak.endpoint"

カスタマイズする時の注意点

Red Hat Single-Sign-On利用時

Keycloakの商用版 Red Hat Single-Sign-On(以下RHSSO)を利用する時は、拡張できるSPIが限定されますので注意が必要です。(Keycloakに比べてかなり限定されます...)RHSSO利用時には、該当するバージョンの公式ドキュメントを参考にしてカスタマイズしてください。

バージョンアップ

Keycloakのバージョンアップにより、継承する元ファイルが変更されることがあります。ですので、Keycloakのバージョンアップの時は注意が必要です。

最後に

Keycloakのカスタマイズポイントについて整理してみました。良いと思ったのは、

  • Keycloakの既存のファイルに手を加えずにカスタマイズできるところ
  • JARを配置するだけでデプロイが可能なところ (例外もありますが...)

あとは、できなったカスタマイズに関するKeycloakのドキュメントもしっかりしていて、分かりやすかった印象を受けました。

では、今日はここまでです。 ありがとうございました。:bow:

参考資料