DEV Community

Cover image for How To Configure Audience In Keycloak
infohash
infohash

Posted on

How To Configure Audience In Keycloak

Keycloak has audience support that you can use to limit the recipients of your access token. Its documentation has a good example on why you should enforce audience before authorizing a request.

In the environment where trust among services is low, you may encounter this scenario:

  1. A frontend client application requires authentication against Keycloak.
  2. Keycloak authenticates a user.
  3. Keycloak issues a token to the application.
  4. The application uses the token to invoke an untrusted service.
  5. The untrusted service returns the response to the application. However, it keeps the applications token.
  6. The untrusted service then invokes a trusted service using the applications token. This results in broken security as the untrusted service misuses the token to access other services on behalf of the client application.

Setup

Installation

I'm running Keycloak version 22.0.0 in development mode in docker. You can deploy it in docker by running this command.

docker run -d --name Keycloak -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:22.0.0 start-dev
Enter fullscreen mode Exit fullscreen mode

Client Creation

Let's create 3 clients Alice, Bob & Charles and assume that none of them trust each other. To create a client, go to Clients > Create client.

client creation

For the purpose of this demo, in order to obtain the access token from CLI, the only authentication flow I have enabled is Service accounts roles which enables client credentials flow. Now let's start.

client credentials flow

Problem

Alice wants to invoke APIs of Bob. She obtains an access token using client credentials flow.

curl --request POST --url http://localhost:8080/realms/master/protocol/openid-connect/token --header 'Authorization: Basic d2F0Y2hkb2dzOnlMdGVLR0U4VzJiWVVObjRYbEUwMHFNaHlZNVdiemhZ' --header 'Content-Type: application/x-www-form-urlencoded' --data grant_type=client_credentials
Enter fullscreen mode Exit fullscreen mode
{
  "exp": 1696069210,
  "iat": 1696068610,
  "jti": "03e260af-5b79-4e84-885d-7f08307c7f29",
  "iss": "http://localhost:8080/realms/master",
  "sub": "9baa1d4f-982c-4d1b-b1bd-af8455234d29",
  "typ": "Bearer",
  "azp": "alice",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "scope": "",
  "clientHost": "172.17.0.1",
  "clientAddress": "172.17.0.1",
  "client_id": "alice"
}
Enter fullscreen mode Exit fullscreen mode

In the above output, scope is empty because there are no default scopes assigned to Alice yet. The aud claim which is the audience of her access token is also missing. If Alice sends this token to Bob, Bob can do 2 things with it:

  1. Bob can reject it for not including him as the intended recipient of the token.

  2. Bob can misuse the token by reusing it for invoking APIs of other services that do not validate the audience which is why you should always enforce audience for your own services.

Now let's configure audience for Alice so that she can set the intended recipient(s) of her access token to prevent misuse. An access token can have multiple audiences.

Configuring Audience

Custom Client Scope

Let's create a client scope untrusted-audience which I will assign to Alice as optional. It should be optional because Alice should be able to decide the intended recipient(s) of the access token by specifying different scopes. To create a client scope, go to Client scopes > Create client scope.

custom client scope

Hardcoded Audience

After creating it, inside this scope you will see a tab called Mappers which is empty. This is where an Audience mapper will be created that will include the client ID of Bob.

Mappers

Go to Mappers > Configure a new mapper > Audience. In the Included Client Audience field, I have added the client ID of Bob. There is another similar field next to it called Included Custom Audience. This is used when you want to add some custom name as audience like the subdomain of your web service or 3rd party service instead of client ID. How do you validate the aud claim is up to you so any arbitrary name is allowed in this field if you are not specifying the client ID.

Bob as audience

Assigning Client Scope To Alice

Go to Clients > alice > Client scopes (tab) > Add client scope > Select untrusted-audience and assign it as an optional client scope.

client scope assignment

Result

With Scope

curl --request POST --url http://localhost:8080/realms/master/protocol/openid-connect/token --header 'Authorization: Basic YWxpY2U6RHl3a2daZWZtNVFJVU9NbW0ydzdCY2dMbzB6dzI5TTc=' --header 'Content-Type: application/x-www-form-urlencoded' --data grant_type=client_credentials --data scope=untrusted-audience
Enter fullscreen mode Exit fullscreen mode
{
  "exp": 1696072871,
  "iat": 1696072271,
  "jti": "ea0d8b36-4946-497f-ab3b-e5dc07f85c62",
  "iss": "http://localhost:8080/realms/master",
  "aud": "bob",
  "sub": "9baa1d4f-982c-4d1b-b1bd-af8455234d29",
  "typ": "Bearer",
  "azp": "alice",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "scope": "untrusted-audience",
  "clientHost": "172.17.0.1",
  "clientAddress": "172.17.0.1",
  "client_id": "alice"
}
Enter fullscreen mode Exit fullscreen mode

Now Bob can verify that he is the intended recipient of this token. Also, Bob cannot misuse it for invoking API of Charles if Charles is validating the aud claim. Similarly, to invoke API of Charles, Alice can create another optional client scope and create an audience mapper in it to add Charles' client ID. In this way, Alice will have control over who to include in the audience by accordingly setting the scope.

Multiple Audiences

If Bob and Charles trusts each other and if Alice wants to use the same access token to invoke APIs of both Bob and Charles, instead of creating optional client scope for each, she can just create another audience mapper to the existing client scope.

multiple audiences

curl --request POST --url http://localhost:8080/realms/master/protocol/openid-connect/token --header 'Authorization: Basic YWxpY2U6RHl3a2daZWZtNVFJVU9NbW0ydzdCY2dMbzB6dzI5TTc=' --header 'Content-Type: application/x-www-form-urlencoded' --data grant_type=client_credentials --data scope=untrusted-audience
Enter fullscreen mode Exit fullscreen mode
{
  "exp": 1696094105,
  "iat": 1696093505,
  "jti": "8e90c92b-28d8-4da2-a527-37bc7d23fcec",
  "iss": "http://localhost:8080/realms/master",
  "aud": [
    "bob",
    "charles"
  ],
  "sub": "9baa1d4f-982c-4d1b-b1bd-af8455234d29",
  "typ": "Bearer",
  "azp": "alice",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "scope": "untrusted-audience",
  "clientHost": "172.17.0.1",
  "clientAddress": "172.17.0.1",
  "client_id": "alice"
}
Enter fullscreen mode Exit fullscreen mode

Without Scope

I have omitted scope as the parameter so no audience will be included.

curl --request POST --url http://localhost:8080/realms/master/protocol/openid-connect/token --header 'Authorization: Basic d2F0Y2hkb2dzOnlMdGVLR0U4VzJiWVVObjRYbEUwMHFNaHlZNVdiemhZ' --header 'Content-Type: application/x-www-form-urlencoded' --data grant_type=client_credentials
Enter fullscreen mode Exit fullscreen mode
{
  "exp": 1696097766,
  "iat": 1696097166,
  "jti": "4966e248-5467-4412-b6d1-8d08d3807750",
  "iss": "http://localhost:8080/realms/master",
  "sub": "9baa1d4f-982c-4d1b-b1bd-af8455234d29",
  "typ": "Bearer",
  "azp": "alice",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "scope": "",
  "clientHost": "172.17.0.1",
  "clientAddress": "172.17.0.1",
  "client_id": "alice"
}
Enter fullscreen mode Exit fullscreen mode

Automatically Add Audience

If you have a large number of clients in your organization, it becomes impractical to create and manage hardcoded audience for each. The Audience Resolve protocol mapper automatically adds client ID as an audience if following conditions are true:

  1. The client must have at least one client role created on itself. (Let's call this role as common-client-role, I'll use this name later.)

  2. The client that you are using must have a role-scope mapping for that role.

  3. The user must be assigned that role.

Let's satisfy all 3 conditions.

Creating a client role on Bob

common client role on bob

Creating an optional client scope for Alice

optional client scope

Go to this client scope, there's a tab called Scope which is empty. There, click on Assign role, filter roles by client and assign the role that you created on Bob. This is called role-scope mapping.

role-scope mapping tab

role-scope mapping

Client Scope Assignment

Assign this client scope to alice as optional.

scope assignment

User Role Assignment

I am using client credentials flow to obtain an access token, so who and where is the user in my case? When you enable Service Account Roles to enable Client Credentials Flow for your client, Keycloak automatically creates a user called service-account-clientID. If I go to Alice, there's a tab called Service account roles. This is where I will assign the role common-client-role to the user service-account-alice.

user role assignment

role assigned to the user

user role assignment tab

Result

With Scope

curl --request POST --url http://localhost:8080/realms/master/protocol/openid-connect/token --header 'Authorization: Basic YWxpY2U6RHl3a2daZWZtNVFJVU9NbW0ydzdCY2dMbzB6dzI5TTc=' --header 'Content-Type: application/x-www-form-urlencoded' --data grant_type=client_credentials --data scope=auto-add-audience
Enter fullscreen mode Exit fullscreen mode
{
  "exp": 1696112410,
  "iat": 1696111810,
  "jti": "eeb61dbe-3a49-4147-9191-c34c39598a52",
  "iss": "http://localhost:8080/realms/master",
  "aud": "bob",
  "sub": "9baa1d4f-982c-4d1b-b1bd-af8455234d29",
  "typ": "Bearer",
  "azp": "alice",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "resource_access": {
    "bob": {
      "roles": [
        "common-client-role"
      ]
    }
  },
  "scope": "auto-add-audience",
  "clientHost": "172.17.0.1",
  "clientAddress": "172.17.0.1",
  "client_id": "alice"
}
Enter fullscreen mode Exit fullscreen mode

Multiple Audiences

To also include Charles as an audience, Alice can create and assign another optional client scope on herself and create a role-scope mapping in that client scope and on the user. For role-scope mapping, just like Bob, Charles also has to create at least one client role on himself.

Alice can accordingly specify scopes to include either or both. Here I'm sending 2 scopes to add both Bob's and Charles's client ID as an audience.

curl --request POST --url http://localhost:8080/realms/master/protocol/openid-connect/token --header 'Authorization: Basic YWxpY2U6RHl3a2daZWZtNVFJVU9NbW0ydzdCY2dMbzB6dzI5TTc=' --header 'Content-Type: application/x-www-form-urlencoded' --data 'grant_type=client_credentials&scope=auto-add-audience+another-scope-to-include-charles'
Enter fullscreen mode Exit fullscreen mode
{
  "exp": 1696114210,
  "iat": 1696113610,
  "jti": "05a2906b-f2d2-4011-970a-7cff0be38b6e",
  "iss": "http://localhost:8080/realms/master",
  "aud": [
    "bob",
    "charles"
  ],
  "sub": "9baa1d4f-982c-4d1b-b1bd-af8455234d29",
  "typ": "Bearer",
  "azp": "alice",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "resource_access": {
    "bob": {
      "roles": [
        "common-client-role"
      ]
    },
    "charles": {
      "roles": [
        "charles-role"
      ]
    }
  },
  "scope": "auto-add-audience another-scope-to-include-charles",
  "clientHost": "172.17.0.1",
  "clientAddress": "172.17.0.1",
  "client_id": "alice"
}
Enter fullscreen mode Exit fullscreen mode

Multiple role-scope mapping

Alice can also create multiple role-scope mappings within a single client scope to add both Bob and Charles as an audience if Bob and Charles trust each other. Trust between Bob and Charles matters because Charles will know that Bob will not misuse Alice's token to invoke his API.

multiple role-scope mapping

curl --request POST --url http://localhost:8080/realms/master/protocol/openid-connect/token --header 'Authorization: Basic YWxpY2U6RHl3a2daZWZtNVFJVU9NbW0ydzdCY2dMbzB6dzI5TTc=' --header 'Content-Type: application/x-www-form-urlencoded' --data grant_type=client_credentials --data scope=auto-add-audience
Enter fullscreen mode Exit fullscreen mode
{
  "exp": 1696097015,
  "iat": 1696096955,
  "jti": "02abe148-7e41-429a-91e3-a6c60249a3af",
  "iss": "http://localhost:8080/realms/master",
  "aud": [
    "bob",
    "charles"
  ],
  "sub": "9baa1d4f-982c-4d1b-b1bd-af8455234d29",
  "typ": "Bearer",
  "azp": "alice",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "resource_access": {
    "bob": {
      "roles": [
        "common-client-role"
      ]
    },
    "charles": {
      "roles": [
        "charles-role"
      ]
    }
  },
  "scope": "auto-add-audience",
  "clientHost": "172.17.0.1",
  "clientAddress": "172.17.0.1",
  "client_id": "alice"
}
Enter fullscreen mode Exit fullscreen mode

Without Scope

curl --request POST --url http://localhost:8080/realms/master/protocol/openid-connect/token --header 'Authorization: Basic YWxpY2U6RHl3a2daZWZtNVFJVU9NbW0ydzdCY2dMbzB6dzI5TTc=' --header 'Content-Type: application/x-www-form-urlencoded' --data grant_type=client_credentials
Enter fullscreen mode Exit fullscreen mode
{
  "exp": 1696116071,
  "iat": 1696115471,
  "jti": "33cba1d0-eae3-415c-bc16-f0ae804bc67c",
  "iss": "http://localhost:8080/realms/master",
  "sub": "9baa1d4f-982c-4d1b-b1bd-af8455234d29",
  "typ": "Bearer",
  "azp": "alice",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "scope": "",
  "clientHost": "172.17.0.1",
  "clientAddress": "172.17.0.1",
  "client_id": "alice"
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)