0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Keycloakの認可機能の使い方(OAuth2スコープ編)

Posted at

前回の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 }
    

参考

0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?