diff --git a/docker/auth/realm.json b/docker/auth/realm.json index 940d0e305..8773087de 100644 --- a/docker/auth/realm.json +++ b/docker/auth/realm.json @@ -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": [ diff --git a/env.d/development/common b/env.d/development/common index b0aea4e69..f98f1830a 100644 --- a/env.d/development/common +++ b/env.d/development/common @@ -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