Keycloak Components: Roles, Scopes, Client Scopes, and Audience
An app wants to call an MCP server that provides SSH tools. For example, a user asks the app to read a log file on a remote machine.
Before running the tool, the MCP server needs two answers:
- Who is asking? This is authentication.
- Are they allowed to do this? This is authorization.
Keycloak handles sign-in and issues an access token. The app sends that token to the MCP server. The MCP server checks it and decides whether to run the tool. A token is signed data: it lets the server check that the information came from the trusted issuer and was not changed.
In this example, the app connects to the MCP server over HTTPS. The MCP server then uses SSH to reach the remote machine. These are two separate connections, with separate authentication.
The whole flow
The user asks: “Read the server log.”
Checks the user and issues an access token.
Sends the tool request with the access token.
If allowed, uses its configured SSH credentials
to read the requested log.
This diagram focuses on the token’s journey; discovery and the login redirect steps are omitted. For HTTP MCP requests, the app sends the token in the Authorization header:
Authorization: Bearer <access-token>
The MCP server must validate the token and check that it was issued for that server. See the MCP authorization specification.
What do we create in Keycloak?
A user is the account signing in. A client is an application registered in Keycloak. A realm holds a set of users, clients, and roles.
For our example, we use two client registrations:
| Name | Purpose |
|---|---|
app |
The application that obtains a token for the signed-in user |
mcp_server |
The server whose tools we want to protect; its registration holds the server’s roles |
Keycloak creates the token. app requests it. mcp_server receives and checks it.
The names below are example configuration, not built-in Keycloak or MCP permissions.
Four concepts, four questions
What is the user allowed to do?
Example:
ssh_reader
What access is the app asking for?
Example:
ssh:read
What should Keycloak put in the token?
The settings behind
ssh:read
Which server is the token for?
Our SSH MCP server
Role: what is the user allowed to do?
Suppose Alice should be able to read logs, but should not be able to restart a service.
We create two roles under the mcp_server client:
| Role | What our MCP server allows |
|---|---|
ssh_reader |
Use the tool that reads approved log files |
ssh_operator |
Use the tool that restarts approved services |
We assign Alice ssh_reader. We do not assign her ssh_operator.
A role is a label on an account. The MCP server’s code gives that label meaning. Creating the role in Keycloak does not automatically restrict SSH commands. Our server must check the role before running the relevant tool.
These are client roles, because they belong to mcp_server. A realm role is defined at the realm level instead, such as employee.
With the usual Keycloak role mappers, Alice’s client role appears here in the access token:
{
"resource_access": {
"mcp_server": {
"roles": ["ssh_reader"]
}
}
}
A role called ssh_reader under app is a different role. If the server checks mcp_server’s roles, putting the same name under app does not satisfy that check. Keycloak’s token role mappings.
Scope: what access is the app asking for?
The app wants to read logs. It requests a scope named ssh:read when obtaining the token.
For a sign-in using OpenID Connect, the scope part of the request could be:
scope=openid ssh:read
openid requests an OpenID Connect login. ssh:read is our custom name for the access needed to read logs.
Requesting access does not grant it. Alice still needs the required role. The server still checks whether the requested tool is allowed.
The distinction is small but useful:
| Role | Scope |
|---|---|
Alice has ssh_reader. |
The app requests ssh:read. |
| Assigned to the account. | Requested for this access. |
Even if Alice can both read logs and restart services, an app that only needs to read logs can request narrower access. Our MCP server must enforce that limit using the granted scopes and its tool rules.
Client scope: what should Keycloak put in the token?
How does Keycloak know what to do with the name ssh:read?
We create a client scope with that name. It is a saved group of settings in Keycloak. We then link it to app.
A client scope can contain:
- Protocol mappers: settings that add information to a token, such as the intended audience.
- Role scope mappings: settings that limit which of the user’s roles may enter the token.
Think of the difference this way: scope=ssh:read is the request. The ssh:read client scope is the configuration Keycloak applies. The configuration itself is not sent to the MCP server; the resulting token is.
A linked client scope can be default, so it applies without an explicit request, or optional, so the app must request it. User role restrictions can still prevent it from applying. Its Include in token scope setting controls whether its name appears in the token’s scope field. Keycloak’s client scopes.
A concrete setup for reading logs
For this example, configure:
- Alice has the
mcp_serverclient rolessh_reader. apphas Full Scope Allowed disabled.- The
ssh:readclient scope allows themcp_serverrolessh_readerthrough its role scope mappings. - That client scope has an Audience mapper adding
https://ssh-mcp.example.com/mcp, our MCP server’s resource identifier. - Link
ssh:readtoappas optional, with Include in token scope enabled. Keep the usual role mappers available toappso eligible roles are written to the access token.
The example assumes no other role scope mappings allow extra roles. Here is the role filtering step:
Alice's roles Roles allowed for this token
┌────────────────────┐ ┌────────────────────┐
│ ssh_reader │ │ ssh_reader │
│ ssh_operator │ │ │
└──────────┬─────────┘ └──────────┬─────────┘
└──────────────┬──────────────┘
▼
Role in the token
┌────────────────────┐
│ ssh_reader │
└────────────────────┘
In this diagram, Alice has both roles to show the filter at work. Only ssh_reader passes through. A role scope mapping never gives Alice a role she does not already have. With Full Scope Allowed enabled, that role filter does not limit her roles. Keycloak’s role scope mappings.
Audience: which server is this token for?
The app may talk to several servers. A token intended for another API should not work at the SSH MCP server just because both trust Keycloak.
The audience names the intended receiver. It appears in the token’s aud field:
{
"aud": ["https://ssh-mcp.example.com/mcp"]
}
Our MCP server checks for that audience. Here we use its resource URL as the identifier. The Keycloak client registration is still named mcp_server; the Audience mapper connects our configuration to the audience value we want.
Audience answers “Is this token for me?” A role answers “What may this user do?” The server needs both checks.
Keycloak can set an audience explicitly with an Audience mapper or derive client audiences from eligible client roles using Audience Resolve. A scope name does not automatically become an audience. Keycloak’s audience documentation.
Put it together: app → mcp_server
With the example configuration, Alice signs in and the app requests ssh:read for the SSH MCP resource. A simplified access-token payload could look like this:
{
"iss": "https://auth.example.com/realms/team",
"sub": "alice-user-id",
"azp": "app",
"aud": ["https://ssh-mcp.example.com/mcp"],
"scope": "openid ssh:read",
"resource_access": {
"mcp_server": {
"roles": ["ssh_reader"]
}
}
}
This shows selected fields, not a complete usable token. Expiry and signature details are omitted.
| Field | Simple meaning |
|---|---|
iss |
Keycloak realm that issued the token |
sub |
User this token represents |
azp |
App that obtained it in this flow |
aud |
Server it is intended for |
scope |
Granted scope names included in the token |
resource_access.mcp_server.roles |
User roles included for the MCP server |
Before reading the log, our MCP server checks:
- The token’s signature, trusted issuer, and expiry are valid.
audincludes its expected resource identifier.scopeincludesssh:read.resource_access.mcp_server.rolesincludesssh_reader.- The requested host and log file are allowed by the tool’s own rules.
| Request | Result in our example |
|---|---|
| Read an approved log with all checks passing | Allowed |
| Read a log with a token intended for a different API | Rejected |
Read a log with ssh:read but no ssh_reader role |
Rejected |
| Restart a service with this read-only token | Rejected |
After authorization, the MCP server uses its configured SSH credentials. The Keycloak token controls access to the MCP tool. SSH authentication controls access to the remote machine. The token is not an SSH key.
To inspect what Keycloak actually produces, use Client scopes → Evaluate for app, select the user and scopes, and look at the generated access token. Check its audience, scopes, and roles against what your MCP server expects. Keycloak’s client scope evaluation.