Skip to content

Latest commit

 

History

History
276 lines (192 loc) · 11.9 KB

File metadata and controls

276 lines (192 loc) · 11.9 KB

Authentication Guide

This guide explains how authentication works in the template MCP server, how to configure it, and how to troubleshoot common issues.

Overview

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.

Running Without Auth (Default)

The simplest path. No SSO provider needed.

# .env
ENABLE_AUTH=False
USE_EXTERNAL_BROWSER_AUTH=False

With auth disabled, all endpoints are accessible without tokens. This is the recommended starting point for development and testing tool logic.

Note: .env.example ships with ENABLE_AUTH=False. The code default in settings.py is True, so if you run without a .env file, auth will be on and you will get 401 errors unless an SSO provider is configured.

Prerequisites for Authenticated Modes

Before enabling auth, you need:

  1. An OIDC/OAuth 2.0 provider — any spec-compliant provider works:

    • Keycloak
    • Auth0
    • Okta
    • Any provider supporting authorization code flow with token introspection
  2. 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 optionally refresh_token)
    • Scopes: configured as needed by your provider
  3. 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)
  4. PostgreSQL — the server stores OAuth tokens in PostgreSQL. Use the included compose.yaml to run one locally:

    podman compose up -d postgres

Local Development With Auth

This mode opens your browser for OAuth login and caches the token in memory.

Configuration

# .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

How the Browser Auth Flow Works

  1. You start the server and make a request to a protected endpoint (e.g., POST /mcp with a tools/call).
  2. The LocalDevelopmentAuthorizationMiddleware detects no valid token.
  3. Your browser opens automatically to the SSO authorization URL.
  4. You log in through your SSO provider.
  5. The provider redirects back to http://localhost:5001/auth/callback/oidc with an authorization code.
  6. The server exchanges the code for an access token and stores it in memory.
  7. Subsequent requests use the cached token automatically.

Finding Your Provider's Endpoint URLs

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

Production Auth

In production, clients send a bearer token in the Authorization header. The server validates it via the introspection endpoint.

Configuration

# .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

How Production Auth Works

  1. Client obtains a token from the SSO provider (outside of this server).
  2. Client sends requests with Authorization: Bearer <token> header.
  3. The AuthorizationMiddleware extracts the token and calls the introspection endpoint.
  4. If the token is valid, the request proceeds. If not, the server returns 401.

Environment Variables Reference

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

Cursor IDE Integration

When connecting to this server from Cursor, set COMPATIBLE_WITH_CURSOR=True in your .env file.

What it changes

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_id optional 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.

Configuration

# .env
COMPATIBLE_WITH_CURSOR=True
ENABLE_AUTH=True
USE_EXTERNAL_BROWSER_AUTH=True   # or False for production

Security note: COMPATIBLE_WITH_CURSOR=True relaxes validation. Use it only for local development with Cursor, not in production.

Discovery Endpoints

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-server

Customizing the OAuth Flow

The 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

Common customization points

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.

Troubleshooting

401 Unauthorized on tool calls

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)

Empty SSO URLs cause silent failures

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.

Callback URL mismatch

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

Browser does not open (local dev mode)

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.

PostgreSQL connection errors

Auth modes require PostgreSQL for token storage. Ensure PostgreSQL is running:

podman compose up -d postgres

"ENABLE_AUTH defaults to True" surprise

The 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

FAQ

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.