This guide explains how authentication works in the template MCP server, how to configure it, and how to troubleshoot common issues.
The server supports three authentication modes controlled by two environment variables:
ENABLE_AUTH |
USE_EXTERNAL_BROWSER_AUTH |
Mode | Required Variables |
|---|---|---|---|
False |
any | No Auth | None |
True |
True |
Local Dev Auth | SSO_CLIENT_ID, SSO_CLIENT_SECRET, SSO_CALLBACK_URL, SSO_AUTHORIZATION_URL, SSO_TOKEN_URL, SSO_INTROSPECTION_URL, POSTGRES_* |
True |
False |
Production Auth | SSO_CLIENT_ID, SSO_CLIENT_SECRET, SSO_CALLBACK_URL, SSO_AUTHORIZATION_URL, SSO_TOKEN_URL, SSO_INTROSPECTION_URL, SESSION_SECRET, POSTGRES_* |
Both authenticated modes require a working OIDC/OAuth 2.0 provider (e.g., Keycloak, Red Hat SSO, Auth0, Okta) with valid client credentials and endpoint URLs configured.
The simplest path. No SSO provider needed.
# .env
ENABLE_AUTH=False
USE_EXTERNAL_BROWSER_AUTH=FalseWith auth disabled, all endpoints are accessible without tokens. This is the recommended starting point for development and testing tool logic.
Note:
.env.exampleships withENABLE_AUTH=False. The code default insettings.pyisTrue, so if you run without a.envfile, auth will be on and you will get 401 errors unless an SSO provider is configured.
Before enabling auth, you need:
-
An OIDC/OAuth 2.0 provider — any spec-compliant provider works:
-
A registered OAuth client in your provider with:
- A client ID and client secret
- Redirect URI set to
http://localhost:5001/auth/callback/oidc - Grant types:
authorization_code(and optionallyrefresh_token) - Scopes: configured as needed by your provider
-
Three endpoint URLs from your provider:
- Authorization endpoint (e.g.,
https://sso.example.com/realms/myrealm/protocol/openid-connect/auth) - Token endpoint (e.g.,
https://sso.example.com/realms/myrealm/protocol/openid-connect/token) - Introspection endpoint (e.g.,
https://sso.example.com/realms/myrealm/protocol/openid-connect/token/introspect)
- Authorization endpoint (e.g.,
-
PostgreSQL — the server stores OAuth tokens in PostgreSQL. Use the included
compose.yamlto run one locally:podman compose up -d postgres
This mode opens your browser for OAuth login and caches the token in memory.
# .env
ENABLE_AUTH=True
USE_EXTERNAL_BROWSER_AUTH=True
# SSO provider credentials
SSO_CLIENT_ID=your-client-id
SSO_CLIENT_SECRET=your-client-secret
SSO_CALLBACK_URL=http://localhost:5001/auth/callback/oidc
# SSO provider endpoints (get these from your provider's OIDC discovery document)
SSO_AUTHORIZATION_URL=https://sso.example.com/realms/myrealm/protocol/openid-connect/auth
SSO_TOKEN_URL=https://sso.example.com/realms/myrealm/protocol/openid-connect/token
SSO_INTROSPECTION_URL=https://sso.example.com/realms/myrealm/protocol/openid-connect/token/introspect
# PostgreSQL (for token storage)
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres- You start the server and make a request to a protected endpoint (e.g.,
POST /mcpwith atools/call). - The
LocalDevelopmentAuthorizationMiddlewaredetects no valid token. - Your browser opens automatically to the SSO authorization URL.
- You log in through your SSO provider.
- The provider redirects back to
http://localhost:5001/auth/callback/oidcwith an authorization code. - The server exchanges the code for an access token and stores it in memory.
- Subsequent requests use the cached token automatically.
Most OIDC providers publish a discovery document at:
https://<your-provider>/.well-known/openid-configuration
Look for these fields in the JSON response:
| Discovery field | Maps to env var |
|---|---|
authorization_endpoint |
SSO_AUTHORIZATION_URL |
token_endpoint |
SSO_TOKEN_URL |
introspection_endpoint |
SSO_INTROSPECTION_URL |
In production, clients send a bearer token in the Authorization header. The server validates it via the introspection endpoint.
# .env (or set via ConfigMap / secrets in OpenShift)
ENABLE_AUTH=True
USE_EXTERNAL_BROWSER_AUTH=False
SSO_CLIENT_ID=your-client-id
SSO_CLIENT_SECRET=your-client-secret
SSO_CALLBACK_URL=https://your-mcp-server.example.com/auth/callback/oidc
SSO_AUTHORIZATION_URL=https://sso.example.com/realms/myrealm/protocol/openid-connect/auth
SSO_TOKEN_URL=https://sso.example.com/realms/myrealm/protocol/openid-connect/token
SSO_INTROSPECTION_URL=https://sso.example.com/realms/myrealm/protocol/openid-connect/token/introspect- Client obtains a token from the SSO provider (outside of this server).
- Client sends requests with
Authorization: Bearer <token>header. - The
AuthorizationMiddlewareextracts the token and calls the introspection endpoint. - If the token is valid, the request proceeds. If not, the server returns 401.
| Variable | Required When | Default | Description |
|---|---|---|---|
ENABLE_AUTH |
Always | True |
Master switch for authentication |
USE_EXTERNAL_BROWSER_AUTH |
Auth enabled | False |
Enables browser-based OAuth for local dev |
SSO_CLIENT_ID |
Auth enabled | "" |
OAuth client ID from your provider |
SSO_CLIENT_SECRET |
Auth enabled | "" |
OAuth client secret from your provider |
SSO_CALLBACK_URL |
Auth enabled | "" |
OAuth redirect URI (must match provider config) |
SSO_AUTHORIZATION_URL |
Auth enabled | "" |
Provider's authorization endpoint |
SSO_TOKEN_URL |
Auth enabled | "" |
Provider's token endpoint |
SSO_INTROSPECTION_URL |
Auth enabled | "" |
Provider's token introspection endpoint |
SESSION_SECRET |
Production | None |
Secret key for session middleware |
COMPATIBLE_WITH_CURSOR |
Cursor IDE | False |
Enables Cursor-compatible OAuth2 flow |
POSTGRES_HOST |
Auth enabled | None |
PostgreSQL host for token storage |
POSTGRES_PORT |
Auth enabled | None |
PostgreSQL port |
POSTGRES_DB |
Auth enabled | None |
PostgreSQL database name |
POSTGRES_USER |
Auth enabled | None |
PostgreSQL username |
POSTGRES_PASSWORD |
Auth enabled | None |
PostgreSQL password |
When connecting to this server from Cursor, set COMPATIBLE_WITH_CURSOR=True in your .env file.
Cursor's MCP client does not send client_id in the token request body, and it does not support PKCE (code_verifier). With COMPATIBLE_WITH_CURSOR=True, the server:
- Makes
client_idoptional in token request models (instead of required) - Skips PKCE verification during the authorization code exchange
- Skips client credential validation in the refresh token and client credentials grant flows
This allows Cursor to complete the OAuth flow without modification on the client side.
# .env
COMPATIBLE_WITH_CURSOR=True
ENABLE_AUTH=True
USE_EXTERNAL_BROWSER_AUTH=True # or False for productionSecurity note:
COMPATIBLE_WITH_CURSOR=Truerelaxes validation. Use it only for local development with Cursor, not in production.
When auth is enabled, the server exposes two RFC 8414 discovery endpoints:
| Endpoint | Description |
|---|---|
GET /.well-known/oauth-protected-resource |
Returns resource server metadata (resource identifier, authorization servers, supported token types) |
GET /.well-known/oauth-authorization-server |
Returns authorization server metadata (issuer, endpoints, grant types, response types, code challenge methods) |
These endpoints are always public (no token required) and return JSON. MCP clients that support OAuth discovery can use them to auto-configure authentication.
Example:
curl http://localhost:5001/.well-known/oauth-authorization-serverThe OAuth implementation lives in template_mcp_server/src/oauth/ with these components:
| File | Purpose |
|---|---|
models.py |
Pydantic models for token requests, client registration, authorization |
handler.py |
Low-level OAuth2 operations (authorization URLs, token exchange, PKCE) |
controller.py |
Business logic for each grant type (auth code, refresh, client credentials) |
routes.py |
FastAPI route definitions (/auth/authorize, /auth/token, /auth/register, etc.) |
service.py |
OAuth service initialization and storage integration |
Adding scopes — modify the scope validation in controller.py and the discovery metadata in api.py.
Changing token lifetime — adjust the token TTL in controller.py where access tokens are generated.
Replacing the token store — the default store is PostgreSQL via StorageService. To use a different backend, implement the same interface and update service.py.
Adding a new grant type — add a handler in controller.py, wire it in routes.py, and add the grant type to the discovery metadata in api.py.
Cause: Auth is enabled but SSO is not configured.
Fix: Either disable auth or configure a provider:
# Option A: disable auth
ENABLE_AUTH=False
# Option B: configure SSO (see sections above)If SSO_AUTHORIZATION_URL, SSO_TOKEN_URL, or SSO_INTROSPECTION_URL are empty strings (the default), the OAuth flow will fail. Ensure all three are set when auth is enabled.
The SSO_CALLBACK_URL must exactly match the redirect URI registered in your SSO provider. For local development, this is typically:
http://localhost:5001/auth/callback/oidc
When USE_EXTERNAL_BROWSER_AUTH=True, the server calls webbrowser.open(). This may not work in headless environments (containers, SSH sessions). Use production auth mode instead.
Auth modes require PostgreSQL for token storage. Ensure PostgreSQL is running:
podman compose up -d postgresThe code default for ENABLE_AUTH is True. If you delete or skip the .env file, auth activates automatically. Always copy .env.example to .env:
cp .env.example .env| Question | Answer |
|---|---|
| Does the server auto-refresh tokens on a timer? | No — the server implements the refresh_token grant at POST /auth/token, but the client is responsible for calling it before the token expires. |
| What does PostgreSQL store? | Only OAuth data: registered clients, authorization codes, access tokens, and refresh tokens. No business data. |
| Is PostgreSQL required when auth is disabled? | No — if ENABLE_AUTH=False, PostgreSQL is not used. |