diff --git a/docs/oauth2-examples-keycloak.md b/docs/oauth2-examples-keycloak.md index 596acb3c38..da70aa19e8 100644 --- a/docs/oauth2-examples-keycloak.md +++ b/docs/oauth2-examples-keycloak.md @@ -19,59 +19,142 @@ See the License for the specific language governing permissions and limitations under the License. --> -# Use Keycloak as OAuth 2.0 server +## Use Keycloak as OAuth 2.0 server -This guide explains how to set up OAuth 2.0 for RabbitMQ -and Keycloak as Authorization Server using the following flows: +This guide explains how to set up OAuth 2.0 for RabbitMQ and Keycloak as Authorization Server using +the following flows: * Access [management UI](./management/) via a browser * Access management HTTP API * Application authentication and authorization +## Keycloak JWT payloads + +Keycloak can issue two types of JWT payloads. + +One type of payload is found in a [Requesting Party Token](./oauth2#requesting-party-token). +RabbitMQ supports this type of token and it extracts the scopes from it. You do not need to +configure anything. + +The second type of payload is the following: + +```json +{ + "realm_access": { + "roles": [ + "offline_access", + "uma_authorization", + "rabbitmq.tag:management", + ] + }, + "resource_access": { + "account": { + "roles": [ + "manage-account", + "manage-account-links", + "view-profile", + "rabbitmq.write:*/*" + ] + } + }, + "roles": "rabbitmq.read:*/*", + "scope": "profile email" +} +``` + +:::info +The claim `roles` is not, strictly speaking, part of Keycloak official claims. Instead, it is a +custom claim configured by the user from the Keycloak administration console. +::: + +RabbitMQ does not read the scopes from this token unless you configure it to do so. For example, to +configure RabbitMQ to extract the scopes from `roles` under the `realm_access` claim, add the +following configuration variable: + +```json +auth_oauth2.additional_scopes_key = realm_access.roles +``` + +To configure RabbitMQ to also read from `resource_access` claim, edit the previous configuration as +follows: + +```json +auth_oauth2.additional_scopes_key = realm_access.roles resource_access.account.roles +``` + +And finally, if you also want to use the scopes in the claim `roles`, you edit the previous +configuration: + +```json +auth_oauth2.additional_scopes_key = roles realm_access.roles resource_access.account.roles +``` + +RabbitMQ reads the scopes from all those sources. + ## Prerequisites to follow this guide * Docker * make -* A local clone of a [GitHub repository](https://github.com/rabbitmq/rabbitmq-oauth2-tutorial/tree/next) for branch `next` that contains all the configuration files and scripts used on this example +* A local clone of a + [GitHub repository](https://github.com/rabbitmq/rabbitmq-oauth2-tutorial/tree/next) for branch + `next` that contains all the configuration files and scripts used on this example * Add the following entry to `/etc/hosts`: -``` -localhost keycloak rabbitmq -``` + + ```console + localhost keycloak rabbitmq + ``` ## Deploy Keycloak -1. First, deploy **Keycloak**. It comes preconfigured with all the required scopes, users and clients. +1. First, deploy Keycloak. It comes preconfigured with all the required scopes, users, and clients. -2. Run the following command to start **Keycloak** server: +2. Start the Keycloak server by running: - ```bash - make start-keycloak - ``` + ```bash + make start-keycloak + ``` -There is a dedicated **Keycloak realm** called `Test` configured as follows: +There is a dedicated Keycloak realm called `Test` configured as follows: -* A [rsa](https://keycloak:8443/admin/master/console/#/test/realm-settings/keys) signing key. Use `admin`:`admin` - when prompted for credentials to access the Keycloak Administration page +* A [rsa](https://keycloak:8443/admin/master/console/#/test/realm-settings/keys) signing key. Use + `admin`:`admin` when prompted for credentials to access the Keycloak Administration page * A [rsa provider](https://keycloak:8443/admin/master/console/#/test/realm-settings/keys/providers) -* Three clients: `rabbitmq-client-code` for the rabbitmq management UI, `mgt_api_client` to access via the -management api and `producer` to access via AMQP protocol. - +* Three clients: `rabbitmq-client-code` for the RabbitMQ management UI, `mgt_api_client` to access + via the management API and `producer` to access via the AMQP protocol. ## Start RabbitMQ -Run the command below to start RabbitMQ configured with the **Keycloak** server we started in the previous section: This is the [rabbitmq.conf](https://github.com/rabbitmq/rabbitmq-oauth2-tutorial/blob/next/conf/keycloak/rabbitmq.conf) used for **Keycloak**. +Run the command below to start RabbitMQ configured with the `Keycloak` server we started in the +previous section: This is the +[rabbitmq.conf](https://github.com/rabbitmq/rabbitmq-oauth2-tutorial/blob/next/conf/keycloak/rabbitmq.conf) +used for Keycloak. + ```bash export MODE=keycloak make start-rabbitmq ``` :::info -RabbitMQ is deployed with TLS enabled and Keycloak is configured with the corresponding `redirect_url` which uses https. +RabbitMQ is deployed with TLS enabled and Keycloak is configured with the corresponding `redirect_url` +which uses HTTPS. +::: + +:::important +RabbitMQ is configured to read the scopes from the custom claim +[extra_scope](https://github.com/rabbitmq/rabbitmq-oauth2-tutorial/blob/next/conf/keycloak/rabbitmq.conf#L11) +and by default from the standard claim `scope`. +However, if your scopes are deep in a map/list structure such as `authorization.permissions.scopes`, +or under `realm_access.roles` or `resource_access.account.roles`, you can configure RabbitMQ to use +those locations instead. For more information, see the section +[Use a different token field for the scope](./oauth2#use-different-token-field). ::: -## Access Management api +## Access Management API -To access the management api run the following command. It uses the client [mgt_api_client](https://keycloak:8443/admin/master/console/#/test/clients/c5be3c24-0c88-4672-a77a-79002fcc9a9d/settings) which has the scope [rabbitmq.tag:administrator](https://keycloak:8443/admin/master/console/#/test/client-scopes/f6e6dd62-22bf-4421-910e-e6070908764c/settings). +To access the management api run the following command. It uses the client +[mgt_api_client](https://keycloak:8443/admin/master/console/#/test/clients/c5be3c24-0c88-4672-a77a-79002fcc9a9d/settings) +that has the scope +[rabbitmq.tag:administrator](https://keycloak:8443/admin/master/console/#/test/client-scopes/f6e6dd62-22bf-4421-910e-e6070908764c/settings). ```bash make curl-keycloak url=https://localhost:15671/api/overview client_id=mgt_api_client secret=LWOuYqJ8gjKg3D2U8CJZDuID3KiRZVDa realm=test @@ -79,23 +162,31 @@ make curl-keycloak url=https://localhost:15671/api/overview client_id=mgt_api_cl ## Application authentication and authorization with PerfTest -To test OAuth 2.0 authentication with AMQP protocol you are going to use RabbitMQ PerfTest tool which uses RabbitMQ Java Client. +To test OAuth 2.0 authentication with the AMQP protocol you use the RabbitMQ PerfTest tool, which +uses RabbitMQ Java Client. -First you obtain the token and pass it as a parameter to the make target `start-perftest-producer-with-token`. +First you obtain the token and pass it as a parameter to the make target +`start-perftest-producer-with-token`. ```bash make start-perftest-producer-with-token PRODUCER=producer TOKEN=$(bin/keycloak/token producer kbOFBXI9tANgKUq8vXHLhT6YhbivgXxn test) ``` -**NOTE**: Initializing an application with a token has one drawback: the application cannot use the connection beyond the lifespan of the token. See the next section where you demonstrate how to refresh the token. +:::info +Initializing an application with a token has one drawback: the application cannot use the connection +beyond the lifespan of the token. See the next section where you demonstrate how to refresh the token. +::: ## Application authentication and authorization with Pika -In the following information, OAuth 2.0 authentication is tested with the AMQP protocol and the Pika library. These tests specifically demonstrate how to refresh a token on a live AMQP connection. +In the following information, OAuth 2.0 authentication is tested with the AMQP protocol and the Pika +library. These tests specifically demonstrate how to refresh a token on a live AMQP connection. -The sample Python application [can be found on GitHub](https://github.com/rabbitmq/rabbitmq-oauth2-tutorial/tree/next/pika-client). +The sample Python application is +[in GitHub](https://github.com/rabbitmq/rabbitmq-oauth2-tutorial/tree/next/pika-client). To run this sample code proceed as follows: + ```bash python3 --version pip install pika @@ -111,16 +202,18 @@ source venv/bin/activate ``` ::: -Note: Ensure you install pika 1.3 +:::important +Ensure that you install pika 1.3. +::: ## Access [management UI](./management/) 1. Go to https://localhost:15671. -2. Click on the single button on the page which redirects to **Keycloak** to authenticate. -3. Enter `rabbit_admin` and `rabbit_admin` and you should be redirected back to RabbitMQ Management fully logged in. +2. Click on the single button on the page which redirects to Keycloak to authenticate. +3. Enter `rabbit_admin` and `rabbit_admin` and you should be redirected back to RabbitMQ Management + fully logged in. - -## Stop keycloak +## Stop Keycloak ```bash make stop-keycloak @@ -128,23 +221,23 @@ make stop-keycloak ## Notes about setting up Keycloak -### Configure Client +### Configure client For backend applications which uses **Client Credentials flow**, you can create a **Client** with: -* **Access Type** : `public` +* **Access Type**: `public` * Turn off `Standard Flow`, `Implicit Flow`, and `Direct Access Grants` * With **Service Accounts Enabled** on. If it is not enabled you do not have the tab `Credentials` * In the `Credentials` tab, you have the `client id` +### Configure client scopes -### Configure Client scopes - -*Default Client Scope* are scopes automatically granted to every token. Whereas *Optional Client Scope* are -scopes which are only granted if they are explicitly requested during the authorization/token request flow. - +*Default Client Scope* are scopes automatically granted to every token. Whereas +*Optional Client Scope* are scopes which are only granted if they are explicitly requested during +the authorization/token request flow. ### Include appropriate aud claim -You must configure a **Token Mapper** of type **Hardcoded claim** with the value of rabbitmq's *resource_server_id**. -You can configure **Token Mapper** either to a **Client scope** or to a **Client**. +You must configure a **Token Mapper** of type **Hardcoded claim** with the value of RabbitMQ's +`resource_server_id`. You can configure **Token Mapper** either to a **Client scope** or to a +**Client**. diff --git a/docs/oauth2.md b/docs/oauth2.md index e2a91c653f..f40d363f85 100644 --- a/docs/oauth2.md +++ b/docs/oauth2.md @@ -46,6 +46,7 @@ There's also a companion [troubleshooting guide for OAuth 2-specific problems](. * [Use a different token field for the scope](#use-different-token-field) * [Preferred username claims](#preferred-username-claims) * [Discovery Endpoint params](#discovery-endpoint-params) +* [Requesting Party Token](#requesting-party-token) * [Rich Authorization Request](#rich-authorization-request) ### [Advanced usage](#advanced-usage) @@ -57,8 +58,7 @@ There's also a companion [troubleshooting guide for OAuth 2-specific problems](. ### Examples for Specific Identity Providers - * How to [set up RabbitMQ with OAuth 2: examples](#examples) - +* How to [set up RabbitMQ with OAuth 2: examples](#examples) ## How it works {#how-it-works} @@ -144,7 +144,7 @@ In chronological order, here is the sequence of events that occur when a client |--------------------------------------------|----------- | `auth_oauth2.resource_server_id` | The [Resource Server ID](#resource-server-id) | `auth_oauth2.resource_server_type` | The Resource Server Type required when using [Rich Authorization Request](#rich-authorization-request) token format -| `auth_oauth2.additional_scopes_key` | Configure the plugin to look for scopes in other fields (maps to `additional_rabbitmq_scopes` in the old format). | +| `auth_oauth2.additional_scopes_key` | [Configure](#use-different-token-field) the plugin to look for scopes in other fields. | | `auth_oauth2.scope_prefix` | [Configure the prefix for all scopes](#scope-prefix). The default value is `auth_oauth2.resource_server_id` followed by the dot `.` character. | | `auth_oauth2.preferred_username_claims` | [List of the JWT claims](#preferred-username-claims) to look for the username associated with the token. | `auth_oauth2.default_key` | ID of the default signing key. @@ -252,9 +252,6 @@ The following configuration declares two signing keys and configures the kid of ```ini auth_oauth2.resource_server_id = new_resource_server_id -auth_oauth2.additional_scopes_key = my_custom_scope_key -auth_oauth2.preferred_username_claims.1 = username -auth_oauth2.preferred_username_claims.2 = user_name auth_oauth2.default_key = id1 auth_oauth2.signing_keys.id1 = test/config_schema_SUITE_data/certs/key.pem auth_oauth2.signing_keys.id2 = test/config_schema_SUITE_data/certs/cert.pem @@ -571,29 +568,124 @@ If a symmetric key is used, the configuration looks like this: ### Use a different token field for the scope {#use-different-token-field} -By default the plugin looks for the `scope` key in the token, you can configure the plugin to also look in other fields using the `extra_scopes_source` variable. Values format accepted are scope as **string** or **list** +The plugin always extracts the scopes from the `scope` claim. However, you can also configure the +plugin to look in other claims using the `auth_oauth2.additional_scopes_key` variable. -```ini -auth_oauth2.resource_server_id = my_rabbit_server -auth_oauth2.additional_scopes_key = my_custom_scope_key -``` +The scopes found in the `scope` claim must be of these two value types: + +- **string separated by spaces** like `my_id.configure:*/* my_id.read:*/* my_id.write:*/*` +- **list** like `["my_id.configure:*/*", "my_id.read:*/*", "my_id.write:*/*"]` + +The scopes found in any claim listed in the `auth_oauth2.additional_scopes_key` variable can be +of several types in addition to the two value types supported by the `scope` claim mentioned earlier. + +#### Map of scopes indexed by resource_server_id {#map-of-scopes-indexed-by-resource-id} + +This is an example of a token where scopes are not yet prefixed with the `resource_server_id`, +but are indexed by the `resource_server_id`: -Token sample: ```ini { "exp": 1618592626, "iat": 1618578226, "aud" : ["my_id"], ... - "scope_as_string": "my_id.configure:*/* my_id.read:*/* my_id.write:*/*", - "scope_as_list": ["my_id.configure:*/*", "my_id.read:*/*", "my_id.write:*/*"], - ... + "complex_claim_as_string": { + "rabbitmq": ["configure:*/* read:*/* write:*/*"] + }, + "complex_claim_as_list": { + "rabbitmq": ["configure:vhost1/*", "read:vhost1/*", "write:vhost1/*"] } + ... +} +``` + +With the following plugin configuration, the plugin reads the scopes from two additional claims: +`complex_claim_as_string` and `complex_claim_as_list`. The plugin reads the scopes and adds the key +value as prefix. For example, given the scope `configure:*/*` it produces `rabbitmq.configure:*/*`. + +```ini +auth_oauth2.resource_server_id = my_rabbit_server +auth_oauth2.additional_scopes_key = complex_claim_as_string complex_claim_as_list +``` + +#### Scopes nested deep in Maps and Lists + +This is the case for tokens issued by the Keycloak Identity Provider, but can be applied to any +token from any provider. + +This first token format stores scopes deep in maps and lists. + +```json +{ + "authorization": { + "permissions": [ + { + "scopes": [ + "rabbitmq-resource.read:*/*" + ], + "rsid": "2c390fe4-02ad-41c7-98a2-cebb8c60ccf1", + "rsname": "allvhost" + }, + { + "scopes": [ + "rabbitmq-resource.write:vhost1/*" + ], + "rsid": "e7f12e94-4c34-43d8-b2b1-c516af644cee", + "rsname": "vhost1" + }, + { + "scopes": [ + "rabbitmq-resource.tag:administrator" + ], + "rsid": "12ac3d1c-28c2-4521-8e33-0952eff10bd9" + } + ] + }, + "scope": "email profile rabbitmq-resource.tag:monitoring", +} +``` + +Given the following configuration: + +```ini +auth_oauth2.resource_server_id = my_rabbit_server +auth_oauth2.additional_scopes_key = authorization.permissions.scopes ``` +The plugin navigates the token structure following this logic: + +1. It looks up the claim `authorization`. +2. It finds a map, it then looks for the next claim `permissions`. +3. This time, it finds a list of maps. It goes over all the items in the list. +4. For each map in the list, it looks up the next claim `scopes`. +5. The value can be a list of scopes or a comma-separated string of scopes or a + [map of scopes indexed by resource_server_id](#map-of-scopes-indexed-by-resource-id). + +Additionally, the plugin always reads the scopes from the official `scope` claim. + +With the above token and plugin's configuration, the list of scopes are following: + +- `rabbitmq-resource.tag:monitoring` +- `rabbitmq-resource.read:*/*` +- `rabbitmq-resource.write:vhost1/*` +- `rabbitmq-resource.tag:administrator` + +In summary, the plugin is able to navigate the token to find the scopes using the appropriate path. + +For example, in each intermediary stage after finding `authorization` and/or `permissions` keys, the +value can be another Map or a List of Maps. In the last stage, after finding the last `scopes` key, +the value can be any of any of the value types explained in the previous section. + +These are: + +- **string separated by spaces** such as `my_id.configure:*/* my_id.read:*/* my_id.write:*/*` +- **list** such as `["my_id.configure:*/*", "my_id.read:*/*", "my_id.write:*/*"]` +- [Map of scopes indexed by resource server id](#map-of-scopes-indexed-by-resource-id) + ### Preferred username claims {#preferred-username-claims} -The username associated with the token must be available to RabbitMQ so that this username is displayed in the RabbitMQ Management UI. +The user name associated with the token must be available to RabbitMQ so that this username is displayed in the RabbitMQ Management UI. By default, RabbitMQ searches for the `sub` claim first, and if it is not found, RabbitMQ uses the `client_id`. Most authorization servers return the user's GUID in the `sub` claim instead of the user's username or email address, anything the user can relate to. When the `sub` claim does not carry a *user-friendly username*, you can configure one or several claims to extract the username from the token. @@ -610,7 +702,6 @@ auth_oauth2.preferred_username_claims.2 = email In the example configuration, RabbitMQ searches for the `user_name` claim first and if it is not found, RabbitMQ searches for the `email`. If these are not found, RabbitMQ uses its default lookup mechanism which first looks for `sub` and then `client_id`. - ### Discovery endpoint parameters {#discovery-endpoint-params} Some OAuth 2.0 providers requires certain query parameters in the OpenId Discovery endpoint. For instance, Microsoft Entra ID requires a query parameter called `appid` when the application uses custom signing keys. The discovery endpoint returns an OpenId configuration tailored for the application that matches the `appid`. @@ -628,10 +719,58 @@ auth_oauth2.discovery_endpoint_params.param2 = value2 ``` This is the URL built to access the OpenId Discovery endpoint: -``` + +```console https://myissuer.com/v2/.well-known/authorization-server?param1=value1¶m2=value2 ``` +### Requesting Party Token {#requesting-party-token} + +A **Requesting Party Token (RPT)** is a special OAuth 2.0 **access token** issued by an +**Authorization Server** in the +[User-Managed Access (UMA) 2.0](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html) +framework. It is used by a **Requesting Party** (such as an application or user) to access a +protected resource on a Resource Server such as RabbitMQ, after being authorized based on +resource-owner policies. + +[Keycloak](./oauth2-examples-keycloak) is one of the Authorization Servers that issues this type of +token. An RPT is typically a JWT with permissions claims under a claim called `authorization`. See +the example below. The rest of the claims have been removed from the token for brevity: + +```json +{ + "authorization": { + "permissions": [ + { + "scopes": [ + "rabbitmq-resource.read:*/*" + ], + "rsid": "2c390fe4-02ad-41c7-98a2-cebb8c60ccf1", + "rsname": "allvhost" + }, + { + "scopes": [ + "rabbitmq-resource:vhost1/*" + ], + "rsid": "e7f12e94-4c34-43d8-b2b1-c516af644cee", + "rsname": "vhost1" + }, + { + "rsid": "12ac3d1c-28c2-4521-8e33-0952eff10bd9", + "scopes": [ + "rabbitmq-resource.tag:administrator" + ] + } + ] + }, + "scope": "email profile", +} +``` + +RabbitMQ supports this token format. It reads all the scopes in all the `permissions` claims. If the +token also contains the standard `scope` claim, RabbitMQ adds it to the list of scopes presented by +the token. + ### Rich Authorization Request {#rich-authorization-request} The [Rich Authorization Request](https://oauth.net/2/rich-authorization-requests/) extension provides a way for @@ -671,7 +810,6 @@ string `finance`, use `^finance$`. The second permission grants the `administrator` user tag in two clusters, `finance` and `inventory`. Other supported user tags as `management`, `policymaker` and `monitoring`. - #### Type field In order for a RabbitMQ node to accept a permission, its value must match that @@ -684,6 +822,7 @@ The `locations` field can be either a string containing a single location or a J zero or many locations. A location consists of a list of key-value pairs separated by forward slash `/` character. Here is the format: + ```bash cluster:[/vhost:][/queue:|/exchange:][/routing-key:] ```