Skip to content

Choose an authorization and credential flow ​

Start here when deciding how to protect an MCP server. mcp-auth supplies the OAuth authorization server and resource-server authorization helpers. Your MCP server owns tool behavior and application permissions.

Choose a login mode ​

Choose the downstream-credential flow separately using the table below. Login methods do not automatically supply a token for a downstream API.

Supported combinations ​

Use caseChooseBoundary to preserve
Expose public MCP tools without sign-inDeploy the MCP resource server without an auth gateMCP authorization is optional; mcp-auth is not needed for this endpoint. Local fixed-subject mode is only a test login.
Protect an MCP server with an existing compatible authorization serverPython, Go or TypeScript SDK onlyVerify that server's issuer, signing algorithm, resource audience and required scopes. The bundled authorization server is optional.
Let employees sign in with one enterprise OIDC/OAuth providerMCP_AUTH_CONNECTOR and a connector fileRegister mcp-auth's callback with the IdP; publish MCP metadata on the resource server.
Offer Google, GitHub, LDAP and OIDC sign-in on one pageMCP_AUTH_LOGIN_METHODS_FILEDo not also set MCP_AUTH_CONNECTOR. Method-prefixed subjects are distinct identities, not linked accounts.
Protect several MCP servers with one issuerMCP_AUTH_RESOURCES and MCP_AUTH_RESOURCE_SCOPESEach issued token targets one resource. Every resource server validates its own exact URL.
Call a downstream API using the user's existing provider tokenSingle connector, downstream_token_strategy=upstream_sessionThe provider's actual audience and scopes apply. Asking for a different audience does not mint a differently scoped credential.
Obtain a new downstream grant through a provider that supports token exchangeSingle connector, downstream_token_strategy=rfc8693Configure the upstream provider's trust/federation and grants; ordinary OIDC login alone does not establish that trust.
Proxy a fixed upstream with per-user credentials from your own secret managerExternal credential connector, with either login modeExchange on every request, verify the credential-access assertion, then resolve credentials in the resource server. No vault credentials enter the MCP client or auth server.
Keep issuers, signing keys or per-provider downstream exchangers separateSeparate server processes and stores behind HTTPSMultiple processes are an isolation choice; multiple resources alone do not require them.

Login methods establish identity only. They do not retain an upstream token session and cannot use the single-connector upstream_session or rfc8693 exchanger. Combine them with external credential connectors when downstream credentials are needed. See login methods and external credential connectors.

Try a local authorization flow ​

From a checkout, follow local development to install Python, Go and Node dependencies. Start the local authorization server in one terminal:

bash
(
  cd auth-server
  MCP_AUTH_LOCAL_DEVELOPMENT=true \
  MCP_AUTH_REQUIRE_HTTPS=false \
  MCP_AUTH_REGISTRATION_ENABLED=true \
  GOWORK=off go run ./cmd/auth-server
)

In another terminal, start the Go example or TypeScript example. Both default to http://localhost:8081/mcp and the authorization server at http://localhost:8080; run one example at a time on that port.

Connect an OAuth-capable MCP client to http://localhost:8081/mcp. It discovers resource and authorization metadata, registers if it uses DCR, opens consent, exchanges a code using S256 PKCE, then calls the tool with an MCP access token. The local server signs in as the fixed subject local-user; this does not test an identity provider or real user authentication.

For the real Keycloak login and stored-provider-token exchange, use the Compose walkthrough. Its hostnames and ports are internal to Docker Compose; it is a test stack, not a desktop-client deployment template.

One enterprise provider, several MCP resources ​

For example, one issuer can protect both a Cully MCP endpoint and a GitLab MCP endpoint. Choose canonical public resource URLs and configure their scopes:

bash
export MCP_AUTH_ISSUER=https://auth.example.com
export MCP_AUTH_RESOURCES=https://mcp.example.com/cully/mcp,https://mcp.example.com/gitlab/mcp
export MCP_AUTH_RESOURCE_SCOPES='{"https://mcp.example.com/cully/mcp":["tools:read"],"https://mcp.example.com/gitlab/mcp":["gitlab:use"]}'
export MCP_AUTH_CONNECTORS_FILE=/etc/mcp-auth/connectors.json
export MCP_AUTH_CONNECTOR=company

This is the resource/login portion of configuration, not a complete production startup command. Add the signing key, durable store and restricted HTTPS proxy from the server guide.

The two token labels mean two separately requested tokens, not one token valid at both servers. Cully's token must be rejected at GitLab, and vice versa.

Each MCP server advertises the same authorization server but its own resource URL and scopes. A token for Cully must fail at the GitLab endpoint. A connector is an identity/provider integration, not one connector per MCP resource.

Scope allowlists are not per-user entitlements. When a client omits scope, the server grants the selected resource's configured scope list after consent. Enforce team membership, object ownership and per-tool permissions in your resource server or policy engine. Do not advertise a write scope if every signed-in user should not be able to request it.

A gateway with external per-user credentials ​

For a gateway that injects a GitLab token from its own vault:

  1. Authenticate the MCP caller and verify the token for the gateway's exact MCP resource URL.
  2. Authenticate the gateway to /token using its registered private_key_jwt. Request urn:mcp-auth:token-type:credential-access-assertion for that resource.
  3. Verify the returned JWT's signature, required credential-access+jwt type, issuer, gateway client audience, expiry, resource, subject and session binding. Enforce one-time use of the assertion ID in the gateway.
  4. Only after that successful exchange, resolve credentials for the verified (issuer, subject, service) and call the configured upstream.

The gateway, vault mapping and proxy are application code; this repository does not ship a turnkey GitLab gateway or a secret-manager adapter. Never use the assertion (token_type=N_A) as an upstream Bearer access token.

Disable exchange-result caching for this path: Python passes use_cache=False on each exchange; TypeScript sets cache: null; Go leaves Cache nil. Ordinary JWT verification is offline and does not consult refresh-token revocation. This external-credential path checks the live refresh family on every exchange, so a revoked session blocks the next exchange. See the full wire contract and revocation rules.

Production deployment ​

The shipped binary serves HTTP. Terminate HTTPS at a reverse proxy, keep MCP_AUTH_REQUIRE_HTTPS=true, set MCP_AUTH_TRUST_PROXY_TLS=true, overwrite X-Forwarded-Proto at the proxy and restrict backend access to that proxy. Changing the issuer URL to https alone does not create a TLS listener.

Configure a persistent signing key and a durable store. The binary supports memory for local development and sqlite for a single durable instance. Shared database and KMS/HSM integrations are extension interfaces for an application embedding the server; they are not built-in CLI backend settings. Protect stored upstream access/refresh tokens as usable credentials whenever upstream_session is enabled. Follow the production checklist.

Verification coverage ​

These are executable repository checks, not certification of every vendor or customer environment:

Flow or boundaryChecked-in verification
Authorization Code + PKCE against Python, Go and TypeScript MCP serversSDK authorization E2E
Multiple OIDC providers, subject prefixes and browser bindingLogin-method E2E
External assertions, wrong-resource denial, refresh and revocationCredential-access E2E
OIDC login and discoveryMock-provider E2E
Keycloak and stored upstream-token exchangeCompose stack and client
Resource-specific scopes and omitted-scope defaultsResource scope tests, default-scope tests
Direct SDK verifier parityShared cases and each SDK's conformance tests
Trusted-proxy HTTPS checks and resource audience bindingServer boundary tests

Google, GitHub and LDAP behavior has controlled Go test coverage; these E2E jobs do not sign in to live Google/GitHub accounts or a production LDAP server. Verify your provider's callback, directory filters, identity claims and policy with real accounts before deployment.

For commands that execute these checks, see development verification.

Released under the MIT License.