前回のKeycloak認可機能の動作確認(RBACでの認可判定)に続き、OAuth 2.0のスコープを使った認可ポリシーの設定と認可判定の動作を確認した。
今回のアクセス制御では、リソースサーバーへのアクセスを表すOAuth 2.0スコープを定義し、リソースサーバーの呼び出し時に指定されたアクセストークンにこのスコープが含まれる場合にアクセスを許可する。(クライアントは認可リクエストにてリソースサーバーのスコープを指定してアクセストークンを取得する必要がある。)
検査環境
基本的には前回同様であるが、JavaScript-Based PolicyをAdmin REST APIで登録できるようにするために upload_scripts
Featureを有効化している。
※ 本番運用では upload_scripts
Featureは非推奨となっており、jarファイルにスクリプトを含めてデプロイする方法が推奨されている。(参考)
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
JAVA_OPTS: -Dkeycloak.profile.feature.upload_scripts=enabled
動作確認手順・結果
-
管理者用アクセストークンを取得する関数を用意する。
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-1)を登録する。
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" }] } } EOF ) echo RESOURCE_SERVER_ID = ${RESOURCE_SERVER_ID}
-
クライアントスコープ(client-scope-1)を登録する。
※ KeycloakではOAuth 2.0(OpenID Connect)のスコープをクライアントスコープというモデルで扱う。(リソースを細分化するためのモデルであるスコープとは異なることに注意。)
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"
}
EOF
)
echo CLIENT_SCOPE_ID = $CLIENT_SCOPE_ID
-
クライアントスコープ(client-scope-1)が認可されているかを検証するJavaScriptポリシー(policy-1)を登録する。
※ RBACではビルトインのロールベースポリシーを利用できるが、クライアントスコープを判定するビルトインポリシーは提供されていないため、JavaScriptでポリシーを記述する必要がある。(より簡単に設定ができないかはKeycloakコミュニティに質問中)
POLICY_NAME=policy-1 get-token curl -s -X POST \ "${BASE_URI}/admin/realms/master/clients/${RESOURCE_SERVER_ID}/authz/resource-server/policy/js" \ -H "Authorization: Bearer ${ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ --data-binary @- << EOF | jq . { "name":"${POLICY_NAME}", "type": "js", "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", "code": "var requiredScope = \"${CLIENT_SCOPE_NAME}\";\rvar attributes = \$evaluation.getContext().getIdentity().getAttributes();\rvar scope = attributes.getValue('scope').asString(0);\r\nif (scope.split(\" \").indexOf(requiredScope) >= 0) {\r \$evaluation.grant();\r} else {\r \$evaluation.deny();\r}" } EOF
登録したJavaScriptポリシーは以下の通り:
var requiredScope = "client-scope-1"; var attributes = $evaluation.getContext().getIdentity().getAttributes(); var scope = attributes.getValue('scope').asString(0); if (scope.split(" ").indexOf(requiredScope) >= 0) { $evaluation.grant(); } else { $evaluation.deny(); }
-
登録したリソース(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 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}
-
ユーザー(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
-
この状態ではクライアントにClient Scopeが割り当てられていないため、アクセストークンの発行でエラーになる。
※ 今回の検証では、アクセストークンをcurlで取得するために、Authorization Code Flowではなく、簡易的にPassword Flowを利用している。
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' # => { "error": "invalid_scope", "error_description": "Invalid scopes: scope-1" }
-
クライアント(client-1)にクライアントスコープ(scope-1)をOptionalとして割り当てる。
※ クライアントスコープの割り当てにはDefault(scopeパラメータを指定しなくても予め認可されるもの)Optional(scopeパラメータに指定できるもの)の2種類がある。
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 .
-
scopeパラメータに何も指定せず取得したアクセストークンでは認可判定NGとなる。
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-1" \ -d "permission=resource-1" \ | jq . # => { "error": "access_denied", "error_description": "not_authorized" }
-
scopeパラメータにクライアントスコープ(client-scope-1)を指定して取得したアクセストークンでは認可判定OKとなる。
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" \ -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-1" \ -d "permission=resource-1" \ | jq . # => { "result": true }