Ana içeriğe geç

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​

  1. In the Casdoor admin console, go to Identity > Applications and add an application.

  2. Fill in the Basic tab:

    FieldValue
    NameA descriptive name, for example my-files-mcp-server
    OrganizationThe organization whose users use the MCP server
    CategoryAgent. This turns on custom scopes. See Application categories
    TypeMCP. Casdoor sets it when you select Agent

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.

  1. In Scopes, add a row for each scope:

    ColumnValue
    NameThe scope in the form resource:action, for example files:read
    Display nameA short name for users, for example Read Files
    DescriptionWhat the scope allows. Users read it on the consent screen
  2. Save the application.

For a file server, the scopes could be:

NameDisplay NameDescription
files:readRead FilesView and download files from your storage
files:writeWrite FilesCreate, modify, and delete files in your storage
files:listList FilesSee file names and metadata in directories

For a database server:

NameDisplay NameDescription
db:queryQuery DatabaseExecute read-only database queries
db:modifyModify DatabaseCreate, update, and delete database records
db:adminDatabase AdminManage 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.

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:

  1. Open the edit page of the organization in which the clients register. Without an organization parameter in the registration URL, this is built-in.
  2. 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:

URLPurpose
https://your-casdoor.com/.well-known/oauth-authorization-serverAuthorization Server Metadata
https://your-casdoor.com/.well-known/openid-configurationOpenID Connect Discovery
https://your-casdoor.com/.well-known/jwksPublic 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"]
}
FieldValue
resourcePublic URL of your MCP server. Tokens for your server carry it in the aud claim
authorization_serversThe URL of Casdoor
scopes_supportedThe 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:

  1. Take the token from the Authorization: Bearer <token> header.
  2. Get the public keys from https://your-casdoor.com/.well-known/jwks. Cache them and refresh the cache periodically.
  3. Verify the signature of the token with the matching key.
  4. Check that the aud claim is the resource URI of your server, for example https://your-mcp-server.com.
  5. Check that the exp claim is in the future.
  6. Read the scopes from the scope claim, 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​

  1. Request the metadata document:

    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"]
    }
  2. Send a request without a token:

    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"
  3. 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.

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.

See also​