Skip to content

OAuth 2.1 Authentication

Jeremy edited this page Nov 16, 2025 · 1 revision

OAuth 2.1 Authentication

OAuth 2.1 authentication support for fine-grained access control and scope-based permissions.


Table of Contents


Overview

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.

Key Features

  • 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

Benefits

  • 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

Quick Start

Step 1: Enable OAuth 2.1 Mode

Set the authentication type to OAuth 2.1:

# In .env file
AUTH_TYPE=oauth2.1

# Or via environment variable
export AUTH_TYPE=oauth2.1

Step 2: Create Configuration Files

# Copy example files
cp config/oauth-clients.example.json config/oauth-clients.json
cp config/oauth-secrets.example.json config/oauth-secrets.json

Step 3: Configure Your Clients

Edit 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
  }
}

Step 4: Start Server

# Development mode
npm run oauth:dev

# Production mode
npm run oauth:start

You should see:

[INFO] OAuth clients configuration loaded
       total_clients: 1
       allow_dynamic_registration: false

OAuth Scopes

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

Scope Hierarchy

  • mcp: Grants access to ALL APIs (superset of all other scopes)
  • mcp:*: Individual API-level scopes

Scope Configuration Examples

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).

How Scopes Work

Authorization Flow:

  1. Client requests specific scopes during authorization (e.g., scope=mcp:bug mcp:psirt)
  2. Server validates against client's allowed scopes in oauth-clients.json
  3. Server issues access token with approved scopes
  4. 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:psirt scopes → 22 tools available
  • Client with mcp scope → All 60+ tools available

Configuration Files

Primary Config: config/oauth-clients.json

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
  }
}

Secrets Config: config/oauth-secrets.json (Optional)

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

Client Types

Public Clients (PKCE-only)

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.

Confidential Clients (with secret)

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"
  }
}

Setup Instructions

Step 1: Copy Example Files

cp config/oauth-clients.example.json config/oauth-clients.json
cp config/oauth-secrets.example.json config/oauth-secrets.json

Step 2: Configure Clients

Edit 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
  }
}

Step 3: Add Secrets (if needed)

Edit config/oauth-secrets.json:

{
  "secrets": {
    "your_app_prod": "generate_a_secure_random_secret_here"
  }
}

Step 4: Configure Environment

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

Step 5: Start Server

# Development mode with auto-reload
npm run oauth:dev

# Production mode
npm run oauth:start

Server 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

Docker Deployment

Option 1: OAuth 2.1 with Pre-configured Clients

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 --http

Option 2: Docker Compose

Create 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: 3

Start with:

docker-compose up -d

Hot Reload in Docker

The 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!


Security Best Practices

1. Never Commit Secrets

Add to .gitignore:

config/oauth-clients.json
config/oauth-secrets.json

Use .example.json files for templates.

2. Use Separate Secrets File

  • Keep main config in version control
  • Keep secrets file out of version control
  • Use read-only volume mounts in Docker (:ro flag)

3. Disable Dynamic Registration in Production

{
  "settings": {
    "allow_dynamic_registration": false
  }
}

4. Use HTTPS in Production

  • All redirect_uris should use https://
  • Only allow http://localhost for development

5. Rotate Secrets Periodically

  • Update oauth-secrets.json
  • Server hot-reloads automatically
  • No downtime needed

6. Principle of Least Privilege

  • Only grant scopes that the application needs
  • Use specific scopes (mcp:bug) instead of mcp when possible
  • Review client permissions regularly

7. Monitor Access Logs

# Watch for authentication issues
tail -f logs/server.log | grep oauth

# Monitor scope validation
tail -f logs/server.log | grep "Scope validation"

Troubleshooting

Client Not Found

Error: "Client not found" during authorization

Solutions:

  1. Check client_id matches exactly:

    {
      "client_id": "mcp_inspector_dev"  // Must match exactly
    }
  2. Check client is enabled:

    {
      "enabled": true  // Not false
    }
  3. Check server logs:

    npm run oauth:dev
    # Look for "Loaded pre-configured OAuth client"

Invalid Scope Error

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.

Dynamic Registration Disabled

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.

Redirect URI Mismatch

Error: "Redirect URI mismatch" during authorization

Solutions:

  1. Ensure exact match:

    {
      "redirect_uris": [
        "http://localhost:6274/oauth/callback"  // Must match exactly
      ]
    }
  2. For localhost with varying ports:

    {
      "redirect_uris": [
        "http://localhost:*/oauth/callback"  // Wildcard port
      ]
    }

Secret Not Loading

Error: Confidential client behaves like public client

Solutions:

  1. Check file exists:

    ls config/oauth-secrets.json
  2. Validate JSON syntax:

    cat config/oauth-secrets.json | jq
  3. Check client_id matches:

    {
      "secrets": {
        "exact_client_id_here": "secret_value"
      }
    }
  4. Check server logs:

    [INFO] Loaded pre-configured OAuth client
           secret_source: secrets_file  // Should say this
    

OAuth Endpoints

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 | jq

Example Configurations

Development Setup (Mixed)

Allow 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
  }
}

Production Setup (Locked Down)

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).

Multi-Tenant Setup

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"
    }
  ]
}

Configuration Settings Reference

allow_dynamic_registration

  • Type: boolean
  • Default: true
  • Description: Allow clients to register at runtime via /register endpoint
  • Recommended: Set to false in production

require_client_secret

  • Type: boolean
  • Default: false
  • Description: Require all clients to have a secret (disables public clients)
  • Recommended: Keep as false to support both public and confidential clients

token_expiry_seconds

  • Type: integer
  • Default: 3600 (1 hour)
  • Description: How long access tokens remain valid

refresh_token_expiry_seconds

  • Type: integer
  • Default: 86400 (24 hours)
  • Description: How long refresh tokens remain valid

Next Steps


Last Updated: November 15, 2025 | Version: 1.18.0

Clone this wiki locally