Keycloakの認可機能への理解を深めるため、実際に保護対象のリソースと認可ルール(ポリシー・パーミッション)を設定し、登録したクライアントとユーザーの組み合わせで取得したアクセストークンでの認可判定の動作を確認した。
今回はアクセス制御モデルとしてよく使われるロールベースアクセス制御(RBAC)を設定する。リソースの認可ルールとしてロールを条件(ロールベースポリシー)として登録し、ユーザーにロールを割り当てていない状態、割り当てた状態での認可判定を実施する。
なお、各種設定は管理コンソールを使わず、基本的にAdmin REST APIで行う。
検証環境
Docker(Docker Compose)で以下の構成の検証環境を立ち上げる。
- Keycloak 11.02
- Ubuntu 22.04(curlコマンドでAPIを呼び出すためのクライアントとして)
docker-compose.yml
version: '3'
services:
curl_jq:
tty: true
build:
context: .
keycloak:
image: quay.io/keycloak/keycloak:11.0.2
ports:
- 8080:8080
environment:
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
※ 動作検証目的のため、Keycloakの管理者ユーザー名・パスワードを admin
に設定している。
Dockerfile
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y curl jq
動作確認手順・結果
-
管理者用アクセストークンを取得する関数を用意する。
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} }
-
ロール(role-1)を登録する。
ROLE_NAME=role-1 get-token ROLE_URI=$(curl -s -X POST \ "${BASE_URI}/admin/realms/master/roles" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ -D - \ --data-binary @- << EOF | grep -oP '(?<=Location: ).+(?=\r)' { "name": "${ROLE_NAME}" } EOF ) echo ROLE_URI = ${ROLE_URI} ROLE_ID=$(curl -s ${ROLE_URI} \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ | jq -r .id) echo ROLE_ID = ${ROLE_ID}
-
リソースサーバー(resource-server-1)と保護対象となるリソース(resource-1)とrole-1を認可条件とするロールベースポリシー(policy-1)を登録する。
※ KeycloakではリソースサーバーはOpenID ConnectのConfidentialクライアントとして登録する。RESOURCE_SERVER_CLIENT_ID=resource-server-1 RESOURCE_NAME=resource-1 POLICY_NAME=policy-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", "publicClient": false, "serviceAccountsEnabled": true, "authorizationServicesEnabled": true, "authorizationSettings": { "resources": [{ "name": "${RESOURCE_NAME}", "displayName": "Resource 1" }], "policies": [{ "type": "role", "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "name": "${POLICY_NAME}", "config": { "roles": "[{\"id\":\"${ROLE_ID}\"}]" } }] } } EOF ) echo RESOURCE_SERVER_ID = ${RESOURCE_SERVER_ID}
-
登録したリソース(resource-1)とポリシー(policy-1)を紐づけるパーミッション(permission-1)を設定する。
get-token curl -s -X POST \ "${BASE_URI}/admin/realms/master/clients/${RESOURCE_SERVER_ID}/authz/resource-server/permission/resource" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ --data-binary @- << EOF | jq . { "type": "resource", "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "name": "permission-1", "resources": ["${RESOURCE_NAME}"], "policies": ["${POLICY_NAME}"] } EOF
-
リソースの利用者となるクライアント(client-1)を登録する。
CLIENT_ID=client-1 get-token curl -s -X POST \ "${BASE_URI}/admin/realms/master/clients" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ --data-binary @- << EOF { "clientId": "${CLIENT_ID}", "protocol": "openid-connect", "publicClient": true } EOF
-
ユーザー(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
-
ユーザー(user-1)にロール(role-1)を割り当てない状態で認可判定を行うとアクセス拒否となる。
※ アクセストークン取得は簡易的にPassword Flowで行っている。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" \ -H "Authorization: Bearer ${CLIENT_ACCESS_TOKEN}" \ -d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ -d "response_mode=decision" \ -d "audience=${RESOURCE_SERVER_CLIENT_ID}" \ -d "permission=${RESOURCE_NAME}" \ | jq . # => { "error": "access_denied", "error_description": "not_authorized" }
-
ユーザー(user-1)にロール(role-1)を割り当てる。
get-token curl -s -X POST \ "${BASE_URI}/admin/realms/master/users/${USER_ID}/role-mappings/realm" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ --data-binary @- << EOF | jq . [{ "id": "${ROLE_ID}", "name": "${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" \ -H "Authorization: Bearer ${CLIENT_ACCESS_TOKEN}" \ -d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ -d "response_mode=decision" \ -d "audience=${RESOURCE_SERVER_CLIENT_ID}" \ -d "permission=${RESOURCE_NAME}" \ | jq . # => { "result": true }