OAuth API Reference
OAuth 2.1 authentication endpoints for MCP server authorization.
All OAuth endpoints are internal and used by the Plugged.in application. External API users should use API key authentication instead.
Base URL
Production: https://app.plugged.in
Development: http://localhost:12005
Endpoints
Initiate OAuth Flow
Initiates OAuth 2.1 authorization flow with PKCE.
UUID of the MCP server to authorize
redirectUri
string
default: "/api/oauth/callback"
OAuth callback URI (usually default)
Request Example:
curl -X POST https://app.plugged.in/api/oauth/initiate \
-H "Content-Type: application/json" \
-H "Cookie: session_token=..." \
-d '{
"serverUuid": "550e8400-e29b-41d4-a716-446655440000"
}'
Response:
{
"authorizationUrl" : "https://auth.example.com/authorize?client_id=...&code_challenge=...&state=..." ,
"state" : "pkce_state_abc123" ,
"expiresAt" : "2024-01-15T10:35:00Z"
}
Status Codes:
Code Description
200 Authorization URL generated successfully 400 Invalid server UUID or missing parameters 401 User not authenticated 404 Server not found or no OAuth configuration 500 OAuth discovery or configuration error
Security Features:
✅ PKCE code_challenge generated (S256 method)
✅ State parameter bound to user session
✅ HMAC integrity hash for state validation
✅ 5-minute expiration on PKCE state
OAuth Callback
Handles OAuth provider callback with authorization code.
Authorization code from OAuth provider
PKCE state parameter (generated during initiation)
OAuth error code (if authorization failed)
Human-readable error description
Success Response:
Redirects to:
/mcp-servers?oauth_success=true&server_uuid=550e8400-e29b-41d4-a716-446655440000
Error Response:
Redirects to:
/mcp-servers?oauth_error=<error_code>&server_uuid=<uuid>
Error Codes:
Code Description User Action
state_expiredPKCE state expired (>5 minutes) Retry OAuth flow state_invalidInvalid or missing state parameter Retry OAuth flow integrity_violationState tampering detected Contact support (security issue) code_exchange_failedToken exchange with provider failed Check OAuth server status user_mismatchCode injection attempt detected Immediate security alert access_deniedUser denied authorization Normal - user cancelled
Security Validations:
✅ PKCE state exists and not expired
✅ HMAC integrity hash verification
✅ User ID binding check (prevents code injection)
✅ One-time use enforcement (state deleted after use)
✅ Rate limiting (10 requests per 15 minutes per user)
OAuth 2.1 Compliance:
✅ PKCE code_verifier validation
✅ State parameter integrity
✅ Token rotation on refresh
✅ Refresh token reuse detection
Token Refresh (Internal)
This endpoint is internal only and used automatically by the application. Manual calls are not recommended.
Refreshes OAuth access token using stored refresh token.
UUID of the MCP server with expired token
Request Example:
curl -X POST https://app.plugged.in/api/oauth/refresh \
-H "Content-Type: application/json" \
-H "Cookie: session_token=..." \
-d '{
"serverUuid": "550e8400-e29b-41d4-a716-446655440000"
}'
Response:
{
"success" : true ,
"expiresAt" : "2024-01-15T11:35:00Z"
}
Status Codes:
Code Description
200 Token refreshed successfully 400 Invalid server UUID or no refresh token available 401 User not authenticated or not server owner 403 Server ownership violation (security) 409 Token reuse detected - all tokens revoked (security) 500 Token exchange with OAuth provider failed
Security Features (OAuth 2.1):
✅ Optimistic locking prevents concurrent refresh
✅ Refresh token marked as used before exchange
✅ Automatic token rotation
✅ Reuse detection → immediate revocation
✅ Server ownership validation (multi-level)
Race Condition Prevention:
The endpoint uses atomic database operations:
// 1. Atomic lock acquisition
UPDATE mcp_server_oauth_tokens
SET refresh_token_locked_at = NOW ()
WHERE server_uuid = $1
RETURNING * ;
// 2. Check if already used (reuse detection)
IF refresh_token_used_at IS NOT NULL :
REVOKE ALL TOKENS // Security measure
RETURN 403
// 3. Exchange with OAuth provider
// 4. Store new tokens atomically
PKCE Cleanup (Internal Cron)
POST /api/oauth/cleanup-pkce
This endpoint requires CRON_SECRET authentication and should only be called by external cron services.
Manually triggers expired PKCE state cleanup.
Bearer token with CRON_SECRET value
Request Example:
curl -X POST https://app.plugged.in/api/oauth/cleanup-pkce \
-H "Authorization: Bearer $CRON_SECRET "
Response:
{
"deleted" : 15 ,
"gracePeriodMinutes" : 10
}
Automatic Cleanup:
The application automatically runs cleanup every 15 minutes with a 10-minute grace period :
// Automatic cleanup (no manual call needed)
setInterval (() => {
cleanupExpiredPkceStates (); // Every 15 minutes
}, 15 * 60 * 1000 );
Grace Period Logic:
States are only deleted if they expired more than 10 minutes ago :
// Cutoff time: 10 minutes ago
const cutoffTime = new Date ( Date . now () - 10 * 60 * 1000 );
// Only delete states that expired BEFORE cutoff
DELETE FROM oauth_pkce_states
WHERE expires_at < cutoffTime ;
This protects OAuth flows that may still be completing even after the 5-minute PKCE expiration.
Metrics Endpoint
Public endpoint exposing Prometheus metrics for monitoring
Returns OAuth metrics in Prometheus format.
Response Example:
# HELP oauth_flows_total Total number of OAuth flows
# TYPE oauth_flows_total counter
oauth_flows_total{provider="github-mcp-server",status="success"} 42
oauth_flows_total{provider="github-mcp-server",status="failure"} 2
# HELP oauth_token_refresh_total Token refresh attempts
# TYPE oauth_token_refresh_total counter
oauth_token_refresh_total{status="success",reason="normal"} 128
oauth_token_refresh_total{status="reuse_detected",reason="security"} 0
# HELP oauth_active_tokens Current active OAuth tokens
# TYPE oauth_active_tokens gauge
oauth_active_tokens 15
# HELP oauth_code_injection_attempts_total Code injection attempts
# TYPE oauth_code_injection_attempts_total counter
oauth_code_injection_attempts_total 0
Available Metrics:
oauth_flows_total{provider, status} - Total flows by outcome
oauth_flow_duration_seconds{provider, status} - Flow duration histogram
oauth_token_refresh_total{status, reason} - Refresh attempts
oauth_token_refresh_duration_seconds{status} - Refresh duration
oauth_token_revocations_total{reason} - Token revocations
oauth_active_tokens - Current active token count
oauth_pkce_validations_total{status, reason} - PKCE validations
oauth_pkce_states_created_total - States created
oauth_pkce_states_cleaned_total{reason} - States cleaned up
oauth_active_pkce_states - Current active states
oauth_security_events_total{event_type, severity} - Security events
oauth_integrity_violations_total{violation_type} - Integrity violations
oauth_code_injection_attempts_total - Code injection attempts
oauth_discovery_attempts_total{method, status} - Discovery attempts
oauth_discovery_duration_seconds{method, status} - Discovery duration
oauth_client_registrations_total{status} - Client registrations
oauth_client_registration_duration_seconds{status} - Registration duration
Scrape Configuration:
# Prometheus configuration
scrape_configs :
- job_name : 'pluggedin-app'
static_configs :
- targets : [ 'localhost:12005' ]
metrics_path : '/metrics'
scrape_interval : 15s
OAuth Flow Diagram
Security Features Summary
PKCE (RFC 7636)
S256 challenge method
256-bit code verifier
One-time state validation
5-minute expiration
State Integrity
HMAC-SHA256 binding
User ID validation
Server UUID binding
Tampering detection
Token Rotation
Single-use refresh tokens
Automatic rotation
Reuse detection
Immediate revocation
Ownership Validation
Server → Profile → Project → User chain
Multi-level verification
Prevents token substitution
Cross-user attack prevention
Error Handling
Client-Side Error Handling
// Handle OAuth callback errors
const searchParams = new URLSearchParams ( window . location . search );
if ( searchParams . has ( 'oauth_error' )) {
const errorCode = searchParams . get ( 'oauth_error' );
const serverUuid = searchParams . get ( 'server_uuid' );
switch ( errorCode ) {
case 'state_expired' :
showNotification ( 'OAuth session expired. Please try again.' );
break ;
case 'access_denied' :
showNotification ( 'Authorization cancelled.' );
break ;
case 'integrity_violation' :
showError ( 'Security error detected. Please contact support.' );
break ;
default :
showError ( `OAuth failed: ${ errorCode } ` );
}
}
Server-Side Error Handling
All OAuth endpoints return structured errors:
{
"error" : "state_expired" ,
"message" : "PKCE state expired after 5 minutes" ,
"details" : {
"state" : "pkce_state_abc123" ,
"expiredAt" : "2024-01-15T10:30:00Z" ,
"currentTime" : "2024-01-15T10:37:00Z"
}
}
Rate Limiting
OAuth endpoints are protected by rate limiting:
Endpoint Limit Window
/api/oauth/initiate10 requests 15 minutes per user /api/oauth/callback10 requests 15 minutes per user /api/oauth/refresh60 requests 1 minute per user /api/oauth/cleanup-pkce1 request 1 minute (cron only)
Rate Limit Headers:
X-RateLimit-Limit : 10
X-RateLimit-Remaining : 7
X-RateLimit-Reset : 1705320000
Rate Limit Exceeded Response:
{
"error" : "rate_limit_exceeded" ,
"message" : "Too many OAuth requests. Please try again later." ,
"retryAfter" : 300
}
Testing OAuth Endpoints
Development Environment
# Set up test OAuth provider
export OAUTH_TEST_CLIENT_ID = test-client-id
export OAUTH_TEST_CLIENT_SECRET = test-client-secret
export OAUTH_TEST_AUTH_URL = https :// auth . example . com / authorize
export OAUTH_TEST_TOKEN_URL = https :// auth . example . com / token
# Start application
pnpm dev
# Test OAuth flow
curl -X POST http://localhost:12005/api/oauth/initiate \
-H "Content-Type: application/json" \
-H "Cookie: session_token=..." \
-d '{"serverUuid": "test-server-uuid"}'
Integration Tests
import { describe , it , expect } from 'vitest' ;
describe ( 'OAuth API' , () => {
it ( 'should initiate OAuth flow with PKCE' , async () => {
const response = await fetch ( '/api/oauth/initiate' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ serverUuid: 'test-uuid' }),
});
const data = await response . json ();
expect ( data . authorizationUrl ). toContain ( 'code_challenge' );
expect ( data . authorizationUrl ). toContain ( 'code_challenge_method=S256' );
expect ( data . state ). toBeDefined ();
});
it ( 'should reject expired PKCE state' , async () => {
// Wait 6 minutes
await sleep ( 6 * 60 * 1000 );
const response = await fetch (
`/api/oauth/callback?code=test&state=expired-state`
);
expect ( response . url ). toContain ( 'oauth_error=state_expired' );
});
});
Best Practices
Security Checklist
✅ Always use HTTPS in production
✅ Never log sensitive tokens or code verifiers
✅ Monitor oauth_code_injection_attempts_total metric
✅ Set up alerts for oauth_token_reuse_detected
✅ Rotate NEXTAUTH_SECRET periodically
✅ Review OAuth security logs weekly
✅ Keep dependencies updated (npm audit)
Performance Tips
✅ Enable HTTP/2 for faster OAuth redirects
✅ Use connection pooling for database queries
✅ Cache OAuth provider metadata (RFC 9728)
✅ Monitor p95 latency for token refresh
✅ Set appropriate PKCE cleanup interval (15 min)
Monitoring Best Practices
✅ Track OAuth flow success rate (SLO: >95%)
✅ Monitor token refresh duration (p95 < 2s)
✅ Alert on any code injection attempts
✅ Review integrity violations daily
✅ Analyze OAuth error distribution weekly