OAuth Authentication
Secure your MCP server with OAuth authentication and protect resources with Bearer tokens.
Overview
The MCP Server adapter supports OAuth 2.0 authentication, allowing you to secure your server and control access to your tools and resources. This is especially important when exposing your API to external AI agents.
If your MCP server contains sensitive data or operations, you should always enable OAuth authentication.
Basic OAuth Setup
Builder Pattern
import { IgniterMcpServer } from '@igniter-js/adapter-mcp-server';
const { handler, auth } = IgniterMcpServer
.create()
.router(AppRouter)
.withOAuth({
issuer: 'https://auth.example.com',
resourceMetadataPath: '/.well-known/oauth-protected-resource',
scopes: ['mcp:read', 'mcp:write'],
verifyToken: async ({ request, bearerToken, context }) => {
// context is automatically typed from your router!
const result = await verifyJWT(bearerToken);
return {
valid: result.valid,
user: result.user
};
}
})
.build();
// Export the handler and OAuth endpoints
export const GET = handler;
export const POST = handler;
export const OPTIONS = auth.cors;Function API
import { createMcpAdapter } from '@igniter-js/adapter-mcp-server';
const { server, auth } = createMcpAdapter({
router: AppRouter,
oauth: {
issuer: 'https://auth.example.com',
resourceMetadataPath: '/.well-known/oauth-protected-resource',
scopes: ['mcp:read', 'mcp:write'],
verifyToken: async ({ request, bearerToken, context }) => {
const result = await verifyJWT(bearerToken);
return {
valid: result.valid,
user: result.user
};
}
}
});
export const GET = server;
export const POST = server;
export const OPTIONS = auth.cors;OAuth Configuration Options
issuer (Required)
The OAuth authorization server URL:
withOAuth({
issuer: 'https://auth.example.com',
// ...
})resourceMetadataPath (Optional)
Path to the OAuth protected resource metadata endpoint. Defaults to /.well-known/oauth-protected-resource:
withOAuth({
issuer: 'https://auth.example.com',
resourceMetadataPath: '/.well-known/oauth-protected-resource',
// ...
})scopes (Optional)
Required OAuth scopes:
withOAuth({
issuer: 'https://auth.example.com',
scopes: ['mcp:read', 'mcp:write'],
// ...
})verifyToken (Required)
Function to verify Bearer tokens. Receives the request, bearer token, and context:
withOAuth({
issuer: 'https://auth.example.com',
verifyToken: async ({ request, bearerToken, context }) => {
// Your token verification logic
if (!bearerToken) {
return undefined; // No token provided
}
try {
const decoded = await verifyJWT(bearerToken);
return {
valid: true,
user: decoded.user,
scopes: decoded.scopes,
};
} catch (error) {
return undefined; // Invalid token
}
}
})Token Verification
JWT Verification Example
import jwt from 'jsonwebtoken';
withOAuth({
issuer: 'https://auth.example.com',
verifyToken: async ({ bearerToken, context }) => {
if (!bearerToken) {
return undefined;
}
try {
const decoded = jwt.verify(bearerToken, process.env.JWT_SECRET);
// Additional validation
if (decoded.exp < Date.now() / 1000) {
return undefined; // Token expired
}
return {
valid: true,
user: {
id: decoded.userId,
email: decoded.email,
roles: decoded.roles,
},
scopes: decoded.scopes || [],
};
} catch (error) {
return undefined; // Invalid token
}
}
})Database Token Verification
withOAuth({
issuer: 'https://auth.example.com',
verifyToken: async ({ bearerToken, context }) => {
if (!bearerToken) {
return undefined;
}
// Look up token in database
const token = await context.context.db.accessToken.findUnique({
where: { token: bearerToken },
include: { user: true }
});
if (!token || token.expiresAt < new Date()) {
return undefined; // Token not found or expired
}
return {
valid: true,
user: token.user,
scopes: token.scopes,
};
}
})OAuth Metadata Endpoint
The adapter automatically exposes an OAuth protected resource metadata endpoint. You need to create a route for it:
Next.js Route:
// src/app/.well-known/oauth-protected-resource/route.ts
import { auth } from '@/app/api/mcp/[...transport]/route';
export const GET = auth.resource;
export const OPTIONS = auth.cors;Express Route:
// Express example
app.get('/.well-known/oauth-protected-resource', auth.resource);
app.options('/.well-known/oauth-protected-resource', auth.cors);Protected Routes
Once OAuth is configured, all MCP requests require a valid Bearer token:
Request Headers:
Authorization: Bearer <your-token>Unprotected Request:
GET /api/mcp/sse
// Returns 401 UnauthorizedProtected Request:
GET /api/mcp/sse
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
// Returns 200 OK with MCP responseAccessing User Context
In your tool handlers, you can access the authenticated user from the context:
.addTool({
name: 'getMyProfile',
description: 'Get my user profile',
args: {},
handler: async (args, context) => {
// User is available in context if OAuth is enabled
// Note: This requires additional setup to pass user from OAuth to context
const user = context.context.user;
return {
content: [{
type: 'text',
text: JSON.stringify(user, null, 2)
}]
};
},
})Complete Example
Here's a complete example with OAuth and user context:
import { IgniterMcpServer } from '@igniter-js/adapter-mcp-server';
import jwt from 'jsonwebtoken';
const { handler, auth } = IgniterMcpServer
.create()
.router(AppRouter)
.withOAuth({
issuer: 'https://auth.example.com',
resourceMetadataPath: '/.well-known/oauth-protected-resource',
scopes: ['mcp:read', 'mcp:write'],
verifyToken: async ({ bearerToken, context }) => {
if (!bearerToken) {
return undefined;
}
try {
const decoded = jwt.verify(bearerToken, process.env.JWT_SECRET) as any;
// You can inject the user into the router context
// This requires modifying createMcpContext to include the user
return {
valid: true,
user: {
id: decoded.userId,
email: decoded.email,
},
scopes: decoded.scopes || ['mcp:read'],
};
} catch (error) {
return undefined;
}
}
})
.build();
export const GET = handler;
export const POST = handler;
export const OPTIONS = auth.cors;Error Responses
When authentication fails, the adapter returns proper OAuth error responses:
401 Unauthorized:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp-server", error="invalid_token"403 Forbidden (insufficient scopes):
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer realm="mcp-server", error="insufficient_scope", scope="mcp:write"Best Practices
1. Always Validate Tokens
Never trust tokens without verification:
// ✅ Good
verifyToken: async ({ bearerToken }) => {
if (!bearerToken) return undefined;
return await verifyJWT(bearerToken);
}
// ❌ Bad
verifyToken: async ({ bearerToken }) => {
return { valid: true }; // No validation!
}2. Check Token Expiration
Always verify token expiration:
verifyToken: async ({ bearerToken }) => {
const decoded = await verifyJWT(bearerToken);
if (decoded.exp < Date.now() / 1000) {
return undefined; // Expired
}
return { valid: true, user: decoded.user };
}3. Validate Scopes
Check required scopes for sensitive operations:
verifyToken: async ({ bearerToken }) => {
const decoded = await verifyJWT(bearerToken);
const hasWriteScope = decoded.scopes?.includes('mcp:write');
return {
valid: true,
user: decoded.user,
scopes: decoded.scopes,
};
}4. Use Environment Variables
Store sensitive configuration in environment variables:
withOAuth({
issuer: process.env.OAUTH_ISSUER,
verifyToken: async ({ bearerToken }) => {
return await verifyJWT(bearerToken, process.env.JWT_SECRET);
}
})Troubleshooting
Token Not Validated
If tokens aren't being validated:
- Check that
verifyTokenis properly implemented - Verify the Bearer token is being extracted correctly
- Check token expiration and signature
CORS Issues
If you encounter CORS issues:
- Export the
auth.corshandler asOPTIONS - Ensure CORS headers are properly set
- Check browser console for CORS errors
401 Responses
If you're getting 401 responses:
- Verify token format:
Bearer <token> - Check token expiration
- Verify token signature matches your secret
- Check
verifyTokenimplementation
Next Steps
- Learn about Event Handlers for monitoring
- Explore Advanced Configurations
- Check out Best Practices