Skip to content

OAuth Proxy Integration Guide for Chatbot Developers

This is an example project for integrating a basic chatbot with the AI Security Gateway. This document only serves as example code to help you build out your solution and integrated with the AI Security Gateway proxies!

Chatbot Table of Contents

  1. Introduction
  2. Architecture Overview
  3. Gateway Configuration
  4. Client Integration Guide
  5. Code Examples
  6. Token Lifecycle Management
  7. Security Considerations
  8. Troubleshooting
  9. Best Practices

Chatbot Integration Introduction

The Problem

Modern chatbots and AI applications often need to integrate with external services that require OAuth authentication (e.g., Atlassian Jira, GitHub, Google Workspace). This creates significant complexity:

  • Token Management: Your application must store, refresh, and manage OAuth tokens for each user
  • User Mapping: Complex logic to map OAuth tokens to your application's user sessions
  • Token Refresh: Implementing refresh token logic with error handling and retry mechanisms
  • Security: Securely storing sensitive tokens and handling token expiration gracefully
  • Maintenance: Ongoing code to handle edge cases, failures, and provider-specific quirks

The Solution

The AI Security Gateway OAuth Proxy eliminates this complexity by acting as an OAuth intermediary. Your chatbot only needs to:

  1. Register once as an OAuth client with the Gateway
  2. Implement a simple OAuth flow to get Gateway-issued JWTs
  3. Store Gateway JWTs (simple strings, no refresh logic needed)
  4. Include JWTs in requests to the Gateway proxy

The Gateway handles all upstream OAuth complexity:

  • ✅ Manages upstream provider tokens (encrypted, secure)
  • ✅ Automatically refreshes expired tokens
  • ✅ Maps tokens to users internally
  • ✅ Injects tokens into requests to MCP servers
  • ✅ Provides centralized security and audit logging

Target Audience

This guide is for:

  • Chatbot developers integrating with OAuth-protected MCP servers
  • Application developers building AI assistants that need external service access
  • DevOps engineers setting up Gateway infrastructure
  • Security teams evaluating OAuth token management solutions

Chatbot Integration Prerequisites

  • AI Security Gateway (latest) installed and running
  • Admin access to Gateway web interface
  • OAuth provider credentials (e.g., Okta, Google, GitHub)
  • Basic understanding of OAuth 2.1 flows
  • Your chatbot/application codebase (JavaScript, Python, or similar)

Chatbot Architecture Overview

High-Level Architecture

Component Responsibilities

ComponentResponsibility
Your ChatbotInitiates OAuth flow, stores Gateway JWTs, makes requests with JWTs
OAuth Proxy ServiceHandles OAuth flows, issues Gateway JWTs, manages client registrations
Token ManagerStores/retrieves encrypted upstream tokens, handles token refresh
MCP ProxyValidates Gateway JWTs, injects upstream tokens, proxies requests
OAuth ProviderAuthenticates users, issues upstream tokens (Okta, Google, etc.)
MCP ServerReceives requests with upstream tokens, returns data

Token Flow Sequence

Benefits Summary

BenefitDescription
Simplified CodeNo token refresh logic, no token-to-user mapping, no expiration handling
Automatic RefreshGateway handles all token refresh automatically and transparently
Centralized SecurityAll tokens encrypted at rest, centralized audit logging, security policies
Reduced Complexity~90% reduction in OAuth-related code in your application
Better SecurityTokens never exposed to your application, encrypted storage, PKCE enforcement
Easier MaintenanceGateway handles provider-specific quirks and edge cases

Gateway Configuration

Chatbot Integration Overview

Important: No code changes are required in the Gateway. All configuration is done through the web admin interface or API. The Gateway already supports OAuth Proxy functionality.

Step 1: Configure OAuth Provider

Location: Gateway Admin UI → OAuth Providers → Add Provider

Configure your OAuth provider (e.g., Okta, Google, GitHub) that the MCP server uses:

yaml
Provider Type: okta  # or google, github, azure_ad, generic
Client ID: your-okta-client-id
Client Secret: your-okta-client-secret
Authorization URL: https://your-okta-domain.okta.com/oauth2/v1/authorize
Token URL: https://your-okta-domain.okta.com/oauth2/v1/token
Scopes: read:jira-work write:jira-work  # MCP server required scopes
Redirect URI: https://gateway.yourcompany.com/api/v1/oauth/callback

Provider-Specific Notes:

  • Okta: Use your Okta domain and application credentials
  • Google: Use Google Cloud Console OAuth 2.0 credentials
  • GitHub: Use GitHub OAuth App credentials
  • Azure AD: Use Azure App Registration credentials

Step 2: Create MCP Proxy with OAuth Proxy

Location: Gateway Admin UI → Proxies → Create Proxy

Configuration:

json
{
  "name": "Atlassian Jira MCP Proxy",
  "type": "mcp",
  "target": "https://your-company.atlassian.net",
  "transport": "http",
  "port": 9001,
  "enabled": true,
  "config": {
    "oauth_proxy_enabled": true,
    "oauth_proxy_mode": "gateway",
    "oauth_proxy_provider_id": 1,
    "oauth_proxy_allowed_redirects": [
      "https://your-chatbot.com/oauth/callback",
      "http://localhost:3000/oauth/callback"
    ],
    "oauth_proxy_require_consent": false
  }
}

Key Settings Explained:

SettingValueDescription
oauth_proxy_enabledtrueEnables OAuth Proxy functionality
oauth_proxy_mode"gateway"Gateway manages tokens (recommended)
oauth_proxy_provider_id1ID of OAuth provider from Step 1
oauth_proxy_allowed_redirects[...]Your chatbot's callback URLs
oauth_proxy_require_consentfalseOptional consent screen (set true for extra security)

Via API:

bash
curl -X POST https://gateway.yourcompany.com/api/v1/proxies/ \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Atlassian Jira MCP Proxy",
    "type": "mcp",
    "target": "https://your-company.atlassian.net",
    "transport": "http",
    "port": 9001,
    "config": {
      "oauth_proxy_enabled": true,
      "oauth_proxy_mode": "gateway",
      "oauth_proxy_provider_id": 1,
      "oauth_proxy_allowed_redirects": [
        "https://your-chatbot.com/oauth/callback"
      ]
    }
  }'

Step 3: Verify Configuration

Check OAuth Proxy Endpoints:

bash
# Get OAuth authorization server metadata
curl https://gateway.yourcompany.com/.well-known/oauth-authorization-server?proxy_id=1

# Response includes:
#  - authorization_endpoint
#  - token_endpoint
#  - registration_endpoint

Expected Response:

json
{
  "issuer": "https://gateway.yourcompany.com",
  "authorization_endpoint": "https://gateway.yourcompany.com/api/v1/oauth-proxy/1/authorize",
  "token_endpoint": "https://gateway.yourcompany.com/api/v1/oauth-proxy/1/token",
  "registration_endpoint": "https://gateway.yourcompany.com/api/v1/oauth-proxy/1/register",
  "code_challenge_methods_supported": ["S256", "plain"]
}

Client Integration Guide

Step 1: Dynamic Client Registration (DCR)

Your chatbot registers once as an OAuth client with the Gateway using RFC 7591 Dynamic Client Registration.

Endpoint: POST /api/v1/oauth-proxy/{proxy_id}/register

Request:

http
POST https://gateway.yourcompany.com/api/v1/oauth-proxy/1/register
Content-Type: application/json

{
  "redirect_uris": [
    "https://your-chatbot.com/oauth/callback",
    "http://localhost:3000/oauth/callback"
  ],
  "client_name": "Your Chatbot Name",
  "client_uri": "https://your-chatbot.com",
  "logo_uri": "https://your-chatbot.com/logo.png",
  "token_endpoint_auth_method": "client_secret_post",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "scope": "read write"
}

Response:

json
{
  "client_id": "gateway_ac3f8d2e91b7456c",
  "client_secret": "gw_secret_9f8e7d6c5b4a3210fedcba9876543210",
  "client_id_issued_at": 1704614400,
  "client_secret_expires_at": 0,
  "redirect_uris": [
    "https://your-chatbot.com/oauth/callback"
  ],
  "token_endpoint_auth_method": "client_secret_post",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"]
}

Store Securely:

  • client_id: Can be stored in application config
  • client_secret: Store encrypted (environment variable, secrets manager)

Step 2: OAuth Authorization Flow

When a user wants to enable the integration, redirect them to the Gateway's authorization endpoint.

Generate PKCE Parameters (Required for Security):

javascript
// Generate code verifier (43-128 characters)
function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64URLEncode(array);
}

// Generate code challenge (S256 method)
async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  return base64URLEncode(new Uint8Array(digest));
}

// Base64 URL encoding (no padding, URL-safe)
function base64URLEncode(buffer) {
  return btoa(String.fromCharCode(...buffer))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

Build Authorization URL:

javascript
async function initiateOAuthFlow(userId) {
  // Generate PKCE parameters
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = await generateCodeChallenge(codeVerifier);
  const state = generateRandomState();
  
  // Store verifier and state in session (for token exchange)
  await storeOAuthState(userId, {
    codeVerifier,
    state
  });
  
  // Build authorization URL
  const authURL = new URL('https://gateway.yourcompany.com/api/v1/oauth-proxy/1/authorize');
  authURL.searchParams.set('response_type', 'code');
  authURL.searchParams.set('client_id', CLIENT_ID);
  authURL.searchParams.set('redirect_uri', 'https://your-chatbot.com/oauth/callback');
  authURL.searchParams.set('state', state);
  authURL.searchParams.set('code_challenge', codeChallenge);
  authURL.searchParams.set('code_challenge_method', 'S256');
  authURL.searchParams.set('scope', 'read write');
  
  // Redirect user to Gateway
  return authURL.toString();
}

Handle Callback:

javascript
async function handleOAuthCallback(code, state, userId) {
  // Verify state (CSRF protection)
  const storedState = await getOAuthState(userId);
  if (storedState.state !== state) {
    throw new Error('Invalid state parameter');
  }
  
  // Exchange authorization code for tokens
  const response = await fetch('https://gateway.yourcompany.com/api/v1/oauth-proxy/1/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code,
      redirect_uri: 'https://your-chatbot.com/oauth/callback',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      code_verifier: storedState.codeVerifier
    })
  });
  
  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Token exchange failed: ${error.error_description}`);
  }
  
  const tokens = await response.json();
  
  // Store Gateway JWT (simple string, no refresh logic needed!)
  await storeUserToken(userId, {
    access_token: tokens.access_token,  // Gateway JWT
    refresh_token: tokens.refresh_token, // Optional: for Gateway JWT refresh
    expires_at: Date.now() + (tokens.expires_in * 1000)
  });
  
  // Clean up OAuth state
  await clearOAuthState(userId);
  
  return tokens;
}

Step 3: Making Requests to MCP Server

Once you have a Gateway JWT, include it in all requests to the Gateway proxy:

javascript
async function callMCPServer(method, params, userId) {
  // Get Gateway JWT for user
  const tokenData = await getUserToken(userId);
  
  if (!tokenData || isTokenExpired(tokenData)) {
    // If Gateway JWT expired, refresh it (optional - Gateway handles upstream refresh)
    if (tokenData?.refresh_token) {
      const newTokens = await refreshGatewayToken(tokenData.refresh_token);
      await storeUserToken(userId, newTokens);
      tokenData.access_token = newTokens.access_token;
    } else {
      // Re-initiate OAuth flow
      throw new Error('Token expired, please re-authenticate');
    }
  }
  
  // Make request to Gateway proxy with Gateway JWT
  const response = await fetch('https://gateway.yourcompany.com:9001/mcp', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${tokenData.access_token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      jsonrpc: '2.0',
      method: method,
      params: params,
      id: generateRequestID()
    })
  });
  
  if (!response.ok) {
    if (response.status === 401) {
      // Token invalid, re-authenticate
      throw new Error('Authentication required');
    }
    throw new Error(`Request failed: ${response.statusText}`);
  }
  
  return response.json();
}

Gateway JWT Refresh (Optional):

javascript
async function refreshGatewayToken(refreshToken) {
  const response = await fetch('https://gateway.yourcompany.com/api/v1/oauth-proxy/1/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET
    })
  });
  
  if (!response.ok) {
    throw new Error('Token refresh failed');
  }
  
  return response.json();
}

Code Examples

Complete JavaScript/TypeScript Implementation

typescript
// oauth-proxy-client.ts

interface TokenData {
  access_token: string;
  refresh_token?: string;
  expires_at: number;
}

interface OAuthState {
  codeVerifier: string;
  state: string;
}

class OAuthProxyClient {
  private gatewayBaseURL: string;
  private proxyID: number;
  private clientID: string;
  private clientSecret: string;
  private redirectURI: string;

  constructor(config: {
    gatewayBaseURL: string;
    proxyID: number;
    clientID: string;
    clientSecret: string;
    redirectURI: string;
  }) {
    this.gatewayBaseURL = config.gatewayBaseURL;
    this.proxyID = config.proxyID;
    this.clientID = config.clientID;
    this.clientSecret = config.clientSecret;
    this.redirectURI = config.redirectURI;
  }

  // Generate PKCE code verifier
  private generateCodeVerifier(): string {
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    return this.base64URLEncode(array);
  }

  // Generate PKCE code challenge
  private async generateCodeChallenge(verifier: string): Promise<string> {
    const encoder = new TextEncoder();
    const data = encoder.encode(verifier);
    const digest = await crypto.subtle.digest('SHA-256', data);
    return this.base64URLEncode(new Uint8Array(digest));
  }

  // Base64 URL encoding
  private base64URLEncode(buffer: Uint8Array): string {
    return btoa(String.fromCharCode(...buffer))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=/g, '');
  }

  // Generate random state
  private generateState(): string {
    const array = new Uint8Array(16);
    crypto.getRandomValues(array);
    return this.base64URLEncode(array);
  }

  // Initiate OAuth flow
  async initiateOAuthFlow(userId: string): Promise<{ authURL: string; state: string }> {
    const codeVerifier = this.generateCodeVerifier();
    const codeChallenge = await this.generateCodeChallenge(codeVerifier);
    const state = this.generateState();

    // Store state and verifier (in session, database, or cache)
    await this.storeOAuthState(userId, { codeVerifier, state });

    // Build authorization URL
    const authURL = new URL(
      `${this.gatewayBaseURL}/api/v1/oauth-proxy/${this.proxyID}/authorize`
    );
    authURL.searchParams.set('response_type', 'code');
    authURL.searchParams.set('client_id', this.clientID);
    authURL.searchParams.set('redirect_uri', this.redirectURI);
    authURL.searchParams.set('state', state);
    authURL.searchParams.set('code_challenge', codeChallenge);
    authURL.searchParams.set('code_challenge_method', 'S256');
    authURL.searchParams.set('scope', 'read write');

    return { authURL: authURL.toString(), state };
  }

  // Handle OAuth callback
  async handleCallback(
    code: string,
    state: string,
    userId: string
  ): Promise<TokenData> {
    // Verify state
    const storedState = await this.getOAuthState(userId);
    if (!storedState || storedState.state !== state) {
      throw new Error('Invalid state parameter');
    }

    // Exchange code for tokens
    const response = await fetch(
      `${this.gatewayBaseURL}/api/v1/oauth-proxy/${this.proxyID}/token`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: new URLSearchParams({
          grant_type: 'authorization_code',
          code: code,
          redirect_uri: this.redirectURI,
          client_id: this.clientID,
          client_secret: this.clientSecret,
          code_verifier: storedState.codeVerifier,
        }),
      }
    );

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Token exchange failed: ${error.error_description}`);
    }

    const tokens = await response.json();

    // Store tokens
    const tokenData: TokenData = {
      access_token: tokens.access_token,
      refresh_token: tokens.refresh_token,
      expires_at: Date.now() + tokens.expires_in * 1000,
    };

    await this.storeUserToken(userId, tokenData);
    await this.clearOAuthState(userId);

    return tokenData;
  }

  // Refresh Gateway JWT (optional)
  async refreshToken(refreshToken: string): Promise<TokenData> {
    const response = await fetch(
      `${this.gatewayBaseURL}/api/v1/oauth-proxy/${this.proxyID}/token`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: new URLSearchParams({
          grant_type: 'refresh_token',
          refresh_token: refreshToken,
          client_id: this.clientID,
          client_secret: this.clientSecret,
        }),
      }
    );

    if (!response.ok) {
      throw new Error('Token refresh failed');
    }

    const tokens = await response.json();
    return {
      access_token: tokens.access_token,
      refresh_token: tokens.refresh_token,
      expires_at: Date.now() + tokens.expires_in * 1000,
    };
  }

  // Make MCP request
  async callMCP(method: string, params: any, userId: string): Promise<any> {
    let tokenData = await this.getUserToken(userId);

    if (!tokenData || this.isTokenExpired(tokenData)) {
      if (tokenData?.refresh_token) {
        tokenData = await this.refreshToken(tokenData.refresh_token);
        await this.storeUserToken(userId, tokenData);
      } else {
        throw new Error('Token expired, please re-authenticate');
      }
    }

    const response = await fetch(`https://gateway.yourcompany.com:9001/mcp`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${tokenData.access_token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        jsonrpc: '2.0',
        method: method,
        params: params,
        id: this.generateRequestID(),
      }),
    });

    if (!response.ok) {
      if (response.status === 401) {
        throw new Error('Authentication required');
      }
      throw new Error(`Request failed: ${response.statusText}`);
    }

    return response.json();
  }

  // Helper methods (implement based on your storage)
  private async storeOAuthState(userId: string, state: OAuthState): Promise<void> {
    // Store in session, database, or cache
    // Example: await redis.set(`oauth:${userId}`, JSON.stringify(state), 'EX', 600);
  }

  private async getOAuthState(userId: string): Promise<OAuthState | null> {
    // Retrieve from session, database, or cache
    // Example: const data = await redis.get(`oauth:${userId}`);
    return null;
  }

  private async clearOAuthState(userId: string): Promise<void> {
    // Delete from session, database, or cache
  }

  private async storeUserToken(userId: string, token: TokenData): Promise<void> {
    // Store in database, encrypted
  }

  private async getUserToken(userId: string): Promise<TokenData | null> {
    // Retrieve from database
    return null;
  }

  private isTokenExpired(token: TokenData): boolean {
    return Date.now() >= token.expires_at - 60000; // 1 minute buffer
  }

  private generateRequestID(): string {
    return Math.random().toString(36).substring(2, 15);
  }
}

// Usage example
const client = new OAuthProxyClient({
  gatewayBaseURL: 'https://gateway.yourcompany.com',
  proxyID: 1,
  clientID: 'gateway_ac3f8d2e91b7456c',
  clientSecret: 'gw_secret_...',
  redirectURI: 'https://your-chatbot.com/oauth/callback',
});

// Initiate OAuth flow
const { authURL } = await client.initiateOAuthFlow(userId);
// Redirect user to authURL

// Handle callback
const tokens = await client.handleCallback(code, state, userId);

// Make MCP request
const result = await client.callMCP('tools/list', {}, userId);

Python Implementation

python
# oauth_proxy_client.py

import secrets
import hashlib
import base64
import requests
from typing import Optional, Dict, Any
from urllib.parse import urlencode, urlparse, parse_qs

class OAuthProxyClient:
    def __init__(self, gateway_base_url: str, proxy_id: int, 
                 client_id: str, client_secret: str, redirect_uri: str):
        self.gateway_base_url = gateway_base_url
        self.proxy_id = proxy_id
        self.client_id = client_id
        self.client_secret = client_secret
        self.redirect_uri = redirect_uri

    def generate_code_verifier(self) -> str:
        """Generate PKCE code verifier (43-128 characters)"""
        random_bytes = secrets.token_bytes(32)
        return self.base64_url_encode(random_bytes)

    def generate_code_challenge(self, verifier: str) -> str:
        """Generate PKCE code challenge (S256 method)"""
        digest = hashlib.sha256(verifier.encode()).digest()
        return self.base64_url_encode(digest)

    def base64_url_encode(self, data: bytes) -> str:
        """Base64 URL encoding (no padding, URL-safe)"""
        return base64.urlsafe_b64encode(data).decode().rstrip('=')

    def generate_state(self) -> str:
        """Generate random state for CSRF protection"""
        random_bytes = secrets.token_bytes(16)
        return self.base64_url_encode(random_bytes)

    def initiate_oauth_flow(self, user_id: str) -> Dict[str, str]:
        """Initiate OAuth flow, return authorization URL and state"""
        code_verifier = self.generate_code_verifier()
        code_challenge = self.generate_code_challenge(code_verifier)
        state = self.generate_state()

        # Store state and verifier (implement based on your storage)
        self.store_oauth_state(user_id, {
            'code_verifier': code_verifier,
            'state': state
        })

        # Build authorization URL
        params = {
            'response_type': 'code',
            'client_id': self.client_id,
            'redirect_uri': self.redirect_uri,
            'state': state,
            'code_challenge': code_challenge,
            'code_challenge_method': 'S256',
            'scope': 'read write'
        }

        auth_url = f"{self.gateway_base_url}/api/v1/oauth-proxy/{self.proxy_id}/authorize?{urlencode(params)}"

        return {'auth_url': auth_url, 'state': state}

    def handle_callback(self, code: str, state: str, user_id: str) -> Dict[str, Any]:
        """Handle OAuth callback, exchange code for tokens"""
        # Verify state
        stored_state = self.get_oauth_state(user_id)
        if not stored_state or stored_state['state'] != state:
            raise ValueError('Invalid state parameter')

        # Exchange code for tokens
        token_url = f"{self.gateway_base_url}/api/v1/oauth-proxy/{self.proxy_id}/token"
        data = {
            'grant_type': 'authorization_code',
            'code': code,
            'redirect_uri': self.redirect_uri,
            'client_id': self.client_id,
            'client_secret': self.client_secret,
            'code_verifier': stored_state['code_verifier']
        }

        response = requests.post(token_url, data=data)
        response.raise_for_status()

        tokens = response.json()

        # Store tokens
        token_data = {
            'access_token': tokens['access_token'],
            'refresh_token': tokens.get('refresh_token'),
            'expires_at': int(time.time()) + tokens['expires_in']
        }

        self.store_user_token(user_id, token_data)
        self.clear_oauth_state(user_id)

        return token_data

    def refresh_token(self, refresh_token: str) -> Dict[str, Any]:
        """Refresh Gateway JWT (optional)"""
        token_url = f"{self.gateway_base_url}/api/v1/oauth-proxy/{self.proxy_id}/token"
        data = {
            'grant_type': 'refresh_token',
            'refresh_token': refresh_token,
            'client_id': self.client_id,
            'client_secret': self.client_secret
        }

        response = requests.post(token_url, data=data)
        response.raise_for_status()

        tokens = response.json()
        return {
            'access_token': tokens['access_token'],
            'refresh_token': tokens.get('refresh_token'),
            'expires_at': int(time.time()) + tokens['expires_in']
        }

    def call_mcp(self, method: str, params: Dict[str, Any], user_id: str) -> Dict[str, Any]:
        """Make MCP request through Gateway proxy"""
        token_data = self.get_user_token(user_id)

        if not token_data or self.is_token_expired(token_data):
            if token_data and token_data.get('refresh_token'):
                token_data = self.refresh_token(token_data['refresh_token'])
                self.store_user_token(user_id, token_data)
            else:
                raise ValueError('Token expired, please re-authenticate')

        # Make request to Gateway proxy
        proxy_url = 'https://gateway.yourcompany.com:9001/mcp'
        headers = {
            'Authorization': f"Bearer {token_data['access_token']}",
            'Content-Type': 'application/json'
        }
        payload = {
            'jsonrpc': '2.0',
            'method': method,
            'params': params,
            'id': secrets.token_hex(8)
        }

        response = requests.post(proxy_url, json=payload, headers=headers)
        response.raise_for_status()

        return response.json()

    # Storage methods (implement based on your backend)
    def store_oauth_state(self, user_id: str, state: Dict[str, str]):
        """Store OAuth state (session, cache, database)"""
        pass

    def get_oauth_state(self, user_id: str) -> Optional[Dict[str, str]]:
        """Retrieve OAuth state"""
        return None

    def clear_oauth_state(self, user_id: str):
        """Clear OAuth state"""
        pass

    def store_user_token(self, user_id: str, token: Dict[str, Any]):
        """Store user token (database, encrypted)"""
        pass

    def get_user_token(self, user_id: str) -> Optional[Dict[str, Any]]:
        """Retrieve user token"""
        return None

    def is_token_expired(self, token: Dict[str, Any]) -> bool:
        """Check if token is expired"""
        return time.time() >= token['expires_at'] - 60  # 1 minute buffer

Token Lifecycle Management

How Gateway Handles Token Refresh

The Gateway automatically manages upstream provider token refresh:

Token Storage Architecture

Automatic Token Injection

When your chatbot makes a request with a Gateway JWT:

  1. Gateway validates JWT - Checks signature, expiration, revocation
  2. Extracts JTI - Gets unique token identifier from JWT
  3. Looks up mapping - Finds upstream token linked to JTI
  4. Checks expiration - If expired, automatically refreshes
  5. Decrypts token - Retrieves and decrypts upstream token
  6. Injects token - Adds Authorization: Bearer {upstream_token} to request
  7. Forwards request - Sends to MCP server with upstream token

All of this is transparent to your chatbot!

User Attribution

Gateway links tokens to Gateway OAuth sessions for full user attribution:

  • Audit Logs: Every request logged with user identity
  • Token Revocation: Revoke all tokens for a specific user
  • Per-User Isolation: Tokens are isolated per user
  • Session Management: Gateway manages user sessions automatically

Chatbot Integration Security Considerations

PKCE Implementation

Why PKCE?

  • Prevents authorization code interception attacks
  • Required for public clients (mobile, desktop apps)
  • OAuth 2.1 best practice

Implementation Requirements:

  1. Generate Code Verifier: 43-128 character random string
  2. Generate Code Challenge: SHA256 hash of verifier (S256 method)
  3. Send Challenge: Include in authorization request
  4. Store Verifier: Keep verifier secret until token exchange
  5. Send Verifier: Include in token exchange request
  6. Gateway Validates: Gateway verifies challenge matches verifier

Example:

javascript
// Generate verifier (store securely)
const verifier = generateCodeVerifier();

// Generate challenge (send in auth request)
const challenge = await generateCodeChallenge(verifier);

// Authorization request
authURL.searchParams.set('code_challenge', challenge);
authURL.searchParams.set('code_challenge_method', 'S256');

// Token exchange (send verifier)
body.set('code_verifier', verifier);

Token Storage Best Practices

Do:

  • ✅ Store Gateway JWTs encrypted at rest
  • ✅ Use secure storage (database with encryption, secrets manager)
  • ✅ Set appropriate expiration times
  • ✅ Implement token rotation
  • ✅ Log token access for audit

Don't:

  • ❌ Store tokens in plain text
  • ❌ Commit tokens to version control
  • ❌ Store tokens in browser localStorage (XSS risk)
  • ❌ Share tokens between users
  • ❌ Log tokens in application logs

Example Secure Storage:

javascript
// Encrypt before storing
async function storeUserToken(userId, tokenData) {
  const encrypted = await encrypt(JSON.stringify(tokenData), ENCRYPTION_KEY);
  await db.users.update({
    where: { id: userId },
    data: { oauth_token: encrypted }
  });
}

// Decrypt when retrieving
async function getUserToken(userId) {
  const user = await db.users.findUnique({ where: { id: userId } });
  if (!user.oauth_token) return null;
  const decrypted = await decrypt(user.oauth_token, ENCRYPTION_KEY);
  return JSON.parse(decrypted);
}

Security Recommendations

  1. Use HTTPS: All OAuth flows must use HTTPS in production
  2. Validate Redirect URIs: Gateway validates redirect URIs against whitelist
  3. State Parameter: Always use state parameter for CSRF protection
  4. Token Expiration: Gateway JWTs expire (default 1 hour), refresh as needed
  5. Rate Limiting: Gateway enforces rate limits on OAuth endpoints
  6. Audit Logging: Monitor Gateway logs for suspicious activity
  7. Token Revocation: Implement token revocation on user logout
  8. Consent Screens: Enable consent screens for extra security (optional)

Chatbot Integration Troubleshooting

Chatbot Integration Common Issues

Issue 1: "Invalid redirect_uri"

Symptoms: OAuth callback fails with invalid_redirect_uri error

Cause: Redirect URI not in Gateway's allowed list

Solution:

  1. Check Gateway proxy configuration
  2. Ensure redirect URI exactly matches (including protocol, domain, path)
  3. Add redirect URI to oauth_proxy_allowed_redirects in proxy config

Issue 2: "PKCE validation failed"

Symptoms: Token exchange fails with invalid_grant error

Cause: Code verifier doesn't match code challenge

Solution:

  1. Ensure you store code verifier between authorization and token exchange
  2. Verify you're using the same verifier that generated the challenge
  3. Check that challenge method matches (S256 or plain)

Issue 3: "Token expired"

Symptoms: Requests fail with 401 Unauthorized

Cause: Gateway JWT expired

Solution:

  1. Check token expiration time
  2. Implement Gateway JWT refresh using refresh token
  3. Or re-initiate OAuth flow to get new tokens

Issue 4: "Upstream token refresh failed"

Symptoms: Gateway logs show token refresh errors

Cause: Upstream provider refresh token invalid or expired

Solution:

  1. Check Gateway logs for specific error
  2. User may need to re-authenticate (refresh token expired)
  3. Verify OAuth provider configuration is correct

Chatbot Debugging Tips

Enable Gateway Debug Logging:

bash
# Set log level to debug
export LOG_LEVEL=debug

# Restart Gateway
./build/unified-admin

Check Gateway Logs:

bash
# View OAuth Proxy logs
tail -f logs/unified-admin.log | grep "oauth-proxy"

# View token refresh logs
tail -f logs/unified-admin.log | grep "token.*refresh"

Test OAuth Flow Manually:

bash
# 1. Get authorization URL
curl "https://gateway.yourcompany.com/api/v1/oauth-proxy/1/authorize?\
  response_type=code&\
  client_id=YOUR_CLIENT_ID&\
  redirect_uri=https://your-chatbot.com/callback&\
  state=test123&\
  code_challenge=TEST_CHALLENGE&\
  code_challenge_method=S256"

# 2. Exchange code for token
curl -X POST https://gateway.yourcompany.com/api/v1/oauth-proxy/1/token \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=https://your-chatbot.com/callback" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_SECRET" \
  -d "code_verifier=CODE_VERIFIER"

Monitor Gateway Metrics:

bash
# Get OAuth Proxy metrics
curl https://gateway.yourcompany.com/api/v1/policies/performance \
  -H "Authorization: Bearer $ADMIN_TOKEN"

Chatbot Integration Best Practices

1. Error Handling

Always implement comprehensive error handling:

javascript
try {
  const result = await client.callMCP('tools/list', {}, userId);
} catch (error) {
  if (error.message === 'Authentication required') {
    // Re-initiate OAuth flow
    await initiateOAuthFlow(userId);
  } else if (error.message.includes('Token expired')) {
    // Refresh token or re-authenticate
    await refreshToken(userId);
  } else {
    // Log and handle other errors
    console.error('MCP request failed:', error);
  }
}

2. Token Refresh Strategy

Option A: Proactive Refresh (Recommended)

  • Check token expiration before each request
  • Refresh if expiring soon (e.g., within 5 minutes)
  • Prevents failed requests due to expiration

Option B: Reactive Refresh

  • Attempt request with current token
  • If 401, refresh token and retry
  • Simpler but may cause one failed request

3. User Experience

Clear Error Messages:

  • "Please connect your [Service] account" (no token)
  • "Your connection expired, please reconnect" (token expired)
  • "Connection failed, please try again" (other errors)

Seamless Re-authentication:

  • Store OAuth state in session
  • Auto-redirect to OAuth flow when needed
  • Return user to original page after auth

4. Chatbot Monitoring

Track Key Metrics:

  • OAuth flow completion rate
  • Token refresh success rate
  • MCP request success rate
  • Average token lifetime

Set Up Alerts:

  • High token refresh failure rate
  • Unusual OAuth flow failures
  • Token expiration spikes

5. Security

Regular Security Audits:

  • Review token storage implementation
  • Check for token leakage in logs
  • Verify PKCE implementation
  • Test token revocation

Compliance:

  • Ensure token encryption meets compliance requirements
  • Maintain audit logs for compliance
  • Implement token retention policies

Chatbot Integration Summary

The AI Security Gateway OAuth Proxy provides a production-ready solution for chatbots and applications that need to integrate with OAuth-protected MCP servers:

Key Takeaways

  1. No Gateway Code Changes: Gateway already supports OAuth Proxy - just configure it
  2. Simple Integration: Your chatbot only needs to implement standard OAuth 2.1 flow
  3. Automatic Token Management: Gateway handles all upstream token complexity
  4. Centralized Security: All tokens encrypted, centralized audit logging
  5. Reduced Complexity: ~90% reduction in OAuth-related code

Integration Checklist

  • [ ] Configure OAuth provider in Gateway
  • [ ] Create MCP proxy with OAuth Proxy enabled
  • [ ] Register chatbot as OAuth client (DCR)
  • [ ] Implement OAuth authorization flow
  • [ ] Implement token exchange
  • [ ] Store Gateway JWTs securely
  • [ ] Implement MCP requests with Gateway JWTs
  • [ ] Add error handling and token refresh
  • [ ] Test end-to-end flow
  • [ ] Monitor and set up alerts

Chatbot Integration Next Steps

  1. Review Gateway Configuration: Ensure OAuth provider and proxy are configured
  2. Implement Client Code: Use code examples in this guide
  3. Test Integration: Test with a small user group first
  4. Monitor: Watch Gateway logs and metrics
  5. Rollout: Gradually roll out to all users