kayx23 commented on code in PR #13246: URL: https://github.com/apache/apisix/pull/13246#discussion_r3109664849
########## docs/en/latest/plugins/authz-keycloak.md: ########## @@ -28,214 +28,737 @@ description: This document contains information about the Apache APISIX authz-ke # --> -## Description - -The `authz-keycloak` Plugin can be used to add authentication with [Keycloak Identity Server](https://www.keycloak.org/). +<head> + <link rel="canonical" href="https://docs.api7.ai/hub/authz-keycloak" /> +</head> -:::tip +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; -Although this Plugin was developed to work with Keycloak, it should work with any OAuth/OIDC and UMA compliant identity providers as well. +## Description -::: +The `authz-keycloak` Plugin supports the integration with [Keycloak](https://www.keycloak.org/) to authenticate and authorize users. See Keycloak's [Authorization Services Guide](https://www.keycloak.org/docs/latest/authorization_services/) for more information about the configuration options available in this Plugin. -Refer to [Authorization Services Guide](https://www.keycloak.org/docs/latest/authorization_services/) for more information on Keycloak. +While the Plugin was developed for Keycloak, it could theoretically be used with other OAuth/OIDC and UMA-compliant identity providers. ## Attributes -| Name | Type | Required | Default | Valid values | Description | -|----------------------------------------------|---------------|----------|-----------------------------------------------|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| discovery | string | False | | https://host.domain/realms/foo/.well-known/uma2-configuration | URL to [discovery document](https://www.keycloak.org/docs/latest/authorization_services/index.html) of Keycloak Authorization Services. | -| token_endpoint | string | False | | https://host.domain/realms/foo/protocol/openid-connect/token | An OAuth2-compliant token endpoint that supports the `urn:ietf:params:oauth:grant-type:uma-ticket` grant type. If provided, overrides the value from discovery. | -| resource_registration_endpoint | string | False | | https://host.domain/realms/foo/authz/protection/resource_set | A UMA-compliant resource registration endpoint. If provided, overrides the value from discovery. | -| client_id | string | True | | | The identifier of the resource server to which the client is seeking access. | -| client_secret | string | False | | | The client secret, if required. You can use APISIX secret to store and reference this value. APISIX currently supports storing secrets in two ways. [Environment Variables and HashiCorp Vault](../terminology/secret.md) | -| grant_type | string | False | "urn:ietf:params:oauth:grant-type:uma-ticket" | ["urn:ietf:params:oauth:grant-type:uma-ticket"] | | -| policy_enforcement_mode | string | False | "ENFORCING" | ["ENFORCING", "PERMISSIVE"] | | -| permissions | array[string] | False | | | An array of strings, each representing a set of one or more resources and scopes the client is seeking access. | -| lazy_load_paths | boolean | False | false | | When set to true, dynamically resolves the request URI to resource(s) using the resource registration endpoint instead of the static permission. | -| http_method_as_scope | boolean | False | false | | When set to true, maps the HTTP request type to scope of the same name and adds to all requested permissions. | -| timeout | integer | False | 3000 | [1000, ...] | Timeout in ms for the HTTP connection with the Identity Server. | -| access_token_expires_in | integer | False | 300 | [1, ...] | Expiration time(s) of the access token. | -| access_token_expires_leeway | integer | False | 0 | [0, ...] | Expiration leeway(s) for access_token renewal. When set, the token will be renewed access_token_expires_leeway seconds before expiration. This avoids errors in cases where the access_token just expires when reaching the OAuth Resource Server. | -| refresh_token_expires_in | integer | False | 3600 | [1, ...] | The expiration time(s) of the refresh token. | -| refresh_token_expires_leeway | integer | False | 0 | [0, ...] | Expiration leeway(s) for refresh_token renewal. When set, the token will be renewed refresh_token_expires_leeway seconds before expiration. This avoids errors in cases where the refresh_token just expires when reaching the OAuth Resource Server. | -| ssl_verify | boolean | False | true | | When set to true, verifies if TLS certificate matches hostname. | -| cache_ttl_seconds | integer | False | 86400 (equivalent to 24h) | positive integer >= 1 | Maximum time in seconds up to which the Plugin caches discovery documents and tokens used by the Plugin to authenticate to Keycloak. | -| keepalive | boolean | False | true | | When set to true, enables HTTP keep-alive to keep connections open after use. Set to `true` if you are expecting a lot of requests to Keycloak. | -| keepalive_timeout | integer | False | 60000 | positive integer >= 1000 | Idle time after which the established HTTP connections will be closed. | -| keepalive_pool | integer | False | 5 | positive integer >= 1 | Maximum number of connections in the connection pool. | -| access_denied_redirect_uri | string | False | | [1, 2048] | URI to redirect the user to instead of returning an error message like `"error_description":"not_authorized"`. | -| password_grant_token_generation_incoming_uri | string | False | | /api/token | Set this to generate token using the password grant type. The Plugin will compare incoming request URI to this value. | +| Name | Type | Required | Default | Valid values | Description | +|------|------|----------|---------|--------------|-------------| +| client_id | string | True | | | Client ID. | +| client_secret | string | False | | | Client secret. The value is encrypted with AES before being stored in etcd. | +| discovery | string | False | | | URL to the discovery document. | +| token_endpoint | string | False | | | Token endpoint that supports the `urn:ietf:params:oauth:grant-type:uma-ticket` grant type to obtain access token. If provided, overrides the value from the discovery document. | +| resource_registration_endpoint | string | False | | | A UMA-compliant resource registration endpoint. Required when `lazy_load_paths` is `true`. The Plugin will first look for the resource registration endpoint from this configuration option; if not found, look for the resource registration endpoint from the discovery document. | +| grant_type | string | False | `urn:ietf:params:oauth:grant-type:uma-ticket` | `urn:ietf:params:oauth:grant-type:uma-ticket` | Must be set to `urn:ietf:params:oauth:grant-type:uma-ticket`. | +| policy_enforcement_mode | string | False | `ENFORCING` | `ENFORCING` or `PERMISSIVE` | The mode of [policy enforcement](https://www.keycloak.org/docs/latest/authorization_services/index.html#policy-enforcement). In `ENFORCING` mode, requests are denied when there is no policy associated with a given resource. In `PERMISSIVE` mode, requests are allowed when there is no policy associated with a given resource. | +| permissions | array[string] | False | | | An array of permissions representing a set of resources and scopes the client is seeking access. The format could be `RESOURCE_ID#SCOPE_ID`, `RESOURCE_ID`, or `#SCOPE_ID`. Used when `lazy_load_paths` is `false`. See [obtaining permissions](https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_obtaining_permissions). | +| lazy_load_paths | boolean | False | `false` | | If `true`, require discovery or resource registration endpoint to dynamically resolve the request URI to resources. This requires the Plugin to obtain a separate access token for itself from the token endpoint. Make sure the `Service Accounts Enabled` option is checked in Keycloak to allow for client credentials grant, and that the issued access token contains the `resource_access` claim with the `uma_protection` role for the Plugin to query resources through the [Protection API](https://www.keycloak.org/docs/latest/authorization_services/index.html#authorization-services). | +| http_method_as_scope | boolean | False | `false` | | If `true`, use the HTTP method of the request as the scope to check whether access should be granted. When `lazy_load_paths` is `false`, the Plugin adds the mapped scope to any of the static permissions configured in the `permissions` attribute, even when they contain one or more scopes already. | +| timeout | integer | False | 3000 | >= 1000 | Timeout in milliseconds for the HTTP connection with the identity provider. | +| access_token_expires_in | integer | False | 300 | >= 1 | Lifetime of the access token in seconds if no `expires_in` attribute is present in the token endpoint response. | +| access_token_expires_leeway | integer | False | 0 | >= 0 | Expiration leeway in seconds for access token renewal. When set to a value greater than 0, token renewal will take place the configured amount of time before token expiration. | +| refresh_token_expires_in | integer | False | 3600 | > 0 | Expiration time of the refresh token in seconds. | +| refresh_token_expires_leeway | integer | False | 0 | >= 0 | Expiration leeway in seconds for refresh token renewal. When set to a value greater than 0, token renewal will take place the configured amount of time before token expiration. | +| ssl_verify | boolean | False | `true` | | If `true`, verify the OpenID provider's SSL certificates. | +| cache_ttl_seconds | integer | False | 86400 | > 0 | TTL in seconds for the Plugin to cache discovery document and access tokens. | +| keepalive | boolean | False | `true` | | If `true`, enable HTTP keep-alive to keep connections open after use. Set to `true` if you are expecting a lot of requests to Keycloak. | +| keepalive_timeout | integer | False | 60000 | >= 1000 | Idle time after which the established HTTP connections will be closed. | +| keepalive_pool | integer | False | 5 | >= 1 | Maximum number of connections in the connection pool. | +| access_denied_redirect_uri | string | False | | | URI to redirect the user to instead of returning an error message like `"error_description":"not_authorized"` when access is denied. | +| password_grant_token_generation_incoming_uri | string | False | | | The URI incoming requests hit to generate a token using the password grant, for example, `/api/token`. If the incoming request's URI matches the configured value, the request method is POST, and `Content-Type` is `application/x-www-form-urlencoded`, a token is generated at the `token_endpoint`. | NOTE: `encrypt_fields = {"client_secret"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields). -### Discovery and endpoints +## Examples -It is recommended to use the `discovery` attribute as the `authz-keycloak` Plugin can discover the Keycloak API endpoints from it. +The examples below demonstrate how you can configure `authz-keycloak` for different scenarios. -If set, the `token_endpoint` and `resource_registration_endpoint` will override the values obtained from the discovery document. +To follow along, complete the [preliminary setups](#set-up-keycloak) for Keycloak. -### Client ID and secret +:::note -The Plugin needs the `client_id` attribute for identification and to specify the context in which to evaluate permissions when interacting with Keycloak. +You can fetch the `admin_key` from `conf/config.yaml` and save to an environment variable with the following command: -If the `lazy_load_paths` attribute is set to true, then the Plugin additionally needs to obtain an access token for itself from Keycloak. In such cases, if the client access to Keycloak is confidential, you need to configure the `client_secret` attribute. +```shell +admin_key=$(yq '.deployment.admin.admin_key[0].key' /conf/config.yaml | sed 's/"//g') +``` -### Policy enforcement mode +::: -The `policy_enforcement_mode` attribute specifies how policies are enforced when processing authorization requests sent to the server. +### Set Up Keycloak -#### `ENFORCING` mode +#### Start Keycloak -Requests are denied by default even when there is no policy associated with a resource. +Start a Keycloak instance named `apisix-quickstart-keycloak` with the administrator name `quickstart-admin` and password `quickstart-admin-pass` in [development mode](https://www.keycloak.org/server/configuration#_starting_keycloak_in_development_mode) in Docker: -The `policy_enforcement_mode` is set to `ENFORCING` by default. +```shell +docker run -d --name "apisix-quickstart-keycloak" \ + -e 'KEYCLOAK_ADMIN=quickstart-admin' \ + -e 'KEYCLOAK_ADMIN_PASSWORD=quickstart-admin-pass' \ + -p 8080:8080 \ + quay.io/keycloak/keycloak:18.0.2 start-dev +``` -#### `PERMISSIVE` mode +Save the Keycloak IP to an environment variable: -Requests are allowed when there is no policy associated with a given resource. +```shell +KEYCLOAK_IP=192.168.42.145 # replace with your host IP +``` -### Permissions +Navigate to `http://localhost:8080` in a browser and click **Administration Console**. Enter the administrator username `quickstart-admin` and password `quickstart-admin-pass` to sign in. -When handling incoming requests, the Plugin can determine the permissions to check with Keycloak statically or dynamically from the properties of the request. +#### Create a Realm -If the `lazy_load_paths` attribute is set to `false`, the permissions are taken from the `permissions` attribute. Each entry in `permissions` needs to be formatted as expected by the token endpoint's `permission` parameter. See [Obtaining Permissions](https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_obtaining_permissions). +In the left menu, hover over **Master**, and select **Add realm** in the dropdown. Enter the realm name `quickstart-realm` and click **Create**. -:::note +#### Create a Client -A valid permission can be a single resource or a resource paired with on or more scopes. +Click **Clients** > **Create** to open the **Add Client** page. Enter **Client ID** as `apisix-quickstart-client`, keep the **Client Protocol** as `openid-connect`, and click **Save**. -::: +After redirecting to the detailed page, select `confidential` as the **Access Type**. Enter wildcard `*` in **Valid Redirect URIs** for simplicity. -If the `lazy_load_paths` attribute is set to `true`, the request URI is resolved to one or more resources configured in Keycloak using the resource registration endpoint. The resolved resources are used as the permissions to check. +Enable authorization for the Client, which also enables service accounts with the `uma_protection` role automatically. Click **Save**. -:::note +#### Save Client ID and Secret -This requires the Plugin to obtain a separate access token for itself from the token endpoint. So, make sure to set the `Service Accounts Enabled` option in the client settings in Keycloak. +Click **Clients** > `apisix-quickstart-client` > **Credentials**, and copy the Client secret from **Secret**. -Also make sure that the issued access token contains the `resource_access` claim with the `uma_protection` role to ensure that the Plugin is able to query resources through the Protection API. +Save the OIDC Client ID and secret to environment variables: -::: +```shell +OIDC_CLIENT_ID=apisix-quickstart-client +OIDC_CLIENT_SECRET=bSaIN3MV1YynmtXvU8lKkfeY0iwpr9cH # replace with your value +``` -### Automatically mapping HTTP method to scope +#### Request Access Token -The `http_method_as_scope` is often used together with `lazy_load_paths` but can also be used with a static permission list. +Request an access token from Keycloak: -If the `http_method_as_scope` attribute is set to `true`, the Plugin maps the request's HTTP method to the scope with the same name. The scope is then added to every permission to check. +```shell +curl -i "http://$KEYCLOAK_IP:8080/realms/quickstart-realm/protocol/openid-connect/token" -X POST \ + -d 'grant_type=client_credentials' \ + -d 'client_id='$OIDC_CLIENT_ID'' \ + -d 'client_secret='$OIDC_CLIENT_SECRET'' +``` + +Save the access token to an environment variable: -If the `lazy_load_paths` attribute is set to false, the Plugin adds the mapped scope to any of the static permissions configured in the `permissions` attribute—even if they contain on or more scopes already. +```shell +ACCESS_TOKEN=<your_access_token> # replace with the access_token value from the response +``` -### Generating a token using `password` grant +### Use Lazy Load Path and Resource Registration Endpoint -To generate a token using `password` grant, you can set the value of the `password_grant_token_generation_incoming_uri` attribute. +The following example demonstrates how you can configure the Plugin to dynamically resolve the request URI to resource(s) using the resource registration endpoint instead of static permissions. -If the incoming URI matches the configured attribute and the request method is POST, a token is generated using the `token_endpoint`. +Create a Route with the `authz-keycloak` Plugin: -You also need to add `application/x-www-form-urlencoded` as `Content-Type` header and `username` and `password` as parameters. +<Tabs +groupId="api" +defaultValue="admin-api" +values={[ +{label: 'Admin API', value: 'admin-api'}, +{label: 'ADC', value: 'adc'}, +{label: 'Ingress Controller', value: 'aic'} +]}> -The example below shows a request if the `password_grant_token_generation_incoming_uri` is `/api/token`: +<TabItem value="admin-api"> ```shell -curl --location --request POST 'http://127.0.0.1:9080/api/token' \ ---header 'Accept: application/json, text/plain, */*' \ ---header 'Content-Type: application/x-www-form-urlencoded' \ ---data-urlencode 'username=<User_Name>' \ ---data-urlencode 'password=<Password>' +curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "id": "authz-keycloak-route", + "uri": "/anything", + "plugins": { + "authz-keycloak": { + "lazy_load_paths": true, + "resource_registration_endpoint": "http://'"$KEYCLOAK_IP"':8080/realms/quickstart-realm/authz/protection/resource_set", + "discovery": "http://'"$KEYCLOAK_IP"':8080/realms/quickstart-realm/.well-known/uma2-configuration", + "client_id": "'"$OIDC_CLIENT_ID"'", + "client_secret": "'"$OIDC_CLIENT_SECRET"'" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "httpbin.org": 1 + } + } + }' ``` -## Enable Plugin +</TabItem> + +<TabItem value="adc"> + +```yaml title="adc.yaml" +services: + - name: authz-keycloak-service + routes: + - name: authz-keycloak-route + uris: + - /anything + plugins: + authz-keycloak: + lazy_load_paths: true + resource_registration_endpoint: "http://<KEYCLOAK_IP>:8080/realms/quickstart-realm/authz/protection/resource_set" + discovery: "http://<KEYCLOAK_IP>:8080/realms/quickstart-realm/.well-known/uma2-configuration" + client_id: "apisix-quickstart-client" + client_secret: "<OIDC_CLIENT_SECRET>" + upstream: + type: roundrobin + nodes: + - host: httpbin.org + port: 80 + weight: 1 +``` -The example below shows how you can enable the `authz-keycloak` Plugin on a specific Route. `${realm}` represents the realm name in Keycloak. +Synchronize the configuration to the gateway: -:::note -You can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command: +```shell +adc sync -f adc.yaml +``` -```bash -admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g') +</TabItem> + +<TabItem value="aic"> + +<Tabs +groupId="k8s-api" +defaultValue="gateway-api" +values={[ +{label: 'Gateway API', value: 'gateway-api'}, +{label: 'APISIX Ingress Controller', value: 'apisix-ingress-controller'} +]}> + +<TabItem value="gateway-api"> + +```yaml title="authz-keycloak-ic.yaml" +apiVersion: apisix.apache.org/v1alpha1 +kind: PluginConfig +metadata: + namespace: aic + name: authz-keycloak-plugin-config +spec: + plugins: + - name: authz-keycloak + config: + lazy_load_paths: true + resource_registration_endpoint: "http://<KEYCLOAK_IP>:8080/realms/quickstart-realm/authz/protection/resource_set" + discovery: "http://<KEYCLOAK_IP>:8080/realms/quickstart-realm/.well-known/uma2-configuration" + client_id: "apisix-quickstart-client" + client_secret: "<OIDC_CLIENT_SECRET>" +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + namespace: aic + name: authz-keycloak-route +spec: + parentRefs: + - name: apisix + rules: + - matches: + - path: + type: Exact + value: /anything + filters: + - type: ExtensionRef + extensionRef: + group: apisix.apache.org + kind: PluginConfig + name: authz-keycloak-plugin-config + backendRefs: + - name: httpbin-external-domain Review Comment: This Gateway API example references `httpbin-external-domain`, but the manifest in this section never creates that `Service`. As written, the route points to a backend that does not exist, so readers who copy this example will not get a working setup. Could we add the missing `Service` resource here (and in the other Gateway API sections in this PR that reuse the same backend)? -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
