OpenID Connect authentication
Over HTTP, the server can authenticate each user with your OpenID Connect provider (Keycloak, Entra ID, Okta, Auth0…) instead of trusting a gateway. It then acts as an OAuth 2.1 resource server: MCP clients discover the provider, sign the user in, and send an access token on every request.
The server never sees passwords and never issues tokens. Client registration, sign-in and consent stay with the provider.
How it works
- An MCP client calls the endpoint without a token. The server answers
401with aWWW-Authenticatechallenge pointing to its RFC 9728 metadata,/.well-known/oauth-protected-resource<path>. - The metadata names your provider as the authorization server. The client signs the user in there and asks for a token bound to the endpoint (RFC 8707
resource). - The client retries with
Authorization: Bearer <access token>. The server verifies the token and reads the caller's profile from one of its claims.
Each user gets their own source catalog, and nobody can choose another one: the profile comes from the token, never from a header.
Configure
In the configuration file:
import { defineKibanaMcpConfig } from "@havesomecode/kibana-mcp-server/config";
import store from "@havesomecode/kibana-mcp-server/stores/postgres";
export default defineKibanaMcpConfig({
url: process.env.KIBANA_URL,
user: process.env.KIBANA_USER,
password: process.env.KIBANA_PASSWORD,
store,
storeOptions: { url: process.env.DATABASE_URL },
http: {
host: "0.0.0.0",
oidc: {
issuer: "https://sso.example.com/realms/acme",
resource: "https://kibana-mcp.example.com/mcp",
requiredScopes: ["kibana-mcp"],
},
},
});Or with environment variables only:
KIBANA_HTTP_HOST=0.0.0.0 \
KIBANA_HTTP_OIDC_ISSUER=https://sso.example.com/realms/acme \
KIBANA_HTTP_OIDC_RESOURCE=https://kibana-mcp.example.com/mcp \
KIBANA_HTTP_OIDC_REQUIRED_SCOPES=kibana-mcp \
kibana-mcp-server serve --http --config ./kibana.mcp.config.mjs| Option | Environment variable | Description |
|---|---|---|
issuer | KIBANA_HTTP_OIDC_ISSUER | Issuer identifier, exactly as published in its discovery document. Enables OpenID Connect. |
resource | KIBANA_HTTP_OIDC_RESOURCE | Public URL of the MCP endpoint, as clients reach it. Required. |
audience | KIBANA_HTTP_OIDC_AUDIENCE | Expected token audience. Defaults to resource. |
mode | KIBANA_HTTP_OIDC_MODE | offline or introspection. Defaults to introspection when clientId is set, offline otherwise. |
clientId, clientSecret | KIBANA_HTTP_OIDC_CLIENT_ID, KIBANA_HTTP_OIDC_CLIENT_SECRET | Confidential client of this server at the provider, used for introspection. |
requiredScopes | KIBANA_HTTP_OIDC_REQUIRED_SCOPES | Scopes every token must carry. Separate them with spaces in the variable: a comma belongs to the scope name. |
scopesSupported | KIBANA_HTTP_OIDC_SCOPES_SUPPORTED | Scopes advertised to clients in the metadata. |
profileClaim | KIBANA_HTTP_OIDC_PROFILE_CLAIM | Claim naming the caller at the issuer. Defaults to sub. |
allowInsecureRequests | KIBANA_HTTP_OIDC_ALLOW_INSECURE_REQUESTS | Allow a non-HTTPS issuer or resource. Development only. |
resource is the public URL
resource is never derived from the incoming request, whose Host header the caller controls. Behind a reverse proxy, use the external HTTPS URL. Its path must be the HTTP path of the server (/mcp by default): clients call that URL and look up its metadata at /.well-known/oauth-protected-resource followed by that path, so a proxy must forward both without rewriting the path.
At startup, the server refuses:
- OpenID Connect combined with
KIBANA_HTTP_AUTH_TOKENor a profile header; - a configuration without named profiles (a
store, saved profiles, or the{profile}placeholder inKIBANA_SOURCE_CATALOG_PATH), since every user needs their own catalog; - a non-HTTPS issuer or resource outside the loopback interface;
- a
resourcewhose path differs from the HTTP path of the server; - an issuer whose discovery document cannot be read, or names another issuer;
- in the offline mode, an issuer whose JWKS cannot be read or is malformed.
Verify tokens
| Mode | How | Use it when |
|---|---|---|
offline | The JWT access token is checked locally with the provider's JWKS: signature, issuer, audience, expiration. No call to the provider per request. | The provider issues RFC 9068 JWT access tokens. |
introspection | The provider is asked about each token (RFC 7662). Answers are cached up to 60 seconds, never past the token expiration. Refused tokens are cached too, in a separate, smaller cache: tokens made up by callers cannot evict accepted ones. The server does not limit request rates: do it at your reverse proxy. | Tokens are opaque, or revocation must apply quickly. |
In the offline mode the JWKS is loaded at startup, then refreshed by oauth4webapi every 5 minutes, or sooner when a token names an unknown key. The JWKS and introspection endpoints are discovered from <issuer>/.well-known/openid-configuration, then /.well-known/oauth-authorization-server. The provider metadata is read once, at startup: restart the server after the provider changes its endpoints. Verification relies on oauth4webapi.
Access token format in offline mode
The offline mode accepts only RFC 9068 JWT access tokens: the JWT header must carry typ: at+jwt, and the payload the iss, sub, aud, exp, iat, jti and client_id claims. Many providers issue plain JWTs by default (typ: JWT, azp instead of client_id), which are refused with 401. If yours does, enable RFC 9068 access tokens on the provider, or use the introspection mode.
Token audience
Every token must name the endpoint in its audience: the aud claim, or the aud field of the introspection answer, must contain resource (or audience when set). A token without it is refused with 401, including when the provider returns no audience at all.
- Keycloak: add an audience mapper (Audience, Included Custom Audience set to the
resourceURL) to a client scope granted to MCP clients. - Entra ID: tokens carry the application ID URI of the API (
api://…) as audience; setaudienceto that URI.
Choose the profile claim
The caller's profile is the issuer followed by # and the value of profileClaim, for example https://sso.example.com/realms/acme#alice. A token without that claim is refused with 401.
The issuer prefix keeps identities apart: the same sub or username at another provider is another person, and a catalog created through a gateway's profile header (alice) is never handed to an OIDC user. Changing issuer therefore starts every user with a new catalog.
sub(default) is stable and unique, but opaque: catalogs are named after identifiers likef81d4fae-7dec-….preferred_usernameoremailgive readable catalog names. Use them only if users cannot change them on the provider, or a user could take over another one's catalog.
Profile names are turned into safe keys before they reach a store, as with the profile header.
Set up the provider
The provider must:
- publish its metadata over HTTPS, with an
issueridentical to the configured one; - let MCP clients register, through Dynamic Client Registration or Client ID Metadata Documents depending on what your clients support, or offer them a pre-registered public client;
- issue tokens for the endpoint audience (see the warning above);
- for the offline mode, issue RFC 9068 access tokens and publish a
jwks_uri; - for the introspection mode, publish an
introspection_endpointand give the server a confidential client.
Grant the scope listed in requiredScopes only to the users allowed to query Kibana: every authenticated caller reads what the Kibana service account can read. See the security model.
Check the setup
# 1. an unauthenticated call is challenged
curl -i https://kibana-mcp.example.com/mcp -X POST \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# HTTP/1.1 401 Unauthorized
# WWW-Authenticate: Bearer error="invalid_token", ..., resource_metadata="https://kibana-mcp.example.com/.well-known/oauth-protected-resource/mcp"# 2. the metadata names the provider
curl -s https://kibana-mcp.example.com/.well-known/oauth-protected-resource/mcp
# {"resource":"https://kibana-mcp.example.com/mcp","authorization_servers":["https://sso.example.com/realms/acme"],...}# 3. a token issued for the endpoint is accepted
curl -s https://kibana-mcp.example.com/mcp -X POST \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'| Status | Meaning |
|---|---|
401 | Missing, expired or invalid token, wrong audience, wrong format in offline mode, or no profile claim. The client must sign in again. |
403 | Valid token without one of requiredScopes. |
500 | The provider (JWKS or introspection) could not be reached. Not a token issue. |