MCP auth setup
This guide configures Casdoor as the authorization server for an MCP server: application, scopes, and consent in Casdoor, then Protected Resource Metadata and token validation on the MCP server.
Prerequisites
- A running Casdoor instance (Server installation or Casdoor Cloud)
- Admin access to Casdoor
- Your MCP server’s public URL (e.g.
https://your-mcp-server.com)
Part 1: Casdoor configuration
Step 1: Create an application
- Go to Applications → Add Application
- Fill in the basic details:
- Name: Choose a descriptive name (e.g., "My Files MCP Server")
- Organization: Select the organization that will own this application
- Category: Select Agent (this unlocks MCP-specific features)
- Type: Select MCP (automatically set when Category is Agent)
The Agent category with MCP type optimizes the application for programmatic access and enables custom scope configuration.
Reference: See Application Categories for more details on Agent vs Default applications.
Step 2: Configure Redirect URIs
Redirect URIs are where Casdoor sends users after authorization. For MCP servers, add URIs for both development and production.
- In the application settings, find Redirect URIs
- Add development URIs for local testing:
http://localhost:*(wildcard for any local port)http://127.0.0.1:*
- Add production URIs for deployment:
https://your-mcp-server.com/oauth/callback- Any other callback URLs your server uses
Example configuration:
http://localhost:*
http://127.0.0.1:*
https://mcp.example.com/oauth/callback
Security note: Wildcard URIs (*) are convenient for development but should be restricted in production. Consider using exact URLs for production deployments.
Step 3: Set Grant Types
Configure which OAuth grant types your application supports.
- Find the Grant types setting
- Enable these grant types:
- ✅ authorization_code: The primary OAuth flow for MCP
- ✅ refresh_token: Allows long-lived access without re-authentication
Do not enable:
- ❌ implicit: Deprecated and insecure for MCP use
- ❌ password: Not compatible with MCP's authorization flow
Step 4: Define Custom Scopes
Custom scopes represent the permissions your MCP server's tools require. Each scope should map to a logical capability.
- Scroll to Custom Scopes section
- Click Add Scope for each permission you want to define
- For each scope, provide:
- Name: Use
resource:actionformat (e.g.,files:read,db:query) - Display Name: Short, user-friendly name (e.g., "Read Files")
- Description: Explain what the scope allows (e.g., "View and download files from your storage")
- Name: Use
Example scopes for a file management MCP server:
| Name | Display Name | Description |
|---|---|---|
files:read | Read Files | View and download files from your storage |
files:write | Write Files | Create, modify, and delete files in your storage |
files:list | List Files | See file names and metadata in directories |
Example scopes for a database MCP server:
| Name | Display Name | Description |
|---|---|---|
db:query | Query Database | Execute read-only database queries |
db:modify | Modify Database | Create, update, and delete database records |
db:admin | Database Admin | Manage schemas, tables, and database settings |
Best practices:
- Keep scopes granular—users should be able to grant partial access
- Use consistent naming (e.g.,
resource:actionpattern) - Write clear descriptions—users see these during authorization
- Start with fewer scopes, add more as your tools evolve
Reference: See Custom Scopes for detailed guidance.
Step 5: Configure Consent Policy
The consent screen shows users what permissions they're granting.
- Find Consent Policy setting
- Choose one of:
- Always: Show consent screen on every authorization (recommended for sensitive data)
- Once: Show consent only on first authorization (better UX for frequent access)
- Never: Skip consent (only for trusted first-party applications)
For MCP servers with powerful tools (file access, database modifications), we recommend Always or Once to ensure users understand what they're authorizing.
Step 6: Enable Dynamic Client Registration (Optional)
If your MCP server will be distributed to end users who need to register their own clients:
- Go to Organization Settings (not Application Settings)
- Find Enable Dynamic Client Registration
- Toggle to Enabled
When enabled, anyone can register new OAuth clients via /api/oauth/register without admin intervention. This is essential for MCP clients like Claude Desktop that auto-register on first use.
Reference: See Dynamic Client Registration for implementation details.
Step 7: Note Your Discovery URLs
Your MCP server will need these Casdoor URLs for token validation:
- Authorization Server Metadata:
https://your-casdoor.com/.well-known/oauth-authorization-server - OIDC Discovery:
https://your-casdoor.com/.well-known/openid-configuration - JWKS Endpoint:
https://your-casdoor.com/.well-known/jwks
Verify the endpoints:
# Check OAuth metadata
curl https://your-casdoor.com/.well-known/oauth-authorization-server
# Check OIDC discovery
curl https://your-casdoor.com/.well-known/openid-configuration
# Check JWKS (JSON Web Key Set for token validation)
curl https://your-casdoor.com/.well-known/jwks
Part 2: MCP Server Configuration
Now configure your MCP server to use Casdoor for authentication.
Step 1: Implement Protected Resource Metadata Endpoint
Your MCP server must serve a JSON document at /.well-known/oauth-protected-resource that tells clients where to find the authorization server.
Endpoint: GET /.well-known/oauth-protected-resource
Response:
{
"resource": "https://your-mcp-server.com",
"authorization_servers": ["https://your-casdoor.com"],
"scopes_supported": [
"files:read",
"files:write",
"files:list"
],
"bearer_methods_supported": ["header"]
}
Field descriptions:
resource: Your MCP server's public URL (used as theaudclaim in tokens)authorization_servers: Array of OAuth servers (just Casdoor's URL)scopes_supported: The custom scopes you defined in Step 4bearer_methods_supported: Always["header"]for MCP (tokens in Authorization header)
Example implementations:
Python (Flask)
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/.well-known/oauth-protected-resource')
def protected_resource_metadata():
return jsonify({
"resource": "https://your-mcp-server.com",
"authorization_servers": ["https://your-casdoor.com"],
"scopes_supported": ["files:read", "files:write", "files:list"],
"bearer_methods_supported": ["header"]
})
Node.js (Express)
const express = require('express');
const app = express();
app.get('/.well-known/oauth-protected-resource', (req, res) => {
res.json({
resource: 'https://your-mcp-server.com',
authorization_servers: ['https://your-casdoor.com'],
scopes_supported: ['files:read', 'files:write', 'files:list'],
bearer_methods_supported: ['header']
});
});
Go (net/http)
package main
import (
"encoding/json"
"net/http"
)
type ProtectedResourceMetadata struct {
Resource string `json:"resource"`
AuthorizationServers []string `json:"authorization_servers"`
ScopesSupported []string `json:"scopes_supported"`
BearerMethodsSupported []string `json:"bearer_methods_supported"`
}
func protectedResourceHandler(w http.ResponseWriter, r *http.Request) {
metadata := ProtectedResourceMetadata{
Resource: "https://your-mcp-server.com",
AuthorizationServers: []string{"https://your-casdoor.com"},
ScopesSupported: []string{"files:read", "files:write", "files:list"},
BearerMethodsSupported: []string{"header"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(metadata)
}
func main() {
http.HandleFunc("/.well-known/oauth-protected-resource", protectedResourceHandler)
http.ListenAndServe(":8080", nil)
}
Step 2: Return 401 Challenges on Unauthorized Requests
When an MCP client connects without a valid token, your server must return a 401 response with a WWW-Authenticate header.
Response headers:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="MCP Server", resource_metadata="https://your-mcp-server.com/.well-known/oauth-protected-resource"
Content-Type: application/json
Response body:
{
"error": "unauthorized",
"message": "Authentication required. Use OAuth 2.0 with the authorization server listed in the Protected Resource Metadata."
}
The resource_metadata parameter in the WWW-Authenticate header tells the client where to find your Protected Resource Metadata endpoint.
Step 3: Validate JWT Tokens
When a client sends a request with a Bearer token, your server must validate it against Casdoor's JWKS endpoint.
Token validation steps:
- Extract the token from the
Authorization: Bearer <token>header - Fetch JWKS from
https://your-casdoor.com/.well-known/jwks - Verify the signature using the public key from JWKS
- Check the
audclaim matches your server's resource URI (https://your-mcp-server.com) - Check the
expclaim to ensure the token hasn't expired - Extract scopes from the
scopeclaim (space-separated string)
See Third-party Integration for complete code examples in Python, Node.js, and Go.
Step 4: Enforce Scopes in Tool Handlers
Each MCP tool should check that the token contains the required scopes.
Example: A read_file tool requires the files:read scope:
def read_file(token_scopes, file_path):
if "files:read" not in token_scopes:
raise PermissionError("Missing required scope: files:read")
# Proceed with file reading
with open(file_path, 'r') as f:
return f.read()
Best practices:
- Check scopes before executing any privileged operation
- Return clear error messages when scopes are missing
- Log authorization failures for security auditing
Testing Your Setup
Test the Protected Resource Metadata
curl https://your-mcp-server.com/.well-known/oauth-protected-resource
Expected output:
{
"resource": "https://your-mcp-server.com",
"authorization_servers": ["https://your-casdoor.com"],
"scopes_supported": ["files:read", "files:write"],
"bearer_methods_supported": ["header"]
}
Test the 401 Challenge
curl -i https://your-mcp-server.com/api/mcp
Expected response:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="MCP Server", resource_metadata="https://your-mcp-server.com/.well-known/oauth-protected-resource"
Test with Claude Desktop (or other MCP client)
- Configure your MCP client to connect to
https://your-mcp-server.com - The client should automatically discover Casdoor via Protected Resource Metadata
- The client will redirect you to Casdoor's authorization page
- After consenting, the client receives a token and can call your tools
Troubleshooting
"Invalid redirect URI" error
Problem: Casdoor rejects the authorization request with an invalid redirect URI error.
Solution: Verify that the client's redirect URI is listed in Step 2 (Redirect URIs). For Claude Desktop, you may need http://localhost:*.
"Token signature verification failed"
Problem: Your server can't verify Casdoor's JWT tokens.
Solution:
- Ensure you're using the correct JWKS endpoint:
https://your-casdoor.com/.well-known/jwks - Check that your server's system clock is synchronized (JWT expiry is time-sensitive)
- Verify the token is a valid JWT (use jwt.io to inspect it)
"Audience mismatch" error
Problem: The token's aud claim doesn't match your server's resource URI.
Solution: Ensure the resource field in your Protected Resource Metadata exactly matches your server's public URL, including scheme (https://) and no trailing slash.
Consent screen not showing
Problem: Users are not seeing the consent screen when authorizing.
Solution: Check the Consent Policy in Step 5. If set to "Never", the consent screen is skipped. Change to "Once" or "Always".
Next Steps
- Integration Examples →: See complete, runnable code in Python, Node.js, and Go
- MCP Specification →: Read the official MCP authorization spec
- OAuth 2.0 in Casdoor →: Learn more about Casdoor's OAuth implementation
- OIDC Client →: Understand Casdoor's OIDC capabilities