🔧(keycloak) configure realm for the MCP server

Add to the dev realm:

- `docs-mcp-client`, the public PKCE client MCP clients authenticate
  with, with optional scopes `docs:documents:{search,read,create}` and
  `docs-mcp`.
- `docs-api`, the confidential client Django uses as introspection
  credentials (`OIDC_RS_CLIENT_ID/SECRET`).
- the `docs:documents:*` consent scopes and a `docs-mcp` audience-mapper
  scope that stamps the `docs-mcp` audience only when requested.

Point the resource-server env at the `impress` realm and the new
clients, allow the `docs-mcp-client` audience, and grant users
`offline_access` for refresh tokens.
This commit is contained in:
Anthony LC
2026-09-06 14:19:57 +02:00
parent 821b1bb1cc
commit 59bf86822c
2 changed files with 147 additions and 6 deletions
+135 -1
View File
@@ -56,7 +56,7 @@
"value": "impress"
}
],
"realmRoles": ["user"]
"realmRoles": ["user", "offline_access"]
},
{
"username": "user-e2e-chromium",
@@ -834,6 +834,88 @@
"offline_access",
"microprofile-jwt"
]
},
{
"clientId": "docs-mcp-client",
"name": "Docs MCP development client",
"description": "Public OAuth client used by MCP clients (MCP Inspector, Claude Code, Codex CLI ...) to authenticate against the docs-mcp server. Authorization Code + PKCE only, no client secret.",
"rootUrl": "",
"adminUrl": "",
"baseUrl": "",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": true,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["*"],
"webOrigins": ["+"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": true,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"publicClient": true,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "+"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"acr",
"roles",
"profile",
"email"
],
"optionalClientScopes": [
"offline_access",
"docs:documents:search",
"docs:documents:read",
"docs:documents:create",
"docs-mcp"
]
},
{
"clientId": "docs-api",
"name": "Docs API resource server",
"description": "Represents the Django Docs API. Confidential client whose credentials Django uses to call Keycloak's token introspection endpoint (OIDC_RS_CLIENT_ID/SECRET). See documentation/mcp_server.md.",
"rootUrl": "",
"adminUrl": "",
"baseUrl": "",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": true,
"clientAuthenticatorType": "client-secret",
"secret": "ThisIsAnExampleKeyForDevPurposeOnly",
"redirectUris": [],
"webOrigins": [],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": false,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"publicClient": false,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"acr",
"roles",
"profile",
"email"
],
"optionalClientScopes": []
}
],
"clientScopes": [
@@ -1359,6 +1441,58 @@
}
}
]
},
{
"name": "docs:documents:search",
"description": "Search the documents accessible to the authenticated user",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "Search your Docs documents"
}
},
{
"name": "docs:documents:read",
"description": "Read documents accessible to the authenticated user",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "Read your Docs documents"
}
},
{
"name": "docs:documents:create",
"description": "Create documents on behalf of the authenticated user",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "Create documents in Docs"
}
},
{
"name": "docs-mcp",
"description": "Adds the docs-mcp audience to the access token. Required for the MCP server to accept the token; only added when this scope is explicitly requested.",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "false",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "docs-mcp audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.custom.audience": "docs-mcp",
"id.token.claim": "false",
"access.token.claim": "true"
}
}
]
}
],
"defaultDefaultClientScopes": [
+12 -5
View File
@@ -52,13 +52,20 @@ OIDC_REDIRECT_ALLOWED_HOSTS="localhost:8083,localhost:3000"
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
# Resource Server Backend
OIDC_OP_URL=http://localhost:8083/realms/docs
OIDC_OP_INTROSPECTION_ENDPOINT = http://nginx:8083/realms/docs/protocol/openid-connect/token/introspect
# Also validates tokens the MCP server forwards from its callers (see core/mcp_api and
# documentation/mcp_server.md): the user's Keycloak token is introspected here with
# OIDC_RS_CLIENT_ID/SECRET (the `docs-api` confidential client) as credentials.
# OIDC_RS_AUDIENCE_CLAIM stays at its "client_id" default: it's process-wide (shared with
# the disabled external_api feature), and new resource-server callers just get added to
# OIDC_RS_ALLOWED_AUDIENCES. For a forwarded token that client_id is the public client the
# user authenticated as (docs-mcp-client).
OIDC_OP_URL=http://localhost:8083/realms/impress
OIDC_OP_INTROSPECTION_ENDPOINT=http://nginx:8083/realms/impress/protocol/openid-connect/token/introspect
OIDC_RESOURCE_SERVER_ENABLED=False
OIDC_RS_CLIENT_ID=docs
OIDC_RS_CLIENT_ID=docs-api
OIDC_RS_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RS_AUDIENCE_CLAIM="client_id" # The claim used to identify the audience
OIDC_RS_ALLOWED_AUDIENCES=""
OIDC_RS_ALLOWED_AUDIENCES=docs-mcp-client
OIDC_RS_SCOPES=docs:documents:search,docs:documents:read,docs:documents:create
# Store OIDC tokens in the session. Needed by search/ endpoint.
# OIDC_STORE_ACCESS_TOKEN=True