Follow-up to #671 (Tools) and #672 (Prompts). Spike: prove OIDC sign-in from the MCP Inspector against Keycloak on a local oinc cluster, then decide what ships.
What
Add "Sign in with OIDC" to the Inspector's authentication modal alongside the pasted bearer token. Authorization code with PKCE, done in the browser, with a pre-registered public client. No change to the plugin backend.
Why this shape
The standalone MCP Inspector needs mcp-gateway's Keycloak workarounds (Authorino hostAliases, an API server certificate rewrite, gateway CORS header injection, an EnvoyFilter faking the DCR preflight). Measured on Keycloak 26.4 with the mcp-gateway realm:
Dropping dynamic client registration removes the only step that needed a proxy or a workaround. A pre-registered public client with a redirect URI and web origin is the standard SPA pattern and is what real identity providers expect anyway. spec.oauthProtectedResource on the MCPGatewayExtension already gives the browser the protected resource metadata, so no discovery request to the gateway is needed either.
The backend-brokered alternative (metadata, registration and token exchange through cmd/plugin-server) was designed and rejected for this spike: it insulates from provider CORS and Console CSP at the cost of a second OAuth implementation in Go. Revisit if CSP enforcement or provider CORS bite in practice.
Flow
initialize returns 401. Modal shows "Sign in with OIDC" when the extension has spec.oauthProtectedResource.authorizationServers.
- Browser takes issuer,
resource and scopes from the extension, and the client id from the annotation console.kuadrant.io/oauth-client-id (default kuadrant-console-mcp-inspector).
- Authorization server metadata via
window.fetch in the MCP specification order (/.well-known/oauth-authorization-server/<path>, /.well-known/openid-configuration/<path>, <path>/.well-known/openid-configuration). Validate issuer, require authorization_endpoint and token_endpoint, require S256 if code_challenge_methods_supported is present.
- PKCE verifier and challenge, random
state. Pending entry in sessionStorage. Redirect to the authorization endpoint with resource (RFC 8707).
- Provider redirects to a new Console route
/mcp-inspector/oauth/callback. Callback page checks state, exchanges the code with a form-encoded POST, keeps only access_token, token_type, expires_in, scope.
- Token goes into an in-memory handoff, never storage. Client-side navigate to
/mcp-inspector, which restores the namespace and extension and connects through the existing X-Kuadrant-MCP-Authorization path.
- Later
401 means sign in again. No refresh tokens.
Never consoleFetch for provider requests; it would carry Console headers off-origin. credentials: 'omit', redirect: 'error'.
Frontend
src/utils/mcp/oauth.ts: settings from the extension, discovery, PKCE, authorize URL, code exchange, pending storage, token handoff.
MCPOAuthCallbackPage plus a console.page/route for /mcp-inspector/oauth/callback (its own page, so the one-route-per-page rule from the namespace switch crash still holds) and the exposedModules entry.
MCPInspectorPage: OIDC action in the modal naming the client id in use, authMode: 'oidc', issuer host and sign out on the connection card, handoff and router-state error handling on mount.
yarn i18n for new strings. Update docs/mcp-inspector.md, which currently says OIDC is unsupported, and document the provider requirement: register the Console origin as a web origin and the callback as a redirect URI.
Dev environment
make oinc-mcp-oidc (scripts/mcp-oidc-setup.sh, manifests in scripts/keycloak/), with a matching teardown. Idempotent:
- Node IP from the first node's
InternalIP; KEYCLOAK_HOST=keycloak.<ip with dashes>.sslip.io. Port 80 on the node is the OpenShift router, reachable from the host on OrbStack and Linux and from every pod, so one plain HTTP issuer URL works for the browser and for Authorino. Stop with an explanation if the host cannot reach it (Docker Desktop, podman machine).
- Realm
mcp: user mcp / mcp, accessTokenLifespan 1800, public client kuadrant-console-mcp-inspector with standard flow, PKCE S256 required, redirect URI http://localhost:9000/mcp-inspector/oauth/callback, web origin http://localhost:9000. CONSOLE_PORT override applied with kcadm.sh in the pod.
- Keycloak 26.4
start-dev --import-realm --proxy-headers=xforwarded, HTTP only, KC_HOSTNAME=http://$KEYCLOAK_HOST set after apply. Route in keycloak with that host, no TLS.
- Verify discovery from the host and from inside the cluster.
- Patch the
MCPGatewayExtension: authorizationServers: [issuer], resource: http://mcp.127-0-0-1.sslip.io/mcp, scopesSupported: [openid, profile, email], and the client id annotation.
- Refuse if another
AuthPolicy targets the gateway section (print the delete command; do not delete). Apply mcp-inspector-auth: JWT for Keycloak plus the existing API key selector so pasted tokens keep working, /.well-known exempt, 401 with WWW-Authenticate: Bearer resource_metadata=....
MCP_PROXY_ALLOW_INSECURE_AUTH=true stays on the backend because the dev listener is HTTP.
Tests
Jest only for this spike: oauth.test.ts (settings and annotation override, discovery order and rejections, RFC 7636 appendix B vector, authorize URL, token request and response filtering, pending round trip, handoff consumed once), MCPOAuthCallbackPage.test.tsx (state mismatch, provider error, success), MCPInspectorPage.test.tsx additions. No Playwright journey.
Out of scope
- Dynamic client registration and client id metadata documents.
- Refresh tokens, multiple authorization servers, provider-side sign-out.
- Tool-level authorisation in the dev realm.
- Kuadrant Operator support for
ConsolePlugin.spec.contentSecurityPolicy ConnectSrc derived from authorizationServers. Console CSP is Report-Only in 4.22, so this works today, but it must land before Console enforces CSP for plugins.
PR description must include
- How to review:
oauth.ts and its tests first, then the callback page, then the Inspector page changes, then the dev scripts.
- Manual check:
make oinc, make oinc-mcp-oidc, open MCP Inspector, select the gateway, "Sign in with OIDC", log in as mcp / mcp, confirm reconnect and a toystore_greet run, confirm the pasted bearer path still works after sign out, confirm the only CSP entries in the browser console are the Report-Only ones for the Keycloak host.
Follow-up to #671 (Tools) and #672 (Prompts). Spike: prove OIDC sign-in from the MCP Inspector against Keycloak on a local oinc cluster, then decide what ships.
What
Add "Sign in with OIDC" to the Inspector's authentication modal alongside the pasted bearer token. Authorization code with PKCE, done in the browser, with a pre-registered public client. No change to the plugin backend.
Why this shape
The standalone MCP Inspector needs
mcp-gateway's Keycloak workarounds (AuthorinohostAliases, an API server certificate rewrite, gateway CORS header injection, anEnvoyFilterfaking the DCR preflight). Measured on Keycloak 26.4 with themcp-gatewayrealm:Access-Control-Allow-Originfor a client's registered web origin;OPTIONSwithout CORS headers (Trusted Host don't set CORS headers in OIDC Client Registration keycloak/keycloak#39629), so a browser cannot register a client.Dropping dynamic client registration removes the only step that needed a proxy or a workaround. A pre-registered public client with a redirect URI and web origin is the standard SPA pattern and is what real identity providers expect anyway.
spec.oauthProtectedResourceon theMCPGatewayExtensionalready gives the browser the protected resource metadata, so no discovery request to the gateway is needed either.The backend-brokered alternative (metadata, registration and token exchange through
cmd/plugin-server) was designed and rejected for this spike: it insulates from provider CORS and Console CSP at the cost of a second OAuth implementation in Go. Revisit if CSP enforcement or provider CORS bite in practice.Flow
initializereturns401. Modal shows "Sign in with OIDC" when the extension hasspec.oauthProtectedResource.authorizationServers.resourceand scopes from the extension, and the client id from the annotationconsole.kuadrant.io/oauth-client-id(defaultkuadrant-console-mcp-inspector).window.fetchin the MCP specification order (/.well-known/oauth-authorization-server/<path>,/.well-known/openid-configuration/<path>,<path>/.well-known/openid-configuration). Validateissuer, requireauthorization_endpointandtoken_endpoint, requireS256ifcode_challenge_methods_supportedis present.state. Pending entry insessionStorage. Redirect to the authorization endpoint withresource(RFC 8707)./mcp-inspector/oauth/callback. Callback page checksstate, exchanges the code with a form-encoded POST, keeps onlyaccess_token,token_type,expires_in,scope./mcp-inspector, which restores the namespace and extension and connects through the existingX-Kuadrant-MCP-Authorizationpath.401means sign in again. No refresh tokens.Never
consoleFetchfor provider requests; it would carry Console headers off-origin.credentials: 'omit',redirect: 'error'.Frontend
src/utils/mcp/oauth.ts: settings from the extension, discovery, PKCE, authorize URL, code exchange, pending storage, token handoff.MCPOAuthCallbackPageplus aconsole.page/routefor/mcp-inspector/oauth/callback(its own page, so the one-route-per-page rule from the namespace switch crash still holds) and theexposedModulesentry.MCPInspectorPage: OIDC action in the modal naming the client id in use,authMode: 'oidc', issuer host and sign out on the connection card, handoff and router-state error handling on mount.yarn i18nfor new strings. Updatedocs/mcp-inspector.md, which currently says OIDC is unsupported, and document the provider requirement: register the Console origin as a web origin and the callback as a redirect URI.Dev environment
make oinc-mcp-oidc(scripts/mcp-oidc-setup.sh, manifests inscripts/keycloak/), with a matching teardown. Idempotent:InternalIP;KEYCLOAK_HOST=keycloak.<ip with dashes>.sslip.io. Port 80 on the node is the OpenShift router, reachable from the host on OrbStack and Linux and from every pod, so one plain HTTP issuer URL works for the browser and for Authorino. Stop with an explanation if the host cannot reach it (Docker Desktop, podman machine).mcp: usermcp/mcp,accessTokenLifespan1800, public clientkuadrant-console-mcp-inspectorwith standard flow, PKCES256required, redirect URIhttp://localhost:9000/mcp-inspector/oauth/callback, web originhttp://localhost:9000.CONSOLE_PORToverride applied withkcadm.shin the pod.start-dev --import-realm --proxy-headers=xforwarded, HTTP only,KC_HOSTNAME=http://$KEYCLOAK_HOSTset after apply.Routeinkeycloakwith that host, no TLS.MCPGatewayExtension:authorizationServers: [issuer],resource: http://mcp.127-0-0-1.sslip.io/mcp,scopesSupported: [openid, profile, email], and the client id annotation.AuthPolicytargets the gateway section (print the delete command; do not delete). Applymcp-inspector-auth: JWT for Keycloak plus the existing API key selector so pasted tokens keep working,/.well-knownexempt,401withWWW-Authenticate: Bearer resource_metadata=....MCP_PROXY_ALLOW_INSECURE_AUTH=truestays on the backend because the dev listener is HTTP.Tests
Jest only for this spike:
oauth.test.ts(settings and annotation override, discovery order and rejections, RFC 7636 appendix B vector, authorize URL, token request and response filtering, pending round trip, handoff consumed once),MCPOAuthCallbackPage.test.tsx(state mismatch, provider error, success),MCPInspectorPage.test.tsxadditions. No Playwright journey.Out of scope
ConsolePlugin.spec.contentSecurityPolicyConnectSrcderived fromauthorizationServers. Console CSP isReport-Onlyin 4.22, so this works today, but it must land before Console enforces CSP for plugins.PR description must include
oauth.tsand its tests first, then the callback page, then the Inspector page changes, then the dev scripts.make oinc,make oinc-mcp-oidc, open MCP Inspector, select the gateway, "Sign in with OIDC", log in asmcp/mcp, confirm reconnect and atoystore_greetrun, confirm the pasted bearer path still works after sign out, confirm the only CSP entries in the browser console are theReport-Onlyones for the Keycloak host.