Skip to content

Commit f2eda1a

Browse files
Simplify Keycloak provider to minimal DCR proxy (option 1 from PR #1937)
This commit drastically simplifies the Keycloak provider from a full OAuth proxy to a minimal DCR-only workaround, implementing the suggested "option 1" approach from PR feedback. **Architectural change: 3 proxy routes → 1 proxy route** BEFORE (full OAuth proxy): - /authorize proxy (authorization endpoint with scope injection) - /register proxy (full DCR with request/response modifications) - /.well-known/oauth-authorization-server (metadata with modifications) - Complex OIDC discovery and configuration management AFTER (minimal DCR proxy): - /register proxy (fixes only token_endpoint_auth_method field) - /.well-known/oauth-authorization-server (simple forwarding) - Hard-coded Keycloak URL patterns (no OIDC discovery) - Authorization flows, token issuance, and validation go directly to Keycloak **Why the minimal proxy is needed:** Keycloak ignores the client's requested token_endpoint_auth_method and always returns "client_secret_basic", but MCP requires "client_secret_post" per RFC 9110. The minimal proxy intercepts only DCR responses to fix this single field. **Changes:** - Reduce full OAuth proxy to minimal DCR proxy (as detailed above) - Simplify realm configuration: * Remove clientProfiles/clientPolicies (executors unavailable in Keycloak 26.3+) * Use realm-level default scopes and explicit clientScopes definitions - Update documentation: * Add MCP Compatibility Note explaining the workaround * Update MCP Inspector instructions with scope requirements - Add integration test proving Keycloak's DCR limitation and verifying the fix Addresses: #1937 (comment)
1 parent 3ff1a95 commit f2eda1a

File tree

7 files changed

+606
-945
lines changed

7 files changed

+606
-945
lines changed

docs/integrations/keycloak.mdx

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
1212

1313
This guide shows you how to secure your FastMCP server using **Keycloak OAuth**. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern with Dynamic Client Registration (DCR), where Keycloak handles user login and your FastMCP server validates the tokens.
1414

15+
<Note>
16+
**MCP Compatibility Note**: While Keycloak has built-in support for Dynamic Client Registration (DCR), there is an important MCP compatibility limitation. Keycloak ignores the client's requested `token_endpoint_auth_method` and always returns `client_secret_basic`, but the MCP specification requires `client_secret_post`. The KeycloakAuthProvider works around this issue by acting as a minimal proxy that intercepts DCR responses from Keycloak and fixes this field automatically. All other OAuth functionality (authorization, token issuance, user authentication) is handled directly by Keycloak.
17+
</Note>
18+
1519
## Configuration
1620

1721
### Prerequisites
@@ -51,11 +55,13 @@ If you prefer using Docker Compose instead, you may want to have a look at the [
5155
1. Download the FastMCP Keycloak realm configuration: [`realm-fastmcp.json`](https://github.com/jlowin/fastmcp/blob/main/examples/auth/keycloak_auth/keycloak/realm-fastmcp.json)
5256
2. Open the file in a text editor and customize as needed:
5357
- **Realm name and display name**: Change `"realm": "fastmcp"` and `"displayName": "FastMCP Realm"` to match your project
54-
- **Trusted hosts configuration**: Look for `"trusted-hosts"` section and update IP addresses if needed
58+
- **Trusted hosts configuration**: Look for `"trusted-hosts"` section and update IP addresses for secure client registration:
5559
- `localhost`: For local development
5660
- `172.17.0.1`: Docker network gateway IP address (required when Keycloak is run with Docker and MCP server directly on localhost)
5761
- `172.18.0.1`: Docker Compose network gateway IP address (required when Keycloak is run with Docker Compose and MCP server directly on localhost)
62+
- `github.com`: Required for MCP Inspector compatibility
5863
- For production, replace these with your actual domain names
64+
- **Default scopes**: The configuration sets `openid`, `profile`, and `email` as default scopes for all clients
5965
3. **Review the test user**: The file includes a test user (`testuser` with password `password123`). You may want to:
6066
- Change the credentials for security
6167
- Replace with more meaningful user accounts
@@ -79,20 +85,9 @@ If you prefer using Docker Compose instead, you may want to have a look at the [
7985
- Click the **Create** button
8086

8187
That's it! This single action will create the `fastmcp` realm and instantly configure everything from the file:
82-
- The realm settings (including user registration policies)
88+
- The realm settings with default scopes for all clients (`openid`, `profile`, `email`)
89+
- The "Trusted Hosts" client registration policy for secure Dynamic Client Registration (DCR)
8390
- The test user with their credentials
84-
- All the necessary Client Policies and Client Profiles required to support Dynamic Client Registration (DCR)
85-
- Trusted hosts configuration for secure client registration
86-
87-
<Note>
88-
You may see this warning in the Keycloak logs during import:
89-
```
90-
Failed to deserialize client policies in the realm fastmcp.Fallback to return empty profiles.
91-
Details: Unrecognized field "profiles" (class org.keycloak.representations.idm.ClientPoliciesRepresentation),
92-
not marked as ignorable (2 known properties: "policies","globalPolicies"])
93-
```
94-
This is due to Keycloak's buggy/strict parser not recognizing valid older JSON formats but doesn't seem to impact functionality and can be safely ignored.
95-
</Note>
9691
</Step>
9792

9893
<Step title="Verify the Configuration">
@@ -183,6 +178,28 @@ When you run the client for the first time:
183178
The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
184179
</Info>
185180

181+
### Testing with MCP Inspector
182+
183+
The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) provides an interactive web UI to explore and test your MCP server.
184+
185+
**Prerequisites**: Node.js must be installed on your system.
186+
187+
1. Launch the Inspector:
188+
```bash
189+
npx -y @modelcontextprotocol/inspector
190+
```
191+
192+
2. In the Inspector UI (opens in your browser), enter your server URL: `http://localhost:8000/mcp`
193+
3. In the **Authentication** section's **OAuth 2.0 Flow** area, locate the **Scope** field
194+
4. In the **Scope** field, enter: `openid profile` (these must exactly match the `required_scopes` configured in your KeycloakAuthProvider or `FASTMCP_SERVER_AUTH_KEYCLOAK_REQUIRED_SCOPES` environment variable)
195+
5. Click **Connect** - your browser will open for Keycloak authentication
196+
6. Log in with your test user credentials (e.g., `testuser` / `password123`)
197+
7. After successful authentication, the Inspector will connect to your server
198+
199+
<Note>
200+
The MCP Inspector requires explicit scope configuration because it doesn't automatically request the scopes defined in Keycloak's client policies. This is the correct OAuth behavior - clients should explicitly request the scopes they need.
201+
</Note>
202+
186203
### Automatic Client Re-registration
187204

188205
<Info>
@@ -266,7 +283,7 @@ from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
266283

267284
# Custom JWT verifier with specific audience
268285
custom_verifier = JWTVerifier(
269-
jwks_uri="http://localhost:8080/realms/fastmcp/.well-known/jwks.json",
286+
jwks_uri="http://localhost:8080/realms/fastmcp/protocol/openid-connect/certs",
270287
issuer="http://localhost:8080/realms/fastmcp",
271288
audience="my-specific-client",
272289
required_scopes=["api:read", "api:write"]

examples/auth/keycloak_auth/README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,15 @@ Manually import the realm:
7575
```
7676

7777
2. In the Inspector UI (opens in your browser):
78-
- Click "Add Server"
7978
- Enter server URL: `http://localhost:8000/mcp`
80-
- Select connection type: **"Direct"** (connects directly to the HTTP server)
81-
- Click "Connect"
82-
- The Inspector will automatically handle the OAuth flow and open Keycloak for authentication
83-
- Once authenticated, you can interactively explore available tools and test them
79+
- In the **Authentication** section's **OAuth 2.0 Flow** area, locate the **Scope** field
80+
- In the **Scope** field, enter: `openid profile` (these must exactly match the `required_scopes` configured in your KeycloakAuthProvider or `FASTMCP_SERVER_AUTH_KEYCLOAK_REQUIRED_SCOPES` environment variable)
81+
- Click **Connect**
82+
- Your browser will open for Keycloak authentication
83+
- Log in with your test user credentials (e.g., `testuser` / `password123`)
84+
- After successful authentication, you can interactively explore available tools and test them
85+
86+
**Note**: The MCP Inspector requires explicit scope configuration because it doesn't automatically request scopes. This is correct OAuth behavior - clients should explicitly request the scopes they need.
8487

8588
The Inspector is particularly useful for:
8689
- Exploring the server's capabilities without writing code

examples/auth/keycloak_auth/keycloak/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
keycloak:
3-
image: quay.io/keycloak/keycloak:26.3
3+
image: quay.io/keycloak/keycloak:26.4
44
container_name: keycloak-fastmcp
55
environment:
66
# Admin credentials

examples/auth/keycloak_auth/keycloak/realm-fastmcp.json

Lines changed: 132 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
"realm": "fastmcp",
33
"displayName": "FastMCP Realm",
44
"enabled": true,
5-
"keycloakVersion": "26.3.5",
5+
"keycloakVersion": "26.4.5",
66
"registrationAllowed": true,
7+
"defaultDefaultClientScopes": ["openid", "profile", "email"],
78
"defaultOptionalClientScopes": ["offline_access"],
89
"components": {
910
"org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [
@@ -47,49 +48,138 @@
4748
"realmRoles": ["offline_access"]
4849
}
4950
],
50-
"clientPolicies": {
51-
"policies": [
52-
{
53-
"name": "Allowed Client Scopes",
54-
"enabled": true,
55-
"conditions": [
56-
{
57-
"condition": "client-scopes",
58-
"configuration": {
59-
"allowed-client-scopes": [
60-
"openid",
61-
"profile",
62-
"email",
63-
"offline_access"
64-
]
65-
}
51+
"clientScopes": [
52+
{
53+
"name": "openid",
54+
"description": "OpenID Connect scope for interoperability with OpenID Connect",
55+
"protocol": "openid-connect",
56+
"attributes": {
57+
"include.in.token.scope": "true",
58+
"display.on.consent.screen": "false"
59+
},
60+
"protocolMappers": [
61+
{
62+
"name": "sub",
63+
"protocol": "openid-connect",
64+
"protocolMapper": "oidc-sub-mapper",
65+
"consentRequired": false,
66+
"config": {}
67+
}
68+
]
69+
},
70+
{
71+
"name": "profile",
72+
"description": "OpenID Connect built-in scope: profile",
73+
"protocol": "openid-connect",
74+
"attributes": {
75+
"include.in.token.scope": "true",
76+
"display.on.consent.screen": "true",
77+
"consent.screen.text": "${profileScopeConsentText}"
78+
},
79+
"protocolMappers": [
80+
{
81+
"name": "username",
82+
"protocol": "openid-connect",
83+
"protocolMapper": "oidc-usermodel-attribute-mapper",
84+
"consentRequired": false,
85+
"config": {
86+
"userinfo.token.claim": "true",
87+
"user.attribute": "username",
88+
"id.token.claim": "true",
89+
"access.token.claim": "true",
90+
"claim.name": "preferred_username",
91+
"jsonType.label": "String"
92+
}
93+
},
94+
{
95+
"name": "full name",
96+
"protocol": "openid-connect",
97+
"protocolMapper": "oidc-full-name-mapper",
98+
"consentRequired": false,
99+
"config": {
100+
"id.token.claim": "true",
101+
"access.token.claim": "true",
102+
"userinfo.token.claim": "true"
103+
}
104+
},
105+
{
106+
"name": "given name",
107+
"protocol": "openid-connect",
108+
"protocolMapper": "oidc-usermodel-attribute-mapper",
109+
"consentRequired": false,
110+
"config": {
111+
"userinfo.token.claim": "true",
112+
"user.attribute": "firstName",
113+
"id.token.claim": "true",
114+
"access.token.claim": "true",
115+
"claim.name": "given_name",
116+
"jsonType.label": "String"
117+
}
118+
},
119+
{
120+
"name": "family name",
121+
"protocol": "openid-connect",
122+
"protocolMapper": "oidc-usermodel-attribute-mapper",
123+
"consentRequired": false,
124+
"config": {
125+
"userinfo.token.claim": "true",
126+
"user.attribute": "lastName",
127+
"id.token.claim": "true",
128+
"access.token.claim": "true",
129+
"claim.name": "family_name",
130+
"jsonType.label": "String"
66131
}
67-
]
132+
}
133+
]
134+
},
135+
{
136+
"name": "email",
137+
"description": "OpenID Connect built-in scope: email",
138+
"protocol": "openid-connect",
139+
"attributes": {
140+
"include.in.token.scope": "true",
141+
"display.on.consent.screen": "true",
142+
"consent.screen.text": "${emailScopeConsentText}"
68143
},
69-
{
70-
"name": "Allowed Client URIs",
71-
"enabled": true,
72-
"conditions": [
73-
{
74-
"condition": "client-uris",
75-
"configuration": {
76-
"uris": [
77-
"http://localhost:8000/*"
78-
]
79-
}
144+
"protocolMappers": [
145+
{
146+
"name": "email",
147+
"protocol": "openid-connect",
148+
"protocolMapper": "oidc-usermodel-attribute-mapper",
149+
"consentRequired": false,
150+
"config": {
151+
"userinfo.token.claim": "true",
152+
"user.attribute": "email",
153+
"id.token.claim": "true",
154+
"access.token.claim": "true",
155+
"claim.name": "email",
156+
"jsonType.label": "String"
80157
}
81-
]
82-
}
83-
],
84-
"profiles": [
85-
{
86-
"name": "dynamic-client-registration-profile",
87-
"to-clients-dynamically-registered": true,
88-
"policies": [
89-
"Allowed Client URIs",
90-
"Allowed Client Scopes"
91-
]
158+
},
159+
{
160+
"name": "email verified",
161+
"protocol": "openid-connect",
162+
"protocolMapper": "oidc-usermodel-property-mapper",
163+
"consentRequired": false,
164+
"config": {
165+
"userinfo.token.claim": "true",
166+
"user.attribute": "emailVerified",
167+
"id.token.claim": "true",
168+
"access.token.claim": "true",
169+
"claim.name": "email_verified",
170+
"jsonType.label": "boolean"
171+
}
172+
}
173+
]
174+
},
175+
{
176+
"name": "offline_access",
177+
"description": "OpenID Connect built-in scope: offline_access",
178+
"protocol": "openid-connect",
179+
"attributes": {
180+
"consent.screen.text": "${offlineAccessScopeConsentText}",
181+
"display.on.consent.screen": "true"
92182
}
93-
]
94-
}
183+
}
184+
]
95185
}

0 commit comments

Comments
 (0)