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
- Introduction
- Architecture Overview
- Gateway Configuration
- Client Integration Guide
- Code Examples
- Token Lifecycle Management
- Security Considerations
- Troubleshooting
- 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:
- Register once as an OAuth client with the Gateway
- Implement a simple OAuth flow to get Gateway-issued JWTs
- Store Gateway JWTs (simple strings, no refresh logic needed)
- 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
| Component | Responsibility |
|---|---|
| Your Chatbot | Initiates OAuth flow, stores Gateway JWTs, makes requests with JWTs |
| OAuth Proxy Service | Handles OAuth flows, issues Gateway JWTs, manages client registrations |
| Token Manager | Stores/retrieves encrypted upstream tokens, handles token refresh |
| MCP Proxy | Validates Gateway JWTs, injects upstream tokens, proxies requests |
| OAuth Provider | Authenticates users, issues upstream tokens (Okta, Google, etc.) |
| MCP Server | Receives requests with upstream tokens, returns data |
Token Flow Sequence
Benefits Summary
| Benefit | Description |
|---|---|
| Simplified Code | No token refresh logic, no token-to-user mapping, no expiration handling |
| Automatic Refresh | Gateway handles all token refresh automatically and transparently |
| Centralized Security | All tokens encrypted at rest, centralized audit logging, security policies |
| Reduced Complexity | ~90% reduction in OAuth-related code in your application |
| Better Security | Tokens never exposed to your application, encrypted storage, PKCE enforcement |
| Easier Maintenance | Gateway 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:
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/callbackProvider-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:
{
"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:
| Setting | Value | Description |
|---|---|---|
oauth_proxy_enabled | true | Enables OAuth Proxy functionality |
oauth_proxy_mode | "gateway" | Gateway manages tokens (recommended) |
oauth_proxy_provider_id | 1 | ID of OAuth provider from Step 1 |
oauth_proxy_allowed_redirects | [...] | Your chatbot's callback URLs |
oauth_proxy_require_consent | false | Optional consent screen (set true for extra security) |
Via API:
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:
# 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_endpointExpected Response:
{
"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:
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:
{
"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 configclient_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):
// 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:
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:
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:
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):
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
// 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
# 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 bufferToken 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:
- Gateway validates JWT - Checks signature, expiration, revocation
- Extracts JTI - Gets unique token identifier from JWT
- Looks up mapping - Finds upstream token linked to JTI
- Checks expiration - If expired, automatically refreshes
- Decrypts token - Retrieves and decrypts upstream token
- Injects token - Adds
Authorization: Bearer {upstream_token}to request - 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:
- Generate Code Verifier: 43-128 character random string
- Generate Code Challenge: SHA256 hash of verifier (S256 method)
- Send Challenge: Include in authorization request
- Store Verifier: Keep verifier secret until token exchange
- Send Verifier: Include in token exchange request
- Gateway Validates: Gateway verifies challenge matches verifier
Example:
// 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:
// 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
- Use HTTPS: All OAuth flows must use HTTPS in production
- Validate Redirect URIs: Gateway validates redirect URIs against whitelist
- State Parameter: Always use state parameter for CSRF protection
- Token Expiration: Gateway JWTs expire (default 1 hour), refresh as needed
- Rate Limiting: Gateway enforces rate limits on OAuth endpoints
- Audit Logging: Monitor Gateway logs for suspicious activity
- Token Revocation: Implement token revocation on user logout
- 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:
- Check Gateway proxy configuration
- Ensure redirect URI exactly matches (including protocol, domain, path)
- Add redirect URI to
oauth_proxy_allowed_redirectsin proxy config
Issue 2: "PKCE validation failed"
Symptoms: Token exchange fails with invalid_grant error
Cause: Code verifier doesn't match code challenge
Solution:
- Ensure you store code verifier between authorization and token exchange
- Verify you're using the same verifier that generated the challenge
- Check that challenge method matches (S256 or plain)
Issue 3: "Token expired"
Symptoms: Requests fail with 401 Unauthorized
Cause: Gateway JWT expired
Solution:
- Check token expiration time
- Implement Gateway JWT refresh using refresh token
- 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:
- Check Gateway logs for specific error
- User may need to re-authenticate (refresh token expired)
- Verify OAuth provider configuration is correct
Chatbot Debugging Tips
Enable Gateway Debug Logging:
# Set log level to debug
export LOG_LEVEL=debug
# Restart Gateway
./build/unified-adminCheck Gateway Logs:
# 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:
# 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:
# 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:
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
- No Gateway Code Changes: Gateway already supports OAuth Proxy - just configure it
- Simple Integration: Your chatbot only needs to implement standard OAuth 2.1 flow
- Automatic Token Management: Gateway handles all upstream token complexity
- Centralized Security: All tokens encrypted, centralized audit logging
- 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
- Review Gateway Configuration: Ensure OAuth provider and proxy are configured
- Implement Client Code: Use code examples in this guide
- Test Integration: Test with a small user group first
- Monitor: Watch Gateway logs and metrics
- Rollout: Gradually roll out to all users