Keycloakでは、OpenID Connectで発行したアクセストークンを使ってResource Server(バックエンドAPI)の認証認可を行うことができる。
この認証認可において、Resource Serverに渡したアクセストークンが別のResource Serverの呼び出しに再利用されることを防ぐために、アクセストークン(KeycloakではJWT形式で発行される)のaudience(audクレーム)に設定する値を制御するAudience Supportという機能が提供されている。(アクセストークンに含めるaudienceを制限し、各Resource Serverでは認可時に自分のクライアントがアクセストークンのaudienceに含まれているか検証することで、アクセストークンの利用できる範囲を絞る仕組み。)
この機能の説明を読んでもあまり使い方が理解できなかったため、実際に動作を確認した。
まとめ
- Audience Supportでは以下の2つの方法でアクセストークンに設定するaudienceを制御できる。
- ユーザーもしくはクライアントに設定したロールに基づいて自動設定する。(実際にはAudience Resolve Protocol Mapperにより設定される。)
- OpenID Connect(OAuth 2.0)のscopeパラメータに指定された値に対応するaudienceを設定する。(実際にはClient Scopeに設定するProtocol Mapperでトークンにaudienceを追加する。)
- 利用範囲を狭めたアクセストークンを取得するために必要な手順を考えると、ロールベースの場合は事前にロールをユーザーもしくはクライアントに割り当てておく必要があり、scopeパラメータの場合はAuthorization Code Flowではリダイレクトが発生する。
- ログイン後にResource Serverごとにこの操作を行うというのは理想的ではないため、この機能はあくまでアクセストークンへのaudienceの設定に使い、利用範囲を狭めたトークンの取得にはOAuth 2.0 Token Exchange(Keycloakではプレビューの段階であるが)を使うのが良い。
下準備
検査環境はKeycloakの認可機能の使い方(RBAC編)と同じものを利用している。
-
管理者用アクセストークンを取得する関数を用意する。
BASE_URI=http://keycloak:8080/auth get-token() { ACCESS_TOKEN=$(curl -s -X POST \ "${BASE_URI}/realms/master/protocol/openid-connect/token" \ -d "client_id=admin-cli" \ -d "username=admin" \ -d "password=admin" \ -d "grant_type=password" \ | jq -r '.access_token') echo ACCESS_TOKEN = ${ACCESS_TOKEN} }
-
リソースサーバー(resource-server-1)を登録する。
RESOURCE_SERVER_CLIENT_ID=resource-server-1 get-token RESOURCE_SERVER_ID=$(curl -s -X POST \ "${BASE_URI}/admin/realms/master/clients" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -D - \ --data-binary @- << EOF | grep -oP '(?<=clients/).+(?=\r)' { "clientId": "${RESOURCE_SERVER_CLIENT_ID}", "protocol": "openid-connect", "bearerOnly": true, "publicClient": false } EOF ) echo RESOURCE_SERVER_ID = ${RESOURCE_SERVER_ID} RESOURCE_SERVER_CLIENT_SECRET=$(curl -s \ "${BASE_URI}/admin/realms/master/clients/${RESOURCE_SERVER_ID}"/client-secret \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" | jq -r .value) echo RESOURCE_SERVER_CLIENT_SECRET = ${RESOURCE_SERVER_CLIENT_SECRET}
-
リソースサーバー(resource-server-1)にクライアントロール(client-role-1)を設定する。
CLIENT_ROLE_NAME=client-role-1 get-token CLIENT_ROLE_URI=$(curl -s -X POST \ "${BASE_URI}/admin/realms/master/clients/${RESOURCE_SERVER_ID}/roles" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -D - \ --data-binary @- << EOF | grep -oP '(?<=Location: ).+(?=\r)' { "name": "${CLIENT_ROLE_NAME}" } EOF ) echo CLIENT_ROLE_URI = ${CLIENT_ROLE_URI} CLIENT_ROLE_ID=$(curl -s ${CLIENT_ROLE_URI} \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ | jq -r .id) echo CLIENT_ROLE_ID = ${CLIENT_ROLE_ID}
-
トークン発行時にリソースサーバー(resource-server-1)をaudienceに設定するProtocol Mapperを紐づけた、クライアントスコープ(client-scope-1)を登録する。
CLIENT_SCOPE_NAME=client-scope-1 get-token CLIENT_SCOPE_ID=$(curl -s -X POST \ "${BASE_URI}/admin/realms/master/client-scopes" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -D - \ --data-binary @- << EOF | grep -oP '(?<=client-scopes/).+(?=\r)' { "name": "${CLIENT_SCOPE_NAME}", "protocol": "openid-connect", "protocolMappers": [{ "name": "protocol-mapper-1", "protocol": "openid-connect", "protocolMapper":"oidc-audience-mapper", "config": { "id.token.claim": "false", "access.token.claim": "true", "included.client.audience": "resource-server-1" } }] } EOF ) echo CLIENT_SCOPE_ID = $CLIENT_SCOPE_ID
-
リソースの利用者となるクライアント(client-1)を登録する。
CLIENT_ID=client-1 get-token ID_OF_CLIENT=$(curl -s -X POST \ "${BASE_URI}/admin/realms/master/clients" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -D - \ --data-binary @- << EOF | grep -oP '(?<=clients/).+(?=\r)' { "clientId": "${CLIENT_ID}", "protocol": "openid-connect", "publicClient": true } EOF ) echo ID_OF_CLIENT = ${ID_OF_CLIENT}
-
クライアント(client-1)にクライアントスコープ(client-scope-1)をOptionalとして割り当てる。
get-token curl -s -X PUT \ "${BASE_URI}/admin/realms/master/clients/${ID_OF_CLIENT}/optional-client-scopes/$CLIENT_SCOPE_ID" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" | jq .
-
ユーザー(user-1)を登録してパスワードを設定する。
USER_NAME=user-1 USER_PASSWORD=password get-token USER_ID=$(curl -s -X POST \ "${BASE_URI}/admin/realms/master/users" \ -D - \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ --data-binary @- << EOF | grep -oP '(?<=users/).+(?=\r)' { "username": "${USER_NAME}", "enabled": true } EOF ) echo USER_ID = ${USER_ID} get-token curl -s -X PUT \ "${BASE_URI}/admin/realms/master/users/${USER_ID}/reset-password" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ --data-binary @- << EOF { "type": "password", "value": "${USER_PASSWORD}", "temporary": false } EOF
動作検証
-
現状の設定のままアクセストークンを発行すると、audクレームにはデフォルトのaccountのみが指定される。
CLIENT_ACCESS_TOKEN=$(curl -s -X POST \ "${BASE_URI}/realms/master/protocol/openid-connect/token" \ -d "grant_type=password" \ -d "client_id=${CLIENT_ID}" \ -d "username=${USER_NAME}" \ -d "password=${USER_PASSWORD}" \ | jq -r '.access_token' ) echo CLIENT_ACCESS_TOKEN = ${CLIENT_ACCESS_TOKEN} curl -s -X POST \ "${BASE_URI}/realms/master/protocol/openid-connect/token/introspect" \ -u "${RESOURCE_SERVER_CLIENT_ID}:${RESOURCE_SERVER_CLIENT_SECRET}" \ -d "token_type_hint=access_token" \ -d "token=${CLIENT_ACCESS_TOKEN}" | jq . # => { "aud": "account", ... }
-
scopeパラメータにClient Scope(client-scope-1)を指定すると、Protocol Mapperによってリソースサーバー(resource-server-1)がaudクレームに追加される。
CLIENT_ACCESS_TOKEN=$(curl -s -X POST \ "${BASE_URI}/realms/master/protocol/openid-connect/token" \ -d "grant_type=password" \ -d "client_id=${CLIENT_ID}" \ -d "username=${USER_NAME}" \ -d "password=${USER_PASSWORD}" \ -d "scope=${CLIENT_SCOPE_NAME}" \ | jq -r '.access_token' ) echo CLIENT_ACCESS_TOKEN = ${CLIENT_ACCESS_TOKEN} curl -s -X POST \ "${BASE_URI}/realms/master/protocol/openid-connect/token/introspect" \ -u "${RESOURCE_SERVER_CLIENT_ID}:${RESOURCE_SERVER_CLIENT_SECRET}" \ -d "token_type_hint=access_token" \ -d "token=${CLIENT_ACCESS_TOKEN}" | jq . # => { "aud": ["resource-server-1", "account"], ... }
-
もしくは、ユーザー(user-1)にロール(role-1)を割り当てると、自動設定(Audience Resolve Protocol Mapper)によりリソースサーバー(resource-server-1)がaudクレームに追加される。
get-token curl -s -X POST \ "${BASE_URI}/admin/realms/master/users/${USER_ID}/role-mappings/clients/${RESOURCE_SERVER_ID}" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ --data-binary @- << EOF | jq . [{ "id": "${CLIENT_ROLE_ID}", "name": "${CLIENT_ROLE_NAME}" }] EOF CLIENT_ACCESS_TOKEN=$(curl -s -X POST \ "${BASE_URI}/realms/master/protocol/openid-connect/token" \ -d "grant_type=password" \ -d "client_id=${CLIENT_ID}" \ -d "username=${USER_NAME}" \ -d "password=${USER_PASSWORD}" \ | jq -r '.access_token' ) echo CLIENT_ACCESS_TOKEN = ${CLIENT_ACCESS_TOKEN} curl -s -X POST \ "${BASE_URI}/realms/master/protocol/openid-connect/token/introspect" \ -u "${RESOURCE_SERVER_CLIENT_ID}:${RESOURCE_SERVER_CLIENT_SECRET}" \ -d "token_type_hint=access_token" \ -d "token=${CLIENT_ACCESS_TOKEN}" | jq . # => { "aud": ["resource-server-1", "account"], ... }