MCP auth setup
Configure Hanzo IAM as the OAuth server for your MCP server.
This guide configures Hanzo IAM as the authorization server for an MCP server: application, scopes, and consent in Hanzo IAM, then Protected Resource Metadata and token validation on the MCP server.
Prerequisites
- A running Hanzo IAM instance (Server installation or Hanzo IAM Cloud)
- Admin access to Hanzo IAM
- Your MCP server’s public URL (e.g.
https://your-mcp-server.com)
Part 1: Hanzo IAM 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 Hanzo IAM 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.
1. Find the **Grant types** setting
2. 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.
1. Scroll to **Custom Scopes** section
2. Click **Add Scope** for each permission you want to define
3. For each scope, provide:
- **Name**: Use `resource:action` format (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")
**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:action` pattern)
- Write clear descriptions—users see these during authorization
- Start with fewer scopes, add more as your tools evolve
**Reference**: See [Custom Scopes](../application/scopes.md) for detailed guidance.
### Step 5: Configure Consent Policy
The consent screen shows users what permissions they're granting.
1. Find **Consent Policy** setting
2. 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:
1. Go to **Organization Settings** (not Application Settings)
2. Find **Enable Dynamic Client Registration**
3. 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](../application/dynamic-client-registration.md) for implementation details.
### Step 7: Note Your Discovery URLs
Your MCP server will need these Hanzo IAM URLs for token validation:
- **Authorization Server Metadata**: `https://your-iam.com/.well-known/oauth-authorization-server`
- **OIDC Discovery**: `https://your-iam.com/.well-known/openid-configuration`
- **JWKS Endpoint**: `https://your-iam.com/.well-known/jwks`
Verify the endpoints:
```bash
# Check OAuth metadata
curl https://your-iam.com/.well-known/oauth-authorization-server
# Check OIDC discovery
curl https://your-iam.com/.well-known/openid-configuration
# Check JWKS (JSON Web Key Set for token validation)
curl https://your-iam.com/.well-known/jwks
## Part 2: MCP Server Configuration
Now configure your MCP server to use Hanzo IAM 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**:
```json
{
"resource": "https://your-mcp-server.com",
"authorization_servers": ["https://your-iam.com"],
"scopes_supported": [
"files:read",
"files:write",
"files:list"
],
"bearer_methods_supported": ["header"]
}
**Field descriptions**:
- `resource`: Your MCP server's public URL (used as the `aud` claim in tokens)
- `authorization_servers`: Array of OAuth servers (just Hanzo IAM's URL)
- `scopes_supported`: The custom scopes you defined in Step 4
- `bearer_methods_supported`: Always `["header"]` for MCP (tokens in Authorization header)
**Example implementations**:
#### Python (Flask)
```python
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-iam.com"],
"scopes_supported": ["files:read", "files:write", "files:list"],
"bearer_methods_supported": ["header"]
})
#### Node.js (Express)
```javascript
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-iam.com'],
scopes_supported: ['files:read', 'files:write', 'files:list'],
bearer_methods_supported: ['header']
});
});
#### Go (net/http)
```go
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-iam.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
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**:
```json
{
"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 Hanzo IAM's JWKS endpoint.
**Token validation steps**:
1. **Extract the token** from the `Authorization: Bearer <token>` header
2. **Fetch JWKS** from `https://your-iam.com/.well-known/jwks`
3. **Verify the signature** using the public key from JWKS
4. **Check the `aud` claim** matches your server's resource URI (`https://your-mcp-server.com`)
5. **Check the `exp` claim** to ensure the token hasn't expired
6. **Extract scopes** from the `scope` claim (space-separated string)
See [Third-party Integration](./third-party-integration.md) 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:
```python
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
```bash
curl https://your-mcp-server.com/.well-known/oauth-protected-resource
Expected output:
```json
{
"resource": "https://your-mcp-server.com",
"authorization_servers": ["https://your-iam.com"],
"scopes_supported": ["files:read", "files:write"],
"bearer_methods_supported": ["header"]
}
### Test the 401 Challenge
```bash
curl -i https://your-mcp-server.com/api/mcp
Expected response:
```http
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)
1. Configure your MCP client to connect to `https://your-mcp-server.com`
2. The client should automatically discover Hanzo IAM via Protected Resource Metadata
3. The client will redirect you to Hanzo IAM's authorization page
4. After consenting, the client receives a token and can call your tools
## Troubleshooting
### "Invalid redirect URI" error
**Problem**: Hanzo IAM 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 Hanzo IAM's JWT tokens.
**Solution**:
- Ensure you're using the correct JWKS endpoint: `https://your-iam.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](https://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 →](./third-party-integration.md)**: See complete, runnable code in Python, Node.js, and Go
- **[MCP Specification →](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)**: Read the official MCP authorization spec
- **[OAuth 2.0 in Hanzo IAM →](../how-to-connect/oauth.md)**: Learn more about Hanzo IAM's OAuth implementation
- **[OIDC Client →](../how-to-connect/oidc-client.md)**: Understand Hanzo IAM's OIDC capabilitiesHow is this guide?
Last updated on