-
Notifications
You must be signed in to change notification settings - Fork 12
OAuth 2.1 Authentication
OAuth 2.1 authentication support for fine-grained access control and scope-based permissions.
- Overview
- Quick Start
- OAuth Scopes
- Configuration Files
- Client Types
- Setup Instructions
- Docker Deployment
- Security Best Practices
- Troubleshooting
- Example Configurations
The Cisco Support MCP Server includes a complete OAuth 2.1 authorization server with scope-based access control, allowing you to control exactly which APIs and tools each OAuth client can access.
- Scope-Based Access Control: Fine-grained permissions per OAuth client
- OAuth 2.1 Compliance: Modern security with PKCE support
- Public & Confidential Clients: Support for both client types
- Hot-Reload Configuration: Update clients without restarting server
- Per-Request Tool Filtering: Dynamic tool lists based on token scopes
- Security Hardened: No information disclosure in error messages
- Security: Control exactly which applications can access your server
- Version Control: Track authorized clients in git (without secrets)
- Flexible Deployment: Separate secrets from main config
- Multi-Tenant Support: Different scopes for different use cases
Set the authentication type to OAuth 2.1:
# In .env file
AUTH_TYPE=oauth2.1
# Or via environment variable
export AUTH_TYPE=oauth2.1# Copy example files
cp config/oauth-clients.example.json config/oauth-clients.json
cp config/oauth-secrets.example.json config/oauth-secrets.jsonEdit config/oauth-clients.json:
{
"clients": [
{
"client_id": "my_mcp_client",
"client_uri": "http://localhost:6274",
"redirect_uris": ["http://localhost:6274/oauth/callback"],
"scopes": ["mcp:bug", "mcp:psirt"],
"grant_types": ["authorization_code"],
"description": "My MCP Client - Bug and Security APIs only",
"enabled": true
}
],
"settings": {
"allow_dynamic_registration": false,
"token_expiry_seconds": 3600
}
}# Development mode
npm run oauth:dev
# Production mode
npm run oauth:startYou should see:
[INFO] OAuth clients configuration loaded
total_clients: 1
allow_dynamic_registration: false
OAuth scopes provide fine-grained control over API access. Each scope maps to a specific Cisco Support API:
| Scope | API Access | Tools | Description |
|---|---|---|---|
mcp |
All APIs | 60+ | Full access to all MCP tools and APIs (default) |
mcp:bug |
Bug Search API | 14 | Bug search, details, and product-specific queries |
mcp:case |
Case Management API | 4 | Support case operations and management |
mcp:eox |
End-of-Life API | 4 | Product lifecycle and EoL information |
mcp:psirt |
Security Advisory API | 8 | Security vulnerabilities and advisories |
mcp:product |
Product Information API | 6 | Product details and specifications |
mcp:software |
Software Suggestions API | 6 | Software recommendations and upgrades |
mcp:serial |
Serial Number API | 3 | Serial number lookup and warranty info |
mcp:rma |
RMA API | 3 | Return authorization tracking |
-
mcp: Grants access to ALL APIs (superset of all other scopes) -
mcp:*: Individual API-level scopes
Full access (default):
{
"scopes": ["mcp"]
}Specific API access:
{
"scopes": ["mcp:bug", "mcp:psirt"] // Only Bug and Security APIs
}Multiple APIs:
{
"scopes": ["mcp:case", "mcp:rma", "mcp:eox"] // Support operations focus
}Security Best Practice: Only grant scopes that the application actually needs (principle of least privilege).
Authorization Flow:
- Client requests specific scopes during authorization (e.g.,
scope=mcp:bug mcp:psirt) - Server validates against client's allowed scopes in
oauth-clients.json - Server issues access token with approved scopes
- Each API request validates token and filters tools based on scopes
Token Validation:
// OAuth middleware attaches scopes to each request:
req.oauth_scopes = ['mcp:bug', 'mcp:psirt'] // From access token
req.oauth_client_id = 'bug_search_app'Tool Filtering:
- Client with
mcp:bug,mcp:psirtscopes → 22 tools available - Client with
mcpscope → All 60+ tools available
Contains client metadata (can be committed to git):
{
"clients": [
{
"client_id": "mcp_inspector_prod",
"client_uri": "https://modelcontextprotocol.io",
"redirect_uris": [
"https://inspector.example.com/oauth/callback"
],
"scopes": ["mcp"],
"grant_types": ["authorization_code", "refresh_token"],
"description": "MCP Inspector - Production",
"enabled": true
}
],
"settings": {
"allow_dynamic_registration": false,
"require_client_secret": false,
"token_expiry_seconds": 3600,
"refresh_token_expiry_seconds": 86400
}
}Contains client secrets (should be gitignored):
{
"secrets": {
"mcp_inspector_prod": "your_production_secret_here_change_me"
}
}Important: Add to .gitignore:
config/oauth-clients.json
config/oauth-secrets.json
For clients that cannot securely store secrets (desktop apps, mobile apps, web apps):
{
"client_id": "mcp_inspector_dev",
"client_uri": "http://localhost:6274",
"redirect_uris": ["http://localhost:6274/oauth/callback"],
"scopes": ["mcp"],
"grant_types": ["authorization_code"],
"description": "Public client - no secret needed, PKCE required"
}No secret needed - PKCE provides security.
For clients that can securely store secrets (backend services):
// In oauth-clients.json
{
"client_id": "backend_service",
"client_uri": "https://api.company.com",
"redirect_uris": ["https://api.company.com/oauth/callback"],
"scopes": ["mcp"],
"grant_types": ["authorization_code", "client_credentials"],
"description": "Backend service with secret"
}
// In oauth-secrets.json
{
"secrets": {
"backend_service": "super_secret_value_keep_this_safe"
}
}cp config/oauth-clients.example.json config/oauth-clients.json
cp config/oauth-secrets.example.json config/oauth-secrets.jsonEdit config/oauth-clients.json:
{
"clients": [
{
"client_id": "your_app_prod",
"client_uri": "https://your-app.com",
"redirect_uris": ["https://your-app.com/oauth/callback"],
"scopes": ["mcp"],
"grant_types": ["authorization_code"],
"description": "Your production application",
"enabled": true
}
],
"settings": {
"allow_dynamic_registration": false
}
}Edit config/oauth-secrets.json:
{
"secrets": {
"your_app_prod": "generate_a_secure_random_secret_here"
}
}In .env:
# OAuth 2.1 mode
AUTH_TYPE=oauth2.1
# Optional: Custom paths
OAUTH_CLIENTS_CONFIG=config/oauth-clients.json
OAUTH_SECRETS_CONFIG=config/oauth-secrets.json
# Optional: Issuer URL
OAUTH2_ISSUER_URL=https://your-server.com# Development mode with auto-reload
npm run oauth:dev
# Production mode
npm run oauth:startServer will log loaded clients:
[INFO] OAuth client secrets loaded
secretsPath: config/oauth-secrets.json
client_count: 1
[INFO] Loaded pre-configured OAuth client
client_id: your_app_prod
client_uri: https://your-app.com
has_secret: true
secret_source: secrets_file
description: Your production application
[INFO] OAuth clients configuration loaded
total_clients: 1
allow_dynamic_registration: false
Recommended for production: Mount OAuth config directory with pre-configured clients.
# Create oauth-config directory with your configs
mkdir oauth-config
cp config/oauth-clients.json oauth-config/
cp config/oauth-secrets.json oauth-config/
# Run container with OAuth config
docker run -d \
--name mcp-cisco-oauth \
-p 3000:3000 \
-v $(pwd)/oauth-config:/oauth-config:ro \
-e AUTH_TYPE=oauth2.1 \
-e OAUTH_CLIENTS_CONFIG=/oauth-config/oauth-clients.json \
-e OAUTH_SECRETS_CONFIG=/oauth-config/oauth-secrets.json \
-e CISCO_CLIENT_ID=your_cisco_client_id \
-e CISCO_CLIENT_SECRET=your_cisco_secret \
-e SUPPORT_API=all \
ghcr.io/sieteunoseis/mcp-cisco-support:latest --httpCreate docker-compose.yml:
version: '3.8'
services:
mcp-server:
image: ghcr.io/sieteunoseis/mcp-cisco-support:latest
container_name: mcp-cisco-oauth
ports:
- "3000:3000"
environment:
- AUTH_TYPE=oauth2.1
- CISCO_CLIENT_ID=${CISCO_CLIENT_ID}
- CISCO_CLIENT_SECRET=${CISCO_CLIENT_SECRET}
- SUPPORT_API=all
- OAUTH_CLIENTS_CONFIG=/oauth-config/oauth-clients.json
- OAUTH_SECRETS_CONFIG=/oauth-config/oauth-secrets.json
- OAUTH2_ISSUER_URL=https://your-server.com
volumes:
- ./oauth-config:/oauth-config:ro
- ./logs:/usr/src/app/logs
command: ["--http"]
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3Start with:
docker-compose up -dThe server automatically watches config files:
# Edit config files on host
vim oauth-config/oauth-clients.json
# Container automatically reloads
docker logs mcp-cisco-oauth
# [INFO] OAuth clients config file changed, reloading...No container restart needed!
Add to .gitignore:
config/oauth-clients.json
config/oauth-secrets.json
Use .example.json files for templates.
- Keep main config in version control
- Keep secrets file out of version control
- Use read-only volume mounts in Docker (
:roflag)
{
"settings": {
"allow_dynamic_registration": false
}
}- All
redirect_urisshould usehttps:// - Only allow
http://localhostfor development
- Update
oauth-secrets.json - Server hot-reloads automatically
- No downtime needed
- Only grant scopes that the application needs
- Use specific scopes (
mcp:bug) instead ofmcpwhen possible - Review client permissions regularly
# Watch for authentication issues
tail -f logs/server.log | grep oauth
# Monitor scope validation
tail -f logs/server.log | grep "Scope validation"Error: "Client not found" during authorization
Solutions:
-
Check client_id matches exactly:
{ "client_id": "mcp_inspector_dev" // Must match exactly } -
Check client is enabled:
{ "enabled": true // Not false } -
Check server logs:
npm run oauth:dev # Look for "Loaded pre-configured OAuth client"
Error: invalid_scope - Requested scope is not authorized
Cause: Client requested scopes that aren't in their allowed list
Solution: Update oauth-clients.json:
{
"client_id": "my_client",
"scopes": ["mcp:bug", "mcp:psirt"] // Add requested scopes here
}Security Note: The error message intentionally doesn't reveal allowed scopes to prevent information disclosure.
Error: access_denied when trying to register
Cause: Dynamic registration is disabled
Solution:
{
"settings": {
"allow_dynamic_registration": true // Enable for development
}
}Or add client to oauth-clients.json instead.
Error: "Redirect URI mismatch" during authorization
Solutions:
-
Ensure exact match:
{ "redirect_uris": [ "http://localhost:6274/oauth/callback" // Must match exactly ] } -
For localhost with varying ports:
{ "redirect_uris": [ "http://localhost:*/oauth/callback" // Wildcard port ] }
Error: Confidential client behaves like public client
Solutions:
-
Check file exists:
ls config/oauth-secrets.json
-
Validate JSON syntax:
cat config/oauth-secrets.json | jq -
Check client_id matches:
{ "secrets": { "exact_client_id_here": "secret_value" } } -
Check server logs:
[INFO] Loaded pre-configured OAuth client secret_source: secrets_file // Should say this
Test OAuth metadata:
# Authorization server metadata
curl http://localhost:3000/.well-known/oauth-authorization-server | jq
# Protected resource metadata
curl http://localhost:3000/.well-known/oauth-protected-resource/mcp | jqAllow both pre-configured and dynamic clients:
{
"clients": [
{
"client_id": "mcp_inspector_dev",
"client_uri": "http://localhost:6274",
"redirect_uris": ["http://localhost:6274/oauth/callback"],
"scopes": ["mcp"],
"grant_types": ["authorization_code"],
"description": "Local development",
"enabled": true
}
],
"settings": {
"allow_dynamic_registration": true, // Allow others to register
"require_client_secret": false
}
}Only pre-configured clients with scope-based access control:
{
"clients": [
{
"client_id": "production_web_app",
"client_uri": "https://app.company.com",
"redirect_uris": ["https://app.company.com/oauth/callback"],
"scopes": ["mcp"],
"grant_types": ["authorization_code", "refresh_token"],
"description": "Production web application - Full API access",
"enabled": true
},
{
"client_id": "mobile_app",
"client_uri": "https://company.com",
"redirect_uris": ["com.company.app://oauth/callback"],
"scopes": ["mcp:bug", "mcp:psirt", "mcp:case"],
"grant_types": ["authorization_code"],
"description": "Mobile app - Limited to bug search, security, and case management",
"enabled": true
},
{
"client_id": "security_scanner",
"client_uri": "https://security.company.com",
"redirect_uris": ["https://security.company.com/oauth/callback"],
"scopes": ["mcp:psirt", "mcp:eox"],
"grant_types": ["authorization_code", "refresh_token"],
"description": "Security scanning tool - Only security and lifecycle data",
"enabled": true
}
],
"settings": {
"allow_dynamic_registration": false, // Only pre-configured clients
"require_client_secret": false, // Allow public clients (mobile)
"token_expiry_seconds": 1800, // 30 minutes
"refresh_token_expiry_seconds": 604800 // 7 days
}
}With oauth-secrets.json:
{
"secrets": {
"production_web_app": "highly_secure_random_string_here"
}
}Note: mobile_app has no secret (public client with PKCE).
Different scopes for different use cases:
{
"clients": [
{
"client_id": "support_team",
"scopes": ["mcp:bug", "mcp:case", "mcp:rma"],
"description": "Support team - Bug tracking, cases, and RMA"
},
{
"client_id": "security_team",
"scopes": ["mcp:psirt", "mcp:eox"],
"description": "Security team - Vulnerabilities and lifecycle"
},
{
"client_id": "engineering_team",
"scopes": ["mcp:bug", "mcp:product", "mcp:software"],
"description": "Engineering - Bugs, products, and software"
},
{
"client_id": "admin",
"scopes": ["mcp"],
"description": "Administrator - Full access"
}
]
}- Type: boolean
- Default: true
-
Description: Allow clients to register at runtime via
/registerendpoint -
Recommended: Set to
falsein production
- Type: boolean
- Default: false
- Description: Require all clients to have a secret (disables public clients)
-
Recommended: Keep as
falseto support both public and confidential clients
- Type: integer
- Default: 3600 (1 hour)
- Description: How long access tokens remain valid
- Type: integer
- Default: 86400 (24 hours)
- Description: How long refresh tokens remain valid
- Security Guide - Complete security best practices
- Docker Deployment - Container deployment instructions
- Troubleshooting Guide - Common issues and solutions
- Advanced Configuration - Environment variables and fine-tuning
Last Updated: November 15, 2025 | Version: 1.18.0