Set up Casdoor for your MCP server
This guide explains how to make Casdoor the authorization server of your own MCP server: first the configuration in Casdoor, then the changes to your MCP server.
Learning outcomes
- Create an application with custom scopes for your MCP server.
- Let MCP clients register themselves.
- Publish Protected Resource Metadata and challenge unauthenticated requests.
- Validate tokens and enforce scopes in your tools.
What you need
- A running Casdoor instance and administrator access to it. See Install the Casdoor server.
- The public URL of your MCP server, for example
https://your-mcp-server.com
Configure Casdoor
Create the application
-
In the Casdoor admin console, go to Identity > Applications and add an application.
-
Fill in the Basic tab:
Field Value Name A descriptive name, for example my-files-mcp-serverOrganization The organization whose users use the MCP server Category Agent. This turns on custom scopes. See Application categoriesType MCP. Casdoor sets it when you selectAgent
Add the redirect URLs
Add the callback URLs of the MCP clients that use your server to Redirect URLs, for example https://mcp.example.com/oauth/callback. Casdoor sends users back to these URLs after authorization.
Casdoor compares redirect URLs exactly, including the scheme, the port, and the path. Clients that register themselves through dynamic client registration bring their own redirect URLs, so you don't add those here.
Set the grant types
In Grant types, turn on:
- Authorization Code: The flow that MCP uses.
- Refresh Token: Lets clients stay signed in without a new authorization.
Leave the implicit grant and the password grant off. MCP doesn't use them.
Define scopes
Define one scope for each capability of your tools.
-
In Scopes, add a row for each scope:
Column Value Name The scope in the form resource:action, for examplefiles:readDisplay name A short name for users, for example Read FilesDescription What the scope allows. Users read it on the consent screen -
Save the application.
For a file server, the scopes could be:
| 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 |
For a database 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 |
Keep the scopes fine-grained, so that users can grant part of the access. Start with few scopes and add more as your tools grow. See Define custom scopes.
Understand the consent screen
Because the application defines custom scopes, Casdoor shows a consent screen when a client requests scopes that the user hasn't granted before. The screen lists the scopes with their display names and descriptions. Casdoor remembers the grant, and users and administrators can revoke it on the Consents page.
Allow dynamic client registration
MCP clients such as Claude Desktop register themselves on first use. To allow this:
- Open the edit page of the organization in which the clients register. Without an
organizationparameter in the registration URL, this isbuilt-in. - Turn on Enable dynamic client registration and save.
Anyone can then register a client through /api/oauth/register. See Register clients dynamically.
Check the discovery URLs
Your MCP server uses the following URLs of Casdoor:
| URL | Purpose |
|---|---|
https://your-casdoor.com/.well-known/oauth-authorization-server | Authorization Server Metadata |
https://your-casdoor.com/.well-known/openid-configuration | OpenID Connect Discovery |
https://your-casdoor.com/.well-known/jwks | Public keys for token validation |
Check that they respond:
# 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
Configure the MCP server
Publish Protected Resource Metadata
Serve the following JSON document at GET /.well-known/oauth-protected-resource:
{
"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 | Value |
|---|---|
resource | Public URL of your MCP server. Tokens for your server carry it in the aud claim |
authorization_servers | The URL of Casdoor |
scopes_supported | The scopes that you defined in Casdoor |
bearer_methods_supported | ["header"]. Clients send the token in the Authorization header |
In Python with 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"]
})
In Node.js with 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']
});
});
In Go with 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)
}
Challenge unauthenticated requests
When a request has no valid token, answer with HTTP 401 and a WWW-Authenticate header whose resource_metadata parameter points to your metadata document:
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
With a body such as:
{
"error": "unauthorized",
"message": "Authentication required. Use OAuth 2.0 with the authorization server listed in the Protected Resource Metadata."
}
Validate tokens
For each request with a bearer token:
- Take the token from the
Authorization: Bearer <token>header. - Get the public keys from
https://your-casdoor.com/.well-known/jwks. Cache them and refresh the cache periodically. - Verify the signature of the token with the matching key.
- Check that the
audclaim is the resource URI of your server, for examplehttps://your-mcp-server.com. - Check that the
expclaim is in the future. - Read the scopes from the
scopeclaim, a space-separated string.
For complete code, see Validate Casdoor tokens in your MCP server.
Enforce scopes in the tools
Check the required scope in each tool before it does anything. For example, a read_file tool requires files:read:
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()
Return a clear error when a scope is missing, and log authorization failures.
Verify the setup
-
Request the metadata document:
curl https://your-mcp-server.com/.well-known/oauth-protected-resourceExpected output:
{
"resource": "https://your-mcp-server.com",
"authorization_servers": ["https://your-casdoor.com"],
"scopes_supported": ["files:read", "files:write"],
"bearer_methods_supported": ["header"]
} -
Send a request without a token:
curl -i https://your-mcp-server.com/api/mcpExpected response:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="MCP Server", resource_metadata="https://your-mcp-server.com/.well-known/oauth-protected-resource" -
Connect an MCP client, such as Claude Desktop, to
https://your-mcp-server.com. The client finds Casdoor through the metadata, sends you to the Casdoor sign-in page, and after you consent, calls your tools with a token.
Troubleshooting
Casdoor reports an invalid redirect URL
The redirect URL of the client isn't in the Redirect URLs of the application, or doesn't match it exactly. Add the exact URL. For a client that registered itself, check the redirect URLs of its dcr_ application.
The signature of the token can't be verified
- Use the JWKS endpoint
https://your-casdoor.com/.well-known/jwks. - Check that the token is a valid JWT, for example at jwt.io.
The token is rejected for its audience
The aud claim must equal the resource of your metadata document exactly, including the scheme and without a trailing slash. Check that the client sends the same value in the resource parameter.
The token has expired too early or too late
Synchronize the clock of your server. JWT expiry depends on it.
The consent screen doesn't appear
Casdoor asks for consent only for custom scopes that the user hasn't granted yet. Check that the client requests your scopes, and revoke earlier grants on the Consents page to test again.