MCP Authentication
This guide explains how to handle authentication in your MCP server when integrating with Diosc's BYOA (Bring Your Own Authentication) model.
The BYOA Principle
Your MCP server is a pass-through for authentication, not a validator.
User's Browser Diosc Hub Your MCP Server Your API
│ │ │ │
│─ Token ───────────▶│ │ │
│ │─ Token ───────────▶│ │
│ │ │─ Token ────────▶│
│ │ │ │─ Validate
│ │ │ │
│ │ │◀─ Data ────────│
│ │◀─ Data ────────────│ │
│◀─ Response ────────│ │ │
Key insight: Only your API validates the token. Everyone else just forwards it.
Headers from Diosc
When Diosc calls your MCP server, it includes:
POST /messages HTTP/1.1
Host: your-mcp-server.example.com
# User's original auth
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
# Diosc-prefixed headers (same data, clear origin)
X-User-Auth-Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
# User identity (extracted from token, for logging)
X-User-Id: user_123
X-Tenant-Id: acme-corp
# Request tracking
X-Request-Id: req_abc123
X-Session-Id: sess_xyz789
Forwarding to Your API
Basic Pattern
async function callApi(endpoint: string, args: any, authHeaders: Record<string, string>) {
const response = await fetch(`https://api.example.com${endpoint}`, {
method: 'POST',
headers: {
// Forward all auth headers
'Authorization': authHeaders['Authorization'],
'Content-Type': 'application/json',
// Include tracking
'X-Request-Id': authHeaders['X-Request-Id']
},
body: JSON.stringify(args)
});
if (!response.ok) {
// Forward error as-is
throw new Error(`API error: ${response.status} ${await response.text()}`);
}
return response.json();
}
Complete Example
import express from 'express';
const app = express();
// Store auth headers per session
const sessionAuth = new Map<string, Record<string, string>>();
// Extract auth headers from request
function extractAuthHeaders(headers: any): Record<string, string> {
const auth: Record<string, string> = {};
// Primary auth header
if (headers['authorization']) {
auth['Authorization'] = headers['authorization'];
}
// Cookie-based auth
if (headers['cookie']) {
auth['Cookie'] = headers['cookie'];
}
// Custom auth headers (your API might need these)
const customHeaders = ['x-api-key', 'x-client-id', 'x-custom-auth'];
for (const header of customHeaders) {
if (headers[header]) {
auth[header] = headers[header];
}
}
// Diosc tracking headers (useful for logging)
if (headers['x-user-id']) auth['X-User-Id'] = headers['x-user-id'];
if (headers['x-tenant-id']) auth['X-Tenant-Id'] = headers['x-tenant-id'];
if (headers['x-request-id']) auth['X-Request-Id'] = headers['x-request-id'];
return auth;
}
// Handle MCP messages
app.post('/messages', express.json(), async (req, res) => {
const sessionId = req.query.sessionId as string;
// Update stored auth (might be refreshed)
sessionAuth.set(sessionId, extractAuthHeaders(req.headers));
const auth = sessionAuth.get(sessionId)!;
const { method, params } = req.body;
try {
if (method === 'tools/call') {
const result = await executeToolCall(params.name, params.arguments, auth);
sendSseResponse(sessionId, result);
}
res.status(202).end();
} catch (error) {
sendSseError(sessionId, error);
res.status(202).end();
}
});
async function executeToolCall(toolName: string, args: any, auth: Record<string, string>) {
switch (toolName) {
case 'search_orders':
return await callApi('/orders/search', args, auth);
case 'get_customer':
return await callApi(`/customers/${args.customerId}`, {}, auth);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
async function callApi(path: string, body: any, auth: Record<string, string>) {
const response = await fetch(`https://api.example.com${path}`, {
method: 'POST',
headers: {
...auth,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (response.status === 401) {
throw new Error('Authentication failed - user token may be expired');
}
if (response.status === 403) {
throw new Error('Access denied - user lacks permission');
}
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
Handling Auth Errors
When your API rejects the token, return a clear error:
async function callApi(path: string, auth: Record<string, string>) {
const response = await fetch(url, { headers: auth });
if (response.status === 401) {
// Token invalid or expired
return {
isError: true,
content: [{
type: 'text',
text: 'Your session has expired. Please log in again.'
}]
};
}
if (response.status === 403) {
// User lacks permission
return {
isError: true,
content: [{
type: 'text',
text: 'You do not have permission to perform this action.'
}]
};
}
// Success
return {
content: [{
type: 'text',
text: JSON.stringify(await response.json())
}]
};
}
The AI will explain these errors to the user in a helpful way.
Multi-Tenant Considerations
If your API requires tenant context:
async function callApi(path: string, auth: Record<string, string>) {
const tenantId = auth['X-Tenant-Id'];
// Option 1: Tenant in header
const response = await fetch(`https://api.example.com${path}`, {
headers: {
...auth,
'X-Tenant-ID': tenantId // Your API's expected header
}
});
// Option 2: Tenant in URL
const response = await fetch(`https://${tenantId}.api.example.com${path}`, {
headers: auth
});
// Option 3: Tenant from token (API extracts it)
const response = await fetch(`https://api.example.com${path}`, {
headers: auth // Token contains tenant claim
});
}
Token Refresh Handling
Diosc handles token refresh on the client side. Your MCP server just needs to:
- Forward whatever token it receives
- Return clear errors when tokens fail
- Accept updated tokens on subsequent requests
// Each request might have a fresh token
app.post('/messages', (req, res) => {
const sessionId = req.query.sessionId;
// Always use the latest auth headers
sessionAuth.set(sessionId, extractAuthHeaders(req.headers));
// ... handle request
});
Different Auth Strategies
JWT Bearer Token
// Most common pattern
headers: {
'Authorization': `Bearer ${token}`
}
API Key
// For service-to-service
headers: {
'X-API-Key': apiKey
}
Session Cookie
// For cookie-based auth
headers: {
'Cookie': `session=${sessionId}`
}
Multiple Auth Methods
// Some APIs need multiple credentials
headers: {
'Authorization': `Bearer ${token}`,
'X-API-Key': clientApiKey,
'X-Client-ID': clientId
}
Security Best Practices
DO
// ✅ Forward headers as-is
headers: { 'Authorization': auth['Authorization'] }
// ✅ Log for debugging (without sensitive data)
console.log(`API call for user ${auth['X-User-Id']}`);
// ✅ Return clear error messages
throw new Error('Permission denied: Cannot delete orders');
DON'T
// ❌ Don't validate tokens
if (!verifyJwt(token)) throw new Error('Invalid');
// ❌ Don't log full tokens
console.log(`Token: ${auth['Authorization']}`);
// ❌ Don't store tokens long-term
database.save({ userId, token });
// ❌ Don't decode and use token claims for authorization
const claims = decodeJwt(token);
if (claims.role !== 'admin') throw new Error('Admins only');
// (Your API should check this, not the MCP server)
Testing Auth Flow
Test with curl
# Simulate Diosc calling your MCP server
curl -X POST http://localhost:3000/messages \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <test-token>" \
-H "X-User-Id: test-user" \
-d '{
"method": "tools/call",
"params": {
"name": "search_orders",
"arguments": {"status": "pending"}
}
}'
Test cases
- Valid token → API returns data → Success
- Expired token → API returns 401 → Clear error message
- Wrong permissions → API returns 403 → Permission denied message
- No token → API returns 401 → Ask user to log in
- Malformed token → API returns 401 → Clear error message
Next Steps
- Building Tools - Design effective MCP tools
- Testing - Comprehensive testing strategies
- BYOA Concepts - Understand the full auth model