Skip to Content
HomeBuild toolsSecure Your MCP Server with OAuth

Adding Resource Server Authentication to Your MCP Server

Resource Server authentication enables your HTTP server to act as an OAuth 2.1 Protected Resource (compliant with MCP’s specification for Authorization ), validating Bearer tokens on every request. This unlocks support for tool-level authorization and secrets on HTTP servers, allowing you to host secure anywhere (local, on-premise, or third-party hosted).

Outcomes

Add MCP compliant OAuth 2.1 front-door authentication  to your HTTP server to enable -level authorization and secrets.

You will Learn

  • What Resource Server authentication is and why it’s needed
  • How to configure your server to validate OAuth tokens
  • How to support multiple authorization servers
  • How to use environment variables for production deployments

Prerequisites

  • An existing server created with arcade new (see Create an MCP Server)
  • Understanding of MCP Authorization 
  • An OAuth 2.1 compliant authorization server (e.g., WorkOS AuthKit, Auth0, Descope, etc.)
  • Authorization server’s JWKS endpoint URL

Understanding Resource Server Authentication

What is it?

Resource Server authentication turns your server into an OAuth 2.1 Protected Resource  that validates Bearer tokens on every HTTP request. Your trusts one or more Authorization Servers  to issue valid tokens for accessing your MCP server.

Why is it needed?

By default, HTTP servers cannot use that require authorization or secrets for security reasons.

Resource Server authentication solves this by:

  1. Authenticating every request - Validates the Bearer token before processing any messages
  2. Extracting identity - The token’s sub claim becomes the context.user_id for execution
  3. Enabling secure - Tools requiring authorization or secrets can now safely execute over HTTP
  4. Supporting OAuth discovery - clients can automatically discover your authentication requirements

Resource Server auth vs -level auth: Resource Server authentication secures access to your server, while tool-level authorization secures access to third-party APIs that your tools use. They work together: Resource Server auth identifies who is calling your server, and tool-level auth enables tools to act on behalf of that .

Choose Your Configuration Approach

The arcade_mcp_server.resource_server module provides two validators:

ResourceServer - Full-featured OAuth 2.1 Resource Server with:

  • Support for multiple authorization servers (multi-IdP, regional endpoints)
  • OAuth discovery metadata endpoint
  • Environment variable configuration
  • Best for production deployments

Configure Your Authorization Server

First, gather these details from your authorization server:

  • Authorization Server URL - The base URL of your authorization server (e.g., https://your-app.authkit.app)
  • Issuer - The expected iss claim in tokens (usually same as authorization server URL)
  • JWKS URI - Where to fetch public keys for token verification (e.g., https://your-app.authkit.app/oauth2/jwks)
  • Canonical URL - Your server’s public URL (e.g., http://127.0.0.1:8000/mcp if running locally)

The Canonical URL must match the aud (audience) claim in tokens issued by your authorization server. Configure your authorization server to issue tokens with your server’s URL as the audience.

Add Authentication to Your Server

Update your server.py to add the auth parameter to MCPApp:

Python
server.py
#!/usr/bin/env python3 """my_server MCP server""" from arcade_mcp_server import MCPApp from arcade_mcp_server.resource_server import ( AccessTokenValidationOptions, AuthorizationServerEntry, ResourceServer, ) # Setup your resource server that trusts a single Authkit authorization server resource_server = ResourceServer( canonical_url="http://127.0.0.1:8000/mcp", authorization_servers=[ AuthorizationServerEntry( authorization_server_url="https://your-workos.authkit.app", issuer="https://your-workos.authkit.app", jwks_uri="https://your-workos.authkit.app/oauth2/jwks", algorithm="RS256", # Authkit doesn't set the aud claim as the MCP server's canonical URL validation_options=AccessTokenValidationOptions( verify_aud=False, ), ) ], ) # Pass the resource_server to MCPApp app = MCPApp( name="my_server", version="1.0.0", auth=resource_server # Enable Resource Server authentication ) # Your tools here... @app.tool def greet(name: Annotated[str, "The name of the person to greet"]) -> str: """Greet a person by name.""" return f"Hello, {name}!" if __name__ == "__main__": app.run(transport="http", host="127.0.0.1", port=8000)

Run Your Authenticated Server

Start your server with HTTP transport:

Terminal
uv run server.py

Your server now requires valid Bearer tokens for all requests. You should see output like:

Terminal
INFO | 14:23:45 | Starting my_server v1.0.0 with 3 tools INFO | 14:23:45 | Resource Server authentication enabled: True INFO | 14:23:45 | Accepted authorization server(s): https://your-app.authkit.app

OAuth Discovery

Now that your server is protected, you can see that your server exposes an OAuth discovery endpoint at http://127.0.0.1:8000/mcp/.well-known/oauth-protected-resource. This endpoint is used by clients to discover the authorization servers that are trusted by your server.

Terminal
curl http://127.0.0.1:8000/.well-known/oauth-protected-resource

You should see a response like:

HTTP
{ "resource":"http://127.0.0.1:8000/mcp", "authorization_servers":["https://your-workos.authkit.app"] }

clients can use this endpoint to automatically discover which authorization servers issue valid tokens for your server.

Verify Your Server is Protected

Try calling your server without a token:

Terminal
curl -i http://127.0.0.1:8000/mcp/

You should receive a 401 response with WWW-Authenticate header:

HTTP
HTTP/1.1 401 Unauthorized date: Tue, 02 Dec 2025 01:00:54 GMT server: uvicorn www-authenticate: Bearer, resource_metadata="http://127.0.0.1:8000/mcp/.well-known/oauth-protected-resource" access-control-allow-origin: * access-control-allow-methods: GET, POST, DELETE access-control-allow-headers: Content-Type, Authorization, Mcp-Session-Id access-control-expose-headers: WWW-Authenticate, Mcp-Session-Id content-length: 12 Unauthorized

Test Your Server with a Valid Token

The easiest way to test your secure server by using the MCP Inspector  as your client & connecting to your server from it.

TODO: INSERT VIDEO HERE

Advanced Configuration

Custom Token Validation Options

Disable specific validations when needed:

Python
from arcade_mcp_server.resource_server import ( ResourceServer, AuthorizationServerEntry, AccessTokenValidationOptions, ) resource_server = ResourceServer( canonical_url="http://127.0.0.1:8000/mcp", authorization_servers=[ AuthorizationServerEntry( authorization_server_url="https://your-app.authkit.app", issuer="https://your-app.authkit.app", jwks_uri="https://your-app.authkit.app/oauth2/jwks", validation_options=AccessTokenValidationOptions( verify_aud=False, # Disable audience validation verify_exp=True, # Still verify expiration (default) verify_iat=True, # Still verify issued-at (default) verify_iss=True, # Still verify issuer (default) ), ) ], )

Security Note: Token signature verification is always enabled and cannot be disabled. Additionally, the sub claim must always be present. Only disable other validations if your authorization server doesn’t comply with and you accept the risk of not validating all claims in the token.

Different JWT Algorithms

Python
AuthorizationServerEntry( authorization_server_url="https://auth.example.com", issuer="https://auth.example.com", jwks_uri="https://auth.example.com/jwks", algorithm="ES256", # Use ECDSA instead of RSA )

Supported algorithms: RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512

Regional Authorization Servers with Shared Keys

Python
resource_server = ResourceServer( canonical_url="https://mcp.example.com", authorization_servers=[ AuthorizationServerEntry( authorization_server_url="https://auth-us.example.com", issuer="https://auth.example.com", # Same issuer jwks_uri="https://auth.example.com/jwks", # Shared JWKS ), AuthorizationServerEntry( authorization_server_url="https://auth-eu.example.com", issuer="https://auth.example.com", # Same issuer jwks_uri="https://auth.example.com/jwks", # Shared JWKS ), ], )

How It Works

  1. Client makes request with Authorization: Bearer <token> header
  2. Middleware intercepts every HTTP request before processing
  3. Token validation occurs:
    • Fetches JWKS from authorization server
    • Verifies token signature
    • Checks expiration, issuer, and audience
    • Extracts sub claim as ID
  4. Resource owner stored in request
  5. processing continues with authenticated
  6. execute with context.user_id set from token’s sub claim

Security Features

  • No token caching - Every request validates the token fresh (per spec)
  • JWKS caching - Public keys cached for performance (default 1 hour)
  • Algorithm enforcement - Prevents algorithm confusion attacks
  • Signature verification - Always enabled, cannot be disabled
  • RFC 6750 compliant - Standard OAuth 2.0 Bearer token usage
  • RFC 9728 compliant - OAuth 2.0 Protected Resource Metadata

Common Authorization Server Configurations

WorkOS AuthKit

Python
AuthorizationServerEntry( authorization_server_url="https://your-app.authkit.app", issuer="https://your-app.authkit.app", jwks_uri="https://your-app.authkit.app/oauth2/jwks", validation_options=AccessTokenValidationOptions( verify_aud=False, # AuthKit may not set aud claim ), )

Auth0

Python
AuthorizationServerEntry( authorization_server_url="https://your-tenant.auth0.com", issuer="https://your-tenant.auth0.com/", jwks_uri="https://your-tenant.auth0.com/.well-known/jwks.json", )

Descope

Python
AuthorizationServerEntry( authorization_server_url="https://api.descope.com", issuer="https://api.descope.com/{project-id}", jwks_uri="https://api.descope.com/{project-id}/.well-known/jwks.json", )

GitHub Actions

Python
AuthorizationServerEntry( authorization_server_url="https://github.com/login/oauth", issuer="https://token.actions.githubusercontent.com", jwks_uri="https://token.actions.githubusercontent.com/.well-known/jwks", )

Next Steps

Last updated on

Adding Resource Server Authentication | Arcade Docs