3
0

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のAudience Support機能の使い方

Posted at

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 ExchangeKeycloakではプレビューの段階であるが)を使うのが良い。

下準備

検査環境は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"], ... }
    
3
0
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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?