# Configuring Keycloak

The MCP Gateway can use Keycloak as the identity provider behind its downstream
OAuth flow. The `mcp-keycloak-oauth-inbound` policy is a Keycloak-friendly
wrapper around the generic `mcp-oauth-inbound` policy: provide your Keycloak
base URL, a realm name, a client ID, and a client secret, and the policy derives
the realm-issuer URL, JWKS URL, and authorize and token URLs from Keycloak's
standard OpenID Connect endpoint layout.

This guide walks through the Keycloak admin console setup, then wires the policy
into a gateway project. Read the [authentication overview](./overview.mdx) first
for the two-layer OAuth model.

## Set up Keycloak

The MCP Gateway acts as an OAuth 2.1 authorization server in front of Keycloak.
Keycloak handles browser login; the gateway issues its own access tokens that
bind to MCP routes.

### Create a client in the realm

1. In the Keycloak admin console, switch to the realm you want the gateway to
   use.
2. Open **Clients** and click **Create client**.
3. Give the client a Client ID (for example, `zuplo-mcp-gateway`) and click
   **Next**.
4. Enable **Client authentication** (so the client requires a secret) and leave
   **Standard flow** (authorization code) enabled. Disable **Service accounts
   roles** and **Direct access grants** — the gateway only needs the browser
   code flow.
5. Click **Next**.
6. Set **Valid redirect URIs** to `https://<gateway-host>/oauth/callback`. Add
   `http://localhost:9000/oauth/callback` for local development.
7. Set **Web origins** to `https://<gateway-host>` (and `http://localhost:9000`
   for local dev).
8. Click **Save**.

### Note the client credentials

Open the client's **Credentials** tab. Copy the **Client secret**. The **Client
ID** is the value you set above.

## Wire the policy into the gateway

Add the policy to `config/policies.json`:

```json
{
  "name": "keycloak-managed-oauth",
  "policyType": "mcp-keycloak-oauth-inbound",
  "handler": {
    "module": "$import(@zuplo/runtime/mcp-gateway)",
    "export": "McpKeycloakOAuthInboundPolicy",
    "options": {
      "keycloakBaseUrl": "$env(KEYCLOAK_BASE_URL)",
      "realm": "$env(KEYCLOAK_REALM)",
      "clientId": "$env(KEYCLOAK_CLIENT_ID)",
      "clientSecret": "$env(KEYCLOAK_CLIENT_SECRET)"
    }
  }
}
```

:::caution

`keycloakBaseUrl` is the Keycloak server root, without `/realms/{realm}` — set
the realm separately on the `realm` option. If your deployment uses a path
prefix (legacy `/auth`), include that in `keycloakBaseUrl`
(`https://sso.example.com/auth`).

:::

Attach the policy to each MCP route in `config/routes.oas.json` and register the
gateway plugin in `modules/zuplo.runtime.ts` (see
[Configuring Auth0](./configuring-auth0.mdx#wire-the-policy-into-the-gateway)
for the route and plugin patterns — they're identical across all wrappers).

## What the wrapper derives

Given `keycloakBaseUrl: "https://sso.example.com"` and
`realm: "customer-portal"`:

| Generic field           | Derived value                                                                  |
| ----------------------- | ------------------------------------------------------------------------------ |
| `oidc.issuer`           | `https://sso.example.com/realms/customer-portal`                               |
| `oidc.jwksUrl`          | `https://sso.example.com/realms/customer-portal/protocol/openid-connect/certs` |
| `browserLogin.url`      | `https://sso.example.com/realms/customer-portal/protocol/openid-connect/auth`  |
| `browserLogin.tokenUrl` | `https://sso.example.com/realms/customer-portal/protocol/openid-connect/token` |

## Test the configuration

The fastest sanity check is to connect an MCP client:

1. Open Claude Desktop, Cursor, Claude Code, or another OAuth-aware MCP client.
2. Add a remote MCP server pointing at one of your `/mcp/{slug}` routes.
3. The client should redirect you to the Keycloak sign-in page. After login, the
   gateway's consent screen renders. Approve it.
4. The client receives an access token and can call `tools/list`.

If something fails partway through, walk the flow manually using the
[manual OAuth testing guide](./manual-oauth-testing.mdx) — it exercises every
endpoint with `curl` so you can see the raw responses.

## Common issues

- **`keycloakBaseUrl` rejected at boot.** The value includes `/realms/...`.
  Strip the realm path; pass the realm name on the `realm` option instead.
- **`Invalid redirect_uri` from Keycloak.** The callback URL on the client
  doesn't match `https://<gateway-host>/oauth/callback`.
- **`Invalid client credentials`.** The client isn't a confidential client
  (Client authentication off), or the secret value doesn't match. Re-copy the
  secret from the **Credentials** tab.

## Related

- [Authentication overview](./overview.mdx)
- [Configuring a generic OIDC provider](./configuring-generic-oidc.mdx)
- [Per-user OAuth to upstream MCP servers](./upstream-oauth.mdx)
