blog

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 の作成と設定について記載します。

【広告】

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です