タグ: Keycloak

  • Keycloakカスタマイズ アクセストークンにデバイスID(claim)を追加したい(2) Authenticator編

    前回はカスタムMapper の作成と登録について記載しました。
    今回はカスタムAuthenticator についてです。
    認可URL に追加されたパラメータ device_id=xxx の値を読み取り、クライアントセッションに格納する処理になります。

    Authenticator の作成

    前回作成したJava プロジェクトにクラスDeviceIdAuthenticatorFactory とDeviceIdAuthenticator を追加します。
    DeviceIdAuthenticatorFactory は特筆すべきことはありません。こちらのサンプル(再掲)を参考にしてください。
    DeviceIdAuthenticator はインターフェースAuthenticator を実装します。
    重要なのはこの部分です。

    Java
    @Override
    public void authenticate(AuthenticationFlowContext context) {
        // パラメータからdevice_id を取得
        String deviceId = context.getHttpRequest().getUri().getQueryParameters().getFirst("device_id");
        if (deviceId != null && !deviceId.isBlank()) {
            // クライアントセッション(デバイス毎のセッション)にdevice_id を保存
            logger.debugf("Device ID captured in authenticator: %s", deviceId);
            context.getAuthenticationSession().setClientNote("device_id", deviceId);
            context.success();
        } else {
            // 取得できなければエラーとして認証処理を中断させる
            logger.warn("Missing device_id parameter in authentication request");
            context.failure(AuthenticationFlowError.INVALID_CREDENTIALS);
        }
    }

    また、Mapper と同じように、作成したFactory クラス完全名を META-INF/services/org.keycloak.authentication.AuthenticatorFactory に記載します。

    com.example.sample.keycloak.DeviceIdAuthenticatorFactory

    jar の作成やKeycloak へのインストール手順は前回と同じです。

    Keycloak のAuthentication flow へ登録

    Keycloak ではログイン処理がAuthentication flow と呼ばれるステップで構成されています。一から作成するのは大変なので、browser の認証フローをコピーしたものをカスタマイズしてきます。

    サイドメニューのAuthentication からbrowser の右側のメニューよりDuplicate を選びます。

    Name をbrowser with device-id とします。

    これで認証フローのコピーが作成されました。

    上の画像は最上位のトグルを閉じた状態になっています。

    Authentication flow の簡単な説明

    Requirement としてRequired(必須)、Alternative(いずれか1つ必須)、Disabled(無効)の3つがあります。
    現在の状態は、Cookie、Identity Provider Redirector、Organization、forms が同じレベル(最上位)に並んでおり、いずれか1つ成功すれば次に進むという意味になります。
    Kerberos はDisabled なので無効です。
    Cookie は一度ログインされたセッションを持っていた場合、ログイン画面を出さずに認証が通ったものとして扱うものです。
    Identity Provider Redirector はGoogle sign in 等の外部OIDC 連携を行っている場合にリダイレクトを行うものです。今回は使っていないのでDisable にしても良いです。
    Organization は組織ごとにログイン画面を変更する場合に使うそうです。こちらも今回は使っていないのでDisable にしても良いです。
    forms はログイン画面を表示します。同じレベルでAlternative として登録されているCookie が満たされなかった場合に、ログイン画面が表示されます。今回はここのサブフローとして作成したカスタムAuthenticator を追加します。

    Authentication flow へ登録つづき

    画面上部のAdd execution をクリックします。

    device で検索を行い、Device ID Capture Authenticator にチェックを入れてAdd をクリックします。

    追加直後の画面です。閉じていたトグルが全て開いた状態になってしまいます。(少し焦ります)

    再度トグルを閉じると、一番下に追加されていることが分かります。

    このままでは、最上位にRequiredとして配置されいるため、先ほど紹介したCookie が使えない問題が起きます。(1敗)
    もう少し細かくお話しすると、この状態ではセッションが確立されていたとしてもdevice_id をパラメータとして毎回送付しなければエラーになってしまいます。Cookie がAlternative でDevice ID がRequired として同列で並ぶため、Cookie が成功してもDevice ID が必須となるためです。

    左側の点々をドラッグできるので、なんとかしてforms 配下の一番上にDevice ID を配置します。
    ドラッグドロップが多少不安定(バグ?)なので何度か試行錯誤が必要です。ctrl キーとマイナスを何度か押して、画面を大きくすると動かしやすいかもしれません。
    最終的に下の画像の構成にします。

    最後にAuthentication 一覧からbrowser with device-id をBrowser flow としてBind します。

    Authenticator 一覧画面を見ると、used by にチェックが入っています。

    以上でKeycloak の設定は完了です。

    取得されたアクセストークンを解析

    前回と同じように、認証完了後に取得できるトークンを解析します。

    device_id としてswift から送付した文字列deivce-ios が追加されていることが分かります。

    感想

    私はAuthentication flow のRequired とAlternative の意味を分からずに、最上位にRequired として登録していたため、いつまでもCookie が通らずに苦戦しました。
    ちゃんと理解しないとダメですね。。。(笑)

    【広告】

  • Keycloakカスタマイズ アクセストークンにデバイスID(claim)を追加したい(1) Mapper編

    ユーザが利用するデバイスを厳密に管理したいのでアクセストークンにデバイスID を含めるやり方を探していました。調べていくと、Keycloak にMapper とAutenticator を登録することで実現可能と分かりました。
    私は当初、管理画面の設定を少し変更すれば実現できるかと思いましたが実際はもう少し複雑で、Java のコードを書いてKeycloak に登録する必要がありました。
    この記事ではJava の作成手順とKeycloak への登録手順を残します。使用したKeycloak はバージョン26.4.2 です。

    認証処理の流れ

    • クライアントが認可URL に追加パラメータ device_id=xxx を送付
    • Keycloak に登録されたカスタムAuthenticator が、パラメータからdevice_id 取得しクライアントセッションに格納
    • ユーザがログインを行う
    • Keycloak に登録されたカスタムのMapper が、クライアントセッションのdevice_id をトークンに追加

    認証処理の順番とは逆になりますが、今回は実装が容易なMapper 登録について記載します。
    内容はこちらの記事を参考にさせていただきました。

    Javaプロジェクトの作成

    必要な依存関係を build.gradle.kts の形式で記載します。

    val keycloakVersion = "26.4.2" // 2025/11時点最新
    dependencies {
        compileOnly("org.keycloak:keycloak-server-spi:${keycloakVersion}")
        compileOnly("org.keycloak:keycloak-server-spi-private:${keycloakVersion}")
        compileOnly("org.keycloak:keycloak-services:${keycloakVersion}")
        compileOnly("com.google.auto.service:auto-service:$autoServiceVersion")
        annotationProcessor("com.google.auto.service:auto-service:$autoServiceVersion")
    }

    こちらにgradle のサンプルソースをおいたのでよかったら参考にしてください。

    Mapper の作成

    Mapper クラスを作成して、AbstractOIDCProtocolMapper 等を継承します。

    重要なのはこの部分です。

    Java
    /**
     * アクセストークン作成時に呼び出される
     */
    @Override
    protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) {
        // クライアントセッション(デバイス毎のセッション)からdevice_id を取得
        String deviceId = clientSessionCtx.getClientSession().getNote("device_id");
        // TODO 動作確認のため固定値を設定。本番稼働時は削除する
        if (deviceId == null) {
            deviceId = "sample-device-id";
        }
        logger.infof("device_id: %s", deviceId);
        OIDCAttributeMapperHelper.mapClaim(token, mappingModel, deviceId);
    }

    本来であればカスタマイズされたAuthenticator がクライアントセッションにdevice_id を格納するはずですが、今時点では実装していません。
    取得できない場合は、固定値sample-device-id をセットすることで動作が確認できるようにしておきます、
    device_id をmapClaim() に引き渡すことで、アクセストークンに追加されます。

    また、作成したクラス完全名を META-INF/services/org.keycloak.protocol.ProtocolMapper に記載します。
    サンプルではこのように記載しています。

    com.example.sample.keycloak.DeviceIdTokenMapper

    jar の作成

    gradle のコマンドでjar を作成します。

    ShellScript
    gradlew clean jar

    私はintelliJ が好きなので、こちらをダブルクリックして作成しています。(成果物は同じ)

    こちらにjar ができあがります。
    build/libs/sample_keycloak_mapper-0.0.1-SNAPSHOT.jar

    Keycloak へインストール

    作成されたjar をkeycloak の/providers フォルダにコピーします。
    コピー後にkeycloak を再起動します。
    うまくいけばKeycloak 起動時のコンソールログにjar を読み込んだ旨のメッセージが出るはずです。

    keycloak-1 | 2025-11-03 13:10:15,266 WARN org.keycloak.services KC-SERVICES0047: 
    device-id-token-mapper (com.example.sample.keycloak.DeviceIdTokenMapper) 
    is implementing the internal SPI protocol-mapper. This SPI is internal and may change without notice

    ※補足
    PROVIDER_ID を変更せずにパッケージを変更すると認識されない場合がありました。
    その場合は起動時にオプションを追加すると解消されました。

    ShellScript
    kc.sh start --spi-providers-reset=true

    Keycloak のClient Scope へ登録

    Keycloak の対象realm の管理画面にログインします。

    Client scopes > Create client scope

    Name をdevice-id-scope とします。他はデフォルトです。

    Mappers > Configure a new mapper

    Name とToken Claim Name にdevice_id と入力します。他はデフォルトです。

    作成されたClient Scope を対象のクライアントに割り当てます。
    Clients > 対象のクライアント(画像略) > Client Scopes > Add client scope

    device-id-scope にチェックを入れて、Add > Default を選択します。

    以上でKeycloak の設定は完了です。

    クライアントからdevice_id を送付

    認可URL にパラメータ device_id を追加して送付します。
    このようなURL になるはずです。
    7行目の追加されたdevice_id が重要な点です。

    GET https://<KEYCLOAK_HOST>/realms/<REALM_NAME>/protocol/openid-connect/auth?
     client_id=<CLIENT_ID>
     &redirect_uri=<REDIRECT_URI>
     &response_type=code
     &scope=openid%20profile
     &state=<RANDOM_STRING>
     &device_id=<DEVICE_ID>

    私はswift のAppAuth for iOS を使ってclient で作成しましたので、参考までにコードを記載しておきます。
    10行目のparams にdevice_id が追加されている点が重要です。

    Swift
    OIDAuthorizationService.discoverConfiguration(
        forIssuer: Constants.issuer
    ) { configuration, error in
        guard let configuration = configuration else {
            // TODO error handling
            return
        }
        // device-id をセット
        // TODO 実際にはUUID などを動的にセットする
        let device_id = "device-ios"
        let params = ["prompt": "login",
                 "device_id": device_id
                 ]
        let request = OIDAuthorizationRequest(
            configuration: configuration,
            clientId: Constants.keycloakClientId,
            scopes: [OIDScopeOpenID, OIDScopeProfile],
            redirectURL: Constants.redirectUrl,
            responseType: OIDResponseTypeCode,
            additionalParameters: params
        )
        guard
            let rootViewController = UIApplication.shared
                .connectedScenes.compactMap({
                    ($0 as? UIWindowScene)?.windows.first?.rootViewController
                }).first
        else {
            // TODO error handling
            return
        }
        self.currentAuthorizationFlow = OIDAuthState.authState(
            byPresenting: request,
            presenting: rootViewController
        ) { authState, error in
            Task {
                await self.handleAuthResponse(authState, error: error)
            }
        }
    }

    取得されたアクセストークンを解析

    認証完了後に取得できるトークンをhttps://jwt.io 等で解析すると、device_id が追加されていることが分かります。

    次回

    次回はAuthenticator の作成と設定について記載します。

    【広告】

  • Spring Boot + Spring Security + Keycloak でJWT の公開鍵を更新してみる

    私は、JWT 署名に使われる公開鍵を変更した場合、リソースサーバで新しい公開鍵を取得する作業が必要だと考えていました。
    しかし、結論から言うとこれは間違いでした。Spring Boot + Spring Security を使用している場合は何もせずに自動対応されます。

    本当にそうなのか、少し不安に思い検証してみました。

    Keycloak で公開鍵の追加

    Keycloak に公開鍵を追加して試してみます。

    Realm settings > Keys
    ここで鍵の一覧を確認できます。JWT の署名で必要なのはRS256 になります。既存の鍵が1つ存在します。

    ※画像は Keycloak バージョン 26.1.4 のものです。以下同様です。

    公開鍵を追加する場合、Add providers > Add provider をクリックします。

    鍵の種類を選ぶ画面が出ますので、rsa-generated を選択します。

    既存の鍵と区別するためにName をrsa-generated-test とします。

    一覧画面で追加されていることが確認できます。

    この状態で、JWKS エンドポイントをブラウザで表示してみます。

    https://<KEYCLOAK_HOST>/realms/<REALM_NAME>/protocol/openid-connect/certs

    表示されたJSON をchrome console で解析させると、kid が8wY… から始まる鍵が公開されていることが分かります。

    古い鍵のprovider のEnabled をoff に変更すると、JWKS エンドポイントからも削除されます。

    Spring Boot で検証

    以下の順序で検証してみました。

    • Spring boot を起動
    • Keycloak に新しい公開鍵の追加して、古い鍵を無効化する
    • 新しい鍵で著名されたJWT をSpring boot に送信

    結果、Spring boot を再起動することなく、新しい鍵によるJWT 検証が行われました。

    内部の挙動(推測)

    Spring boot はJWK を一定時間キャッシュします。
    JWT ヘッダーに含まれるkid に対応する公開鍵がキャッシュに存在しない場合、JWKS エンドポイントを呼び出して新たな鍵を取得した上で再度検証を行なっていると考えられます。

    便利ですね。

    【広告】