Skip to main content
Version: Dev

Interactive MCP Sign-In

With interactive sign-in, an MCP client is configured with a URL and nothing else:

{
"type": "http",
"url": "https://gateway.example.com/mcp/jira"
}

The first time a user turns on or uses the tool, one browser window takes them through three steps: they sign in to your identity provider, authorize the upstream MCP server, and allow the client on a consent page. After that the client holds a gateway session that renews itself, and the gateway calls the upstream with that user's own token, refreshing it as needed. The user is not asked again. Upstream tokens never reach the client, and the gateway session token never reaches the upstream.

Interactive sign-in is available on a gateway released after 1.8.0.

Minimal Example: Keycloak and Jira​

This example signs users in at Keycloak and connects them to Jira through mcp-atlassian. Members of the Keycloak group jira-users get the tools. It was tested with Keycloak 26.0, Jira Software 10.3.26, mcp-atlassian 0.23.1, and Open WebUI 0.12.5.

The gateway's public address in this example is https://gateway.example.com. Clients connect to https://gateway.example.com/mcp/jira, the per-server entry. A session works only on the entry of the server it was issued for, so the aggregated /mcp entry does not offer this sign-in.

Create the Keycloak Client​

In the realm, here corp, create an OpenID Connect client named aisix-mcp:

  1. Turn on Client authentication and the Standard flow.
  2. Add https://gateway.example.com/mcp-oauth/login/callback as a valid redirect URI.
  3. Add a Group Membership mapper with the token claim name groups, Full group path off, and Add to ID token on.

In Jira, go to Administration > Applications > Application links and create an External application link with the Incoming direction:

  1. Set the redirect URL to https://gateway.example.com/mcp-oauth/callback.
  2. Set the scope to READ.
  3. Keep the generated client ID and client secret.

Run mcp-atlassian​

Set JIRA_URL and ATLASSIAN_OAUTH_ENABLE and nothing else. With OAuth enabled, the server uses the token the gateway sends with each request:

export JIRA_URL="https://jira.example.com"
export ATLASSIAN_OAUTH_ENABLE="true"

mcp-atlassian --transport streamable-http --host 0.0.0.0 --port 9000 --path /mcp

This setup also lists Confluence tools. Calls to them fail, because no Confluence URL is configured.

Configure the Gateway​

The gateway needs five things: its public URL, a trust provider with login, a caller API key with access to the server, a claim mapping from the group to that key, and the MCP server itself.

AISIX Enterprise or AISIX Cloud​

Export the connection details and the secrets from the previous steps:

export AISIX_CP="YOUR_AISIX_CONTROL_PLANE_API_URL"
export AISIX_TOKEN="YOUR_ADMIN_TOKEN"
export ENV_ID="YOUR_ENVIRONMENT_ID"
export KEYCLOAK_CLIENT_SECRET="YOUR_KEYCLOAK_CLIENT_SECRET"
export JIRA_CLIENT_ID="YOUR_JIRA_CLIENT_ID"
export JIRA_CLIENT_SECRET="YOUR_JIRA_CLIENT_SECRET"

Set the public URL and create the trust provider:

curl -sS -X PATCH "$AISIX_CP/environments/$ENV_ID" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mcp_resource_url": "https://gateway.example.com/mcp"}'

curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/oidc_providers" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "corp",
"issuer": "https://sso.example.com/realms/corp",
"audiences": ["aisix-mcp"],
"identity_claim": "preferred_username",
"login": {
"client_id": "aisix-mcp",
"client_secret": "'"$KEYCLOAK_CLIENT_SECRET"'"
}
}'

Create the caller API key and map the group to it:

export API_KEY_ID=$(curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/api_keys" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"display_name": "jira-users",
"allowed_models": [],
"mcp_access": { "allow": ["jira__*"] }
}' | jq -r '.api_key.id')

curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/claim_mappings" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "jira-users",
"jwt_provider": "corp",
"priority": 100,
"match": [
{ "claim": "groups", "op": "contains", "values": ["jira-users"] }
],
"resolve": { "api_key_id": "'"$API_KEY_ID"'" }
}'

Create the MCP server:

curl -sS -X POST "$AISIX_CP/mcp_servers" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "jira",
"url": "https://mcp-atlassian.example.com/mcp",
"auth_type": "oauth2_authorization_code",
"client_id": "'"$JIRA_CLIENT_ID"'",
"secret": "'"$JIRA_CLIENT_SECRET"'",
"token_url": "https://jira.example.com/rest/oauth2/latest/token",
"authorization_url": "https://jira.example.com/rest/oauth2/latest/authorize",
"scopes": ["READ"],
"allowed_environments": ["'"$ENV_ID"'"]
}'

In the dashboard, the provider form has a Gateway sign-in section, and the MCP server form offers OAuth 2.0 (per-user sign-in) as its authentication type. The Gateway sign-in section also shows both redirect URIs for you to copy, once the environment's MCP Access page has a resource URL.

AISIX OSS​

Add these entries to resources.yaml:

resources.yaml (interactive sign-in)
mcp_auth_settings:
- resource_url: https://gateway.example.com/mcp

oidc_providers:
- name: corp
issuer: https://sso.example.com/realms/corp
audiences: ["aisix-mcp"]
identity_claim: preferred_username
login:
client_id: aisix-mcp
client_secret: ${KEYCLOAK_CLIENT_SECRET}

api_keys:
- display_name: jira-users
key_env: JIRA_USERS_KEY
allowed_models: []
mcp_access:
allow: ["jira__*"]

claim_mappings:
- name: jira-users
jwt_provider: corp
priority: 100
match:
- claim: groups
op: contains
values: ["jira-users"]
resolve:
api_key: jira-users

mcp_servers:
- name: jira
url: https://mcp-atlassian.example.com/mcp
auth_type: oauth2_authorization_code
client_id: ${JIRA_CLIENT_ID}
secret: ${JIRA_CLIENT_SECRET}
token_url: https://jira.example.com/rest/oauth2/latest/token
authorization_url: https://jira.example.com/rest/oauth2/latest/authorize
scopes: ["READ"]

Every caller API key entry requires a credential even when nobody presents it, so set JIRA_USERS_KEY as well, and do not distribute it. A gateway without a control plane keeps sign-in data in memory. See Gateways Without a Control Plane before you rely on it.

Check the Challenge​

A request with no credential gets a 401 that points at this server's own sign-in:

curl -sS -i -X POST "https://gateway.example.com/mcp/jira" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

The response carries this header:

WWW-Authenticate: Bearer resource_metadata="https://gateway.example.com/.well-known/oauth-protected-resource/mcp/jira"

Connect a Client​

Give the client the URL. In Open WebUI, add the tool in the admin settings with the MCP type and OAuth 2.1 authentication, then select Register Client and set the tool's visibility so your users can see it. Claude Code and the MCP TypeScript SDK were also tested and work the same way.

A client's redirect URI must be https, or http on loopback. The consent page shows the address the client is sending users back to, because that page is what protects a user who followed a link from another person.

Configuration Reference​

MCP Server​

auth_type: oauth2_authorization_code is available on type: mcp servers and not on type: openapi servers. Every OAuth field is optional:

FieldWhen omitted
client_idThe gateway registers itself once with the upstream as a public client.
secretThe gateway authenticates as a public client. A secret needs a client_id.
token_urlDiscovered from the upstream.
authorization_urlDiscovered from the upstream.
registration_urlDiscovered from the upstream, and used only when client_id is omitted.
scopesThe gateway requests what the upstream's protected-resource metadata asks for.

Discovery reads the upstream's RFC 9728 and then RFC 8414 metadata. A server with authorization_url, token_url, and client_id configured makes no metadata request at all, which is what the Jira example does.

Trust Provider login​

FieldRequiredDescription
login.client_idYesThe client registered at the identity provider.
login.client_secretNoSent to the token endpoint in the request body. Omit it for a public client. It is write-only in the AISIX Cloud Admin API and a placeholder in aisix export.
login.scopesNoRequested beside openid and the provider's required_scopes.

The provider needs an issuer and cannot use hmac_secret. Endpoints come from the issuer's OIDC discovery document. As for any JWKS provider, audiences must not be empty.

Copy issuer from the issuer field of that discovery document. It must match exactly, including any trailing slash. If the document's value differs, new sign-ins fail with 500 and mcp_oauth_misconfigured, and the gateway logs the trust provider's discovery document names another issuer. The control plane does not read the discovery document, so it cannot detect this when you save. When the only difference is a trailing slash, existing sessions keep working, so only new sign-ins show the failure.

The number of enabled providers with login in an environment decides how every oauth2_authorization_code server in it works. It is not a per-server setting:

Enabled providers with loginResult
OneInteractive sign-in, as on this page.
NoneRelay mode. See Relay Mode.
More than one, or login on a provider without issuer or with hmac_secretThe AISIX Cloud Admin API refuses to save it with 400. In a resources file, nothing rejects it at load, and sign-in fails with 500 and mcp_oauth_misconfigured.

A gateway at 1.8.0 or earlier ignores login, and while one is registered in the environment, the response to the save carries a warnings array. oauth2_authorization_code itself cannot be loaded by such a gateway, so the AISIX Cloud Admin API refuses to save the server with 422 and DP_INCOMPATIBLE while one is registered in the target environment.

Audiences​

Two different checks read aud:

  • A JWT presented as a bearer token is checked against audiences only. mcp_auth_settings.resource_url is used only for discovery and is never compared with aud.
  • The ID token at interactive sign-in is checked against login.client_id. audiences does not need to list that value, and is not consulted for this check.

Redirect URIs​

<base> is resource_url without its trailing /mcp, so a path prefix is included.

RegisterAtValue
Sign-in callbackThe identity provider<base>/mcp-oauth/login/callback
Upstream callbackThe upstream's OAuth client, when you configure client_id<base>/mcp-oauth/callback

Both must match exactly. Use https: the gateway marks its sign-in cookie Secure only on an https base, and Jira's OAuth provider requires an https base URL.

Behind a gateway that routes by path prefix, also forward <base>/mcp/{server}/, <base>/mcp-oauth/, and <base>/.well-known/oauth-protected-resource/mcp/{server}. Some clients also try well-known paths outside the prefix. Those 404 responses are harmless, because the clients go on to <base>/mcp/{server}/.well-known/openid-configuration. See Run Behind a Path-Prefix Gateway.

Jira​

These hold for Jira Software 10.3.26, the tested version:

  • The scope is the exact string READ, in upper case. It is required.
  • An access token lives for 3600 seconds by default, set by the system property atlassian.oauth2.provider.access.token.expiration.seconds. The gateway refreshes it 60 seconds before it expires.
  • mcp-atlassian needs JIRA_URL and ATLASSIAN_OAUTH_ENABLE=true.

Keycloak​

Claim mappings, bound claims, and the identity claim read the ID token only. The gateway does not call the userinfo endpoint and does not read the access token, so a mapper must have Add to ID token on.

Keep the claims small. The ID token's claims travel in the sign-in's flow cookie, which is limited to 4000 bytes. A long group list can overflow it, and the user then gets an error page instead of a session. Give the gateway's client only the mappers it needs.

Gateways Without a Control Plane​

A gateway without a dp-manager keeps sign-in data in its own memory. That data is lost on restart and is not shared, so sessions and upstream credentials belong to one gateway process. Run a single process for interactive sign-in in this mode.

AISIX Enterprise and AISIX Cloud​

Upstream credentials are stored through the dp-manager, encrypted with the deployment's master key, so all gateways in an environment share them and accept each other's sessions.

The Connected users panel of an MCP server lists who is connected and offers Disconnect and Disconnect all. The same operations are GET /mcp_servers/{id}/credentials, DELETE /mcp_servers/{id}/credentials/{credential_id}, and DELETE /mcp_servers/{id}/credentials. They need the mcp_server_credentials permission, which is separate from mcp_servers and which plain members do not have, because the list shows other people's identities.

For self-hosted deployments, each dp-manager replica opens up to 8 more PostgreSQL connections for this store. The number is fixed. See PostgreSQL.

Relay Mode​

With no login provider, the same server needs a gateway credential from the client, and sign-in covers only the upstream. The client sends its gateway credential in x-aisix-api-key, including on the token request, and receives a placeholder token with no authority of its own. The upstream token stays in the gateway, stored under the caller's identity. The credential must name a user: a JWT, or an API key with an owner. Any other credential gets 403 and mcp_oauth_no_subject.

Behavior​

Sessions​

The access token lasts 1 hour. The refresh token lasts 14 days, and each refresh counts 14 days afresh with no overall cap. A refresh token works once. The gateway does not check back with the identity provider at refresh, so a client in regular use stays signed in for as long as the key and mapping allow.

The consent page has Allow and Cancel, and every new sign-in ends on it, including one where the user has already authorized the upstream. Cancel refuses that client, which gets access_denied and no session.

For a user signed in through the gateway, Cancel does not undo the upstream authorization they just gave. The upstream token stays stored, the Connected users panel shows them as connected, and their next sign-in does not ask for upstream authorization again. An administrator removes it by disconnecting the user. Declining on the upstream's own page stores nothing.

Changes to a key, a claim mapping, or a provider apply on the next request, evaluated against the claims recorded at sign-in.

Refusals​

SituationDuring sign-inOn /mcp/{server} with a session
Identity maps to no key403 page401 jwt_identity_unmapped
Key disabled or expired403 page401 api_key_disabled or api_key_expired
bound_claims not met403 page403 jwt_claims_rejected
required_scopes not granted403 pageNot applicable
Key has no access to the server403 pageNo HTTP refusal
Session invalid or expired, its provider deleted or disabled, or the provider's issuer changedNot applicable401 mcp_oauth_authorization_required
Upstream credential goneThe user authorizes the upstream again401 mcp_oauth_authorization_required
Storage or the upstream's token endpoint unavailable503 page503 mcp_oauth_unavailable

Every 401 carries WWW-Authenticate with resource_metadata. A sign-in refusal page names the identity, as the refused a sign-in log line does, so a user can quote it to you.

A key that loses access to the server after sign-in is not an HTTP refusal. initialize succeeds, the tools are absent from tools/list, and tools/call is a JSON-RPC invalid_params error. Restoring access needs no new sign-in.

Revocation at the Upstream​

The gateway notices a revoked grant when the upstream answers with 401, or when it next refreshes the token. Jira does not answer with 401: it treats a dead token as an anonymous request and returns 200. Tools can therefore look empty rather than fail, and the model reports that it found nothing. The gateway notices only at the next refresh, which at Jira's default lifetime is up to about 59 minutes later.

Jira also keeps refreshing tokens for a deactivated user, so that user is never prompted. Clear their credentials as described in Offboarding.

Open WebUI Recovery​

When a stored credential is gone, Open WebUI shows Failed to connect to MCP server and the model reports that it has no such tool. To recover immediately, select Disconnect on the tool and turn it back on. The browser flow runs again.

Without that, recovery can take tens of minutes, 41 minutes in the tested case. Open WebUI does not respond to the 401 by signing in. It only refreshes its session shortly before expiry, and that refresh is what sends it back to the browser.

Disconnect in Open WebUI forgets only the client's session and sends the gateway nothing. If the upstream credential is still good, turning the tool on again shows only the consent page.

Disconnecting Users​

WhoHow
A user signed in through the gatewayCannot disconnect themselves. DELETE /mcp/{server}/oauth/credential takes a gateway credential and does not accept a session token.
A relay-mode callerDELETE /mcp/{server}/oauth/credential with their gateway credential.
An administrator, AISIX Enterprise or AISIX CloudThe Connected users panel or the Admin API.
An administrator, gateway without a control planeOnly a restart, which signs everyone out.

Disconnecting removes the stored credential. It does not revoke the grant at the upstream, so the authorization stays listed at Jira.

Disconnecting does not end gateway sessions, which are stateless. A session issued earlier stops working only because no credential is left, so if the same user authorizes again, that session works again and can still refresh, with that user's own new upstream token. To keep someone out, also stop them signing in. Disabling their key does that and refuses their sessions on the next request, along with those of everyone else the key serves. Removing them at the identity provider does not touch live sessions, but it makes the sign-in that would restore the credential fail, so do it before you disconnect.

A gateway holding the token in its local cache can keep using it on requests for up to 5 minutes from when it cached it, not from the disconnect. After that, requests get 401. The one exception is a store outage: while the store is unavailable, a gateway keeps using a token it already holds, past those 5 minutes and until the token expires, because it cannot check. A new sign-in and a session refresh read the store, so right after a disconnect a sign-in asks for upstream authorization again, and a refresh is refused with invalid_grant, which sends the user back to the browser.

Offboarding​

Disabling a user at the identity provider or removing them from a group does not affect live sessions, because sessions carry the claims from sign-in and do not check back. An administrator clears the user's credentials on each server. The next refresh fails, and the new sign-in meets the identity provider's current answer.

To cut access at once for everyone a key serves, disable the key or delete the claim mapping. That applies on the next request.

What Disconnects Users​

On AISIX Enterprise and AISIX Cloud, these changes remove stored upstream credentials:

  • Changing an MCP server's url, client_id, token_url, authorization_url, registration_url, or auth_type disconnects every user of that server.
  • Deleting the server or its environment removes them, and removing the server from an environment removes that environment's.
  • Deleting an OIDC provider or changing its issuer or identity_claim makes its users authorize each MCP server again, and those signed in through the gateway also sign in again. A change that only adds or removes a trailing slash on issuer clears no stored tokens and signs nobody out, but it can break new sign-ins.

Other edits, such as scopes, a new secret, jwks_uri, or hmac_secret, disconnect nobody.

After deleting the only provider with login, the environment is in relay mode, and sign-in works again only once a new provider with login is configured. Disabling that provider has the same effect while it stays disabled, without removing credentials.

Changing client_id without sending a new secret removes the old secret, on an MCP server and on login alike, because the secret belonged to the old client.

A gateway without a control plane removes nothing. There, changing url, token_url, issuer, identity_claim, or a user's identity value leaves their stored credential out of reach, and they authorize the upstream again. Deleting a provider ends its sessions.

With or without a control plane, a changed identity_claim puts stored credentials out of reach even for a user whose value is the same under the new claim. Such a user's session gets a 401 challenge on its next request and invalid_grant on refresh, and they authorize the upstream again.

Master Key​

For self-hosted deployments, stored records live up to 365 days and no job re-encrypts them under a new master key. A record is rewritten under the active key only when the gateway next writes it, which for the environment's session key can be many months later. Before retiring a key, consider that records encrypted with it become unreadable. Reads of those records return 503, so affected users and sign-ins get mcp_oauth_unavailable rather than a new prompt. Keep every retired key configured, as described in Secrets.

Outbound Requests​

outbound.ssrf_protection governs URLs that the operator did not choose. URLs an administrator configures are not gated: issuer discovery and the endpoints it names, token_url, registration_url, and the server's url, along with any metadata on the same authority as that url. An intranet identity provider or upstream works with no entry.

An endpoint or document that an upstream's metadata names on another authority (scheme, host, and port) is gated, so on a private address it needs explicit configuration or an allowed_hosts entry. These requests never follow redirects. See Configuration Files.

Observability​

aisix_auth_decisions_total records these decisions under method="mcp_oauth_session" and method="mcp_oauth_login". A session refused because its provider's issuer changed has reason="session_issuer_changed". See the Metric Reference.

Usage events carry the user's identity in jwt_subject, even when many users share one key. Access-log lines for a shared key do not identify the user.

Next Steps​