Skip to main content

MCP Authorization Flow

Complete walkthrough of the OAuth 2.1 authorization flow for MCP servers, from initial discovery through token issuance and access. This flow implements the MCP Authorization specification with LumoAuth acting as the authorization server.

Complete Flow Diagram

Step-by-Step Implementation

Step 1: Trigger the Authorization Challenge

When an MCP client attempts to connect to a protected MCP server without a token, the server returns a 401 Unauthorized response with a WWW-Authenticate header containing the Protected Resource Metadata URL:

# MCP client sends request without authentication
curl -v https://mcp.example.com/mcp

# Server responds:
# HTTP/1.1 401 Unauthorized
# WWW-Authenticate: Bearer resource_metadata="https://app.lumoauth.dev/t/acme-corp/api/v1/.well-known/oauth-protected-resource/mcp/mcp_abc123",
# scope="mcp:read mcp:write"

Step 2: Discover the Authorization Server

The MCP client fetches the Protected Resource Metadata to discover which authorization server to use:

curl https://app.lumoauth.dev/t/acme-corp/api/v1/.well-known/oauth-protected-resource/mcp/mcp_abc123
{
"resource": "https://mcp.example.com",
"authorization_servers": [
"https://app.lumoauth.dev/t/acme-corp/api/v1/.well-known/oauth-authorization-server"
],
"bearer_methods_supported": ["header"],
"scopes_supported": ["mcp:read", "mcp:write"]
}

Then fetch the Authorization Server Metadata:

curl https://app.lumoauth.dev/t/acme-corp/api/v1/.well-known/oauth-authorization-server
{
"issuer": "https://app.lumoauth.dev/t/acme-corp/api/v1",
"authorization_endpoint": "https://app.lumoauth.dev/t/acme-corp/api/v1/oauth/authorize",
"token_endpoint": "https://app.lumoauth.dev/t/acme-corp/api/v1/oauth/token",
"introspection_endpoint": "https://app.lumoauth.dev/t/acme-corp/api/v1/oauth/introspect",
"registration_endpoint": "https://app.lumoauth.dev/t/acme-corp/api/v1/connect/register",
"code_challenge_methods_supported": ["S256"],
"grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
"client_id_metadata_document_supported": true
}

Step 3: Client Registration

MCP supports three client registration approaches. Clients SHOULD follow this priority order:

PriorityMethodWhen to Use
1Pre-registered clientClient has existing credentials for this authorization server
2Client ID Metadata DocumentsAS supports client_id_metadata_document_supported — use HTTPS URL as client_id
3Dynamic Client RegistrationAS provides registration_endpoint
curl -X POST https://app.lumoauth.dev/t/acme-corp/api/v1/connect/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP Client",
"redirect_uris": ["http://127.0.0.1:3000/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'

Public Client Authentication

Setting token_endpoint_auth_method: "none" creates a public client that doesn't require a client secret. However, the client_id returned in the registration response must still be sent in all token requests. Public clients use PKCE for security instead of a shared secret.

Step 4: Authorization Request

The MCP client initiates the OAuth 2.1 authorization code flow with PKCE and resource parameter:

# Generate PKCE parameters
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=/+' | head -c 43)
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')

# Build authorization URL
AUTH_URL="https://app.lumoauth.dev/t/acme-corp/api/v1/oauth/authorize?\
response_type=code&\
client_id=CLIENT_ID&\
redirect_uri=http://127.0.0.1:3000/callback&\
scope=mcp:read mcp:write&\
resource=https://mcp.example.com&\
code_challenge=$CODE_CHALLENGE&\
code_challenge_method=S256&\
state=RANDOM_STATE"

# Open in browser for user authentication
echo "Open in browser: $AUTH_URL"
Required Parameters

The resource parameter must match a registered MCP server URI in your tenant. Use the exact URI configured during server registration.

curl -X POST https://app.lumoauth.dev/t/acme-corp/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTHORIZATION_CODE" \
-d "redirect_uri=http://127.0.0.1:3000/callback" \
-d "client_id=CLIENT_ID" \
-d "code_verifier=$CODE_VERIFIER" \
-d "resource=https://mcp.example.com"
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"scope": "mcp:read mcp:write"
}

Step 6: Access the MCP Server

Include the access token in the Authorization header for all MCP requests:

curl -X POST https://mcp.example.com/mcp \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}'

Token in Every Request

Per the MCP spec, authorization MUST be included in every HTTP request from client to server, even if they are part of the same logical session. Tokens MUST NOT be included in the URI query string.

Token Validation

Your MCP server MUST validate access tokens before processing requests. LumoAuth supports two validation methods:

Option A: Token Introspection

curl -X POST https://app.lumoauth.dev/t/acme-corp/api/v1/oauth/introspect \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "CLIENT_ID:CLIENT_SECRET" \
-d "token=eyJhbGciOiJSUzI1NiIs..."
{
"active": true,
"scope": "mcp:read mcp:write",
"client_id": "client_abc123",
"token_type": "Bearer",
"exp": 1739190000,
"aud": "https://mcp.example.com",
"sub": "user-uuid"
}

Option B: JWT Verification

For JWT access tokens (default in LumoAuth), validate locally using the tenant's public keys:

  1. Fetch the JWKS from LumoAuth's jwks_uri

  2. Verify the JWT signature

  3. Check exp (expiration), iss (issuer), and aud (audience)

  4. Verify aud matches your server's Resource URI

    Audience Validation is Critical

MCP servers MUST validate that the aud (audience) claim matches their Resource URI. Without this check, tokens issued for other services could be replayed against your MCP server. This prevents the confused deputy problem.

Error Handling

StatusWhenHeader
401 UnauthorizedNo token, expired token, or invalid tokenWWW-Authenticate: Bearer resource_metadata="..."
403 ForbiddenToken valid but insufficient scopesWWW-Authenticate: Bearer error="insufficient_scope", scope="...", resource_metadata="..."

Common Token Request Errors

ErrorDescriptionSolution
invalid_requestMissing required parameter: client_idAlways include client_id in token requests, even for public clients with token_endpoint_auth_method: "none"
invalid_grantInvalid or expired authorization codeAuthorization codes are single-use and expire after 5 minutes. Ensure you exchange them immediately.
invalid_grantPKCE validation failedVerify your code_verifier matches the code_challenge sent in the authorization request
invalid_clientClient authentication failedFor confidential clients, verify your client_secret is correct. For public clients, ensure client_id is included.
invalid_targetInvalid or unauthorized resourceThe resource parameter must match a registered MCP server Resource URI

Security Considerations

  • PKCE Required: MCP clients MUST use PKCE with S256. LumoAuth rejects authorization requests without a valid code challenge.
  • Resource Binding: Tokens are bound to the MCP server's Resource URI via the resource parameter (RFC 8707) and aud claim.
  • No Token Passthrough: MCP servers MUST NOT forward client tokens to upstream APIs. Each service-to-service call requires its own token.
  • Short-lived Tokens: LumoAuth issues short-lived access tokens (configurable per MCP server). Use refresh tokens for long-lived sessions.
  • HTTPS Only: All authorization server endpoints use HTTPS. Resource URIs should use HTTPS in production.