Skip to main content

OAuth 2.1 Security Implementation

Plugged.in implements OAuth 2.1 security best practices to ensure the highest level of security for MCP server authentication. This document outlines the security measures in place.

OAuth 2.1 Overview

OAuth 2.1 is the next evolution of the OAuth 2.0 framework, consolidating security best practices from various OAuth extensions and eliminating insecure patterns.
Key Difference: OAuth 2.1 is NOT a separate specification, but rather a consolidation of OAuth 2.0 + security best practices (PKCE, token rotation, etc.)

Implemented Security Features

1. PKCE (Proof Key for Code Exchange)

RFC 7636 - Mandatory for all OAuth flows

Protection Against

  • Authorization code interception attacks
  • Code injection attacks
  • Man-in-the-middle attacks

Implementation

  • S256 challenge method (SHA-256)
  • 256-bit code verifier entropy
  • Automatic verification on callback
How it works:
// 1. Generate code verifier (256 bits of entropy)
const codeVerifier = crypto.randomBytes(32).toString('base64url');

// 2. Generate code challenge (SHA-256 hash)
const codeChallenge = crypto
  .createHash('sha256')
  .update(codeVerifier)
  .digest('base64url');

// 3. Send challenge in authorization request
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

// 4. Send verifier in token exchange
tokenParams.set('code_verifier', codeVerifier);

2. State Parameter Integrity Binding

OAuth 2.1 Best Practice - Prevents PKCE state tampering
Attack Prevented: Attackers cannot modify stored PKCE state parameters (server UUID, user ID, code verifier) to steal tokens or hijack flows.
Implementation:
// Generate HMAC-SHA256 integrity hash
const integrityHash = crypto
  .createHmac('sha256', secret)
  .update(`${state}|${serverUuid}|${userId}|${codeVerifier}`)
  .digest('hex');

// Store with PKCE state
await db.insert(oauthPkceStatesTable).values({
  state,
  server_uuid: serverUuid,
  user_id: userId,
  code_verifier: codeVerifier,
  integrity_hash: integrityHash, // ✅ OAuth 2.1 enhancement
  expires_at: new Date(Date.now() + 5 * 60 * 1000),
});

// Verify on callback (timing-safe comparison)
if (!verifyIntegrityHash(pkceState)) {
  // Revoke and reject - tampering detected
}
Files:
  • lib/oauth/integrity.ts - HMAC generation and verification
  • app/actions/trigger-mcp-oauth.ts - Hash creation
  • app/api/oauth/callback/route.ts - Hash verification

3. Reduced PKCE Expiration (5 Minutes)

OAuth 2.1 Recommendation - Reduced attack window
OAuth 2.1 recommends shorter PKCE state expiration to minimize the window for:
  • Replay attacks
  • Code interception attempts
  • State prediction attacks
Previous: 10 minutes (OAuth 2.0 typical) Current: 5 minutes (OAuth 2.1 recommended)
// Updated expiration
expires_at: new Date(Date.now() + 5 * 60 * 1000) // 5 minutes
Files:
  • db/schema.ts - Schema documentation
  • app/actions/trigger-mcp-oauth.ts:447 - State creation
  • app/api/oauth/callback/route.ts:166 - Expiration check

4. Refresh Token Rotation

OAuth 2.1 Best Practice - Prevents token reuse attacks

Single-Use Tokens

Each refresh token can only be used once. After use, it’s immediately invalidated.

Reuse Detection

If a refresh token is reused, all tokens are revoked as a security measure.
Implementation Flow: Code:
// 1. Check for token reuse (security measure)
if (tokenRecord.refresh_token_used_at) {
  console.error('[OAuth Security] Refresh token reuse detected!');
  // Revoke ALL tokens as security measure
  await db.delete(mcpServerOAuthTokensTable)
    .where(eq(mcpServerOAuthTokensTable.server_uuid, serverUuid));
  return false;
}

// 2. Mark token as used BEFORE exchange
await db.update(mcpServerOAuthTokensTable)
  .set({ refresh_token_used_at: new Date() })
  .where(eq(mcpServerOAuthTokensTable.server_uuid, serverUuid));

// 3. Exchange for new tokens
const newTokens = await exchangeRefreshToken(refreshToken);

// 4. Store new tokens with cleared used_at flag
await db.update(mcpServerOAuthTokensTable)
  .set({
    access_token_encrypted: encrypt(newTokens.access_token),
    refresh_token_encrypted: encrypt(newTokens.refresh_token),
    refresh_token_used_at: null, // ✅ Fresh token, not used
    updated_at: new Date(),
  })
  .where(eq(mcpServerOAuthTokensTable.server_uuid, serverUuid));
Database Schema:
ALTER TABLE mcp_server_oauth_tokens
ADD COLUMN refresh_token_used_at TIMESTAMPTZ;
Files:
  • db/schema.ts:1940 - Schema definition
  • lib/oauth/token-refresh-service.ts:97-104 - Reuse detection
  • lib/oauth/token-refresh-service.ts:128-139 - Mark as used
  • lib/oauth/token-refresh-service.ts:179-190 - Store new tokens

5. HTTP Basic Authentication for Client Credentials

RFC 6749 Section 2.3.1 - Prevents credential logging
Security Issue: Sending client_secret in URL-encoded body causes it to be logged in:
  • Proxy access logs
  • WAF logs
  • Server access logs
  • Load balancer logs
Secure Method:
// ✅ CORRECT: HTTP Basic Auth (RFC 6749 Section 2.3.1)
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
headers['Authorization'] = `Basic ${credentials}`;

// Body contains ONLY refresh_token
const tokenParams = new URLSearchParams({
  grant_type: 'refresh_token',
  refresh_token: refreshToken,
  // NO client_secret here!
});
Files:
  • lib/oauth/token-refresh-service.ts:134-140 - Token refresh
  • app/api/oauth/callback/route.ts - Token exchange

Security Enhancements Summary

FeatureStandardPlugged.in ImplementationSecurity Benefit
PKCERFC 7636✅ S256 method, 256-bit entropyPrevents code interception
State IntegrityOAuth 2.1 BP✅ HMAC-SHA256 bindingPrevents state tampering
PKCE ExpirationOAuth 2.1 BP✅ 5 minutes (vs 10)Reduced attack window
Token RotationOAuth 2.1 BP✅ Single-use + reuse detectionPrevents token replay
Client AuthRFC 6749 §2.3.1✅ HTTP Basic AuthPrevents credential logging
User BindingOWASP✅ PKCE state → user_id FKPrevents flow hijacking
Server OwnershipCustom✅ Multi-level validationPrevents token substitution
Rate LimitingOWASP✅ 10 req/15min on callbackPrevents brute force

Attack Scenarios Prevented

Before OAuth 2.1 Implementation

Authorization Code Injection → Attacker could hijack victim’s OAuth flow Token Reuse Attacks → Stolen refresh tokens could be used indefinitely State Parameter Tampering → Attacker could modify PKCE state to steal tokens Credential Logging → Client secrets exposed in access logs Extended Attack Window → 10-minute PKCE expiration too long

After OAuth 2.1 Implementation

All OAuth flows bound to authenticated userRefresh tokens are single-use onlyHMAC integrity verification prevents tamperingClient secrets never logged (HTTP Basic Auth)5-minute PKCE expiration reduces riskAutomatic token revocation on reuseServer ownership validationRate limiting on OAuth endpoints

Migration Notes

Database Changes

Two migrations were applied to support OAuth 2.1: Migration 0071: State Integrity Hash
ALTER TABLE oauth_pkce_states
ADD COLUMN integrity_hash TEXT NOT NULL;
Migration 0072: Refresh Token Rotation
ALTER TABLE mcp_server_oauth_tokens
ADD COLUMN refresh_token_used_at TIMESTAMPTZ;

Backward Compatibility

Fully Backward Compatible
  • Existing OAuth flows continue to work
  • New security features apply to all new flows
  • No breaking changes to API
  • Automatic migration on application upgrade

Environment Variables

Required

.env
# Use existing NEXTAUTH_SECRET or set dedicated OAuth secret
NEXTAUTH_SECRET=<your-secret-here>

# Or use dedicated OAuth integrity secret
OAUTH_INTEGRITY_SECRET=<generate-with-openssl-rand-base64-32>
.env
# Enable automatic PKCE cleanup
CRON_SECRET=<your-cron-secret>

# Production deployment flag
CLOUD_DEPLOY=true

Monitoring & Observability

Structured Logging with Loki

Plugged.in uses structured JSON logging with Loki for comprehensive OAuth security monitoring. All security events are automatically logged with full context for analysis.
See Observability Documentation for complete setup guide

Security Events Logged

All OAuth operations emit structured JSON logs to Loki:
{
  "level": 30,
  "time": 1699564800000,
  "service_name": "pluggedin-app",
  "event": "oauth_flow_success",
  "provider": "github-mcp-server",
  "duration_ms": 3500,
  "hasRefreshToken": true,
  "msg": "OAuth flow completed successfully"
}

LogQL Security Queries

Critical Security Events (Last Hour):
{service_name="pluggedin-app"}
  | json
  | severity="critical"
  | event =~ "(token_reuse|code_injection)"
All Integrity Violations:
{service_name="pluggedin-app"}
  | json
  | event="oauth_integrity_violation"
  | line_format "{{.violationType}}: {{.msg}}"
Token Refresh Failures:
{service_name="pluggedin-app"}
  | json
  | event="token_refresh_failure"
  | line_format "{{.reason}}: {{.err.message}}"
OAuth Flow Success Rate:
(
  sum(count_over_time({service_name="pluggedin-app"} | json | event="oauth_flow_success" [5m]))
  /
  sum(count_over_time({service_name="pluggedin-app"} | json | event =~ "oauth_flow_(success|failure)" [5m]))
) * 100

Prometheus Metrics

17 OAuth-specific metrics for real-time monitoring:

Flow Success Rate

oauth_flows_total{status="success"} / oauth_flows_totalSLO: > 95%

Token Reuse Detection

oauth_token_refresh_total{status="reuse_detected"}Alert: > 0 (Critical)

Code Injection Attempts

oauth_code_injection_attempts_totalAlert: > 0 (Critical)

Integrity Violations

oauth_integrity_violations_totalAlert: > 0 (High)
PromQL Alert Queries:
# P0: Token reuse detected
increase(oauth_token_refresh_total{status="reuse_detected"}[5m]) > 0

# P0: Code injection attempt
increase(oauth_code_injection_attempts_total[5m]) > 0

# P1: OAuth flow success rate below 95%
(
  sum(rate(oauth_flows_total{status="success"}[5m]))
  / sum(rate(oauth_flows_total[5m]))
) < 0.95

# P1: Token refresh p95 duration > 2s
histogram_quantile(0.95,
  sum(rate(oauth_token_refresh_duration_seconds_bucket[5m])) by (le)
) > 2

Grafana Dashboards

Pre-built dashboards available in /observability/dashboards:

OAuth Overview

  • Success rates
  • Operations/min
  • Active tokens
  • Error trends

Security Dashboard

  • Security events timeline
  • Attack heatmap
  • Top attackers
  • Violation breakdown

Performance Dashboard

  • Latency percentiles (p50, p95, p99)
  • Throughput by provider
  • Slow operations
  • Discovery performance

Critical Alerts Configuration

Configure these alerts in Grafana:
groups:
  - name: oauth_critical
    rules:
      # P0 Alerts - Page immediately
      - alert: OAuthTokenReuseDetected
        expr: increase(oauth_token_refresh_total{status="reuse_detected"}[5m]) > 0
        labels:
          severity: critical
          priority: P0
        annotations:
          summary: "OAuth token reuse attack detected"
          description: "Potential replay attack - tokens revoked"

      - alert: OAuthCodeInjectionAttempt
        expr: increase(oauth_code_injection_attempts_total[5m]) > 0
        labels:
          severity: critical
          priority: P0
        annotations:
          summary: "OAuth code injection attempt"
          description: "Authorization code injection attack in progress"

      # P1 Alerts - Notify team
      - alert: OAuthFlowSuccessRateLow
        expr: |
          (sum(rate(oauth_flows_total{status="success"}[5m]))
           / sum(rate(oauth_flows_total[5m]))) < 0.95
        for: 5m
        labels:
          severity: high
          priority: P1
        annotations:
          summary: "OAuth flow success rate below 95%"

      - alert: OAuthIntegrityViolations
        expr: increase(oauth_integrity_violations_total[15m]) > 5
        labels:
          severity: high
          priority: P1
        annotations:
          summary: "Multiple OAuth integrity violations detected"

Metrics to Monitor

OAuth Flow Success Rate
percentage
required
SLO: > 95%Metric: oauth_flows_total{status="success"} / oauth_flows_totalAlert if: < 95% for 5 minutes
Token Refresh Success Rate
percentage
required
SLO: > 99%Metric: oauth_token_refresh_total{status="success"} / oauth_token_refresh_totalAlert if: < 99% for 5 minutes
Token Reuse Detection
counter
required
Expected: 0Metric: oauth_token_refresh_total{status="reuse_detected"}Alert if: > 0 (IMMEDIATE - Critical)
Code Injection Attempts
counter
required
Expected: 0Metric: oauth_code_injection_attempts_totalAlert if: > 0 (IMMEDIATE - Critical)
Integrity Violations
counter
Expected: ~0Metric: oauth_integrity_violations_totalAlert if: > 5 per 15 minutes (High)
Token Refresh p95 Latency
duration
SLO: < 2 secondsMetric: histogram_quantile(0.95, oauth_token_refresh_duration_seconds_bucket)Alert if: > 2s for 5 minutes

Incident Response Playbooks

Severity: Critical - Immediate action requiredIndicators:
  • oauth_refresh_token_reuse_detected event in logs
  • oauth_token_refresh_total{status="reuse_detected"} > 0
Automatic Actions:
  • ✅ All tokens for server immediately revoked
  • ✅ User forced to re-authenticate
  • ✅ Security event logged with full context
Manual Response:
  1. Check logs for serverUuid and userId
  2. Review user’s recent OAuth activity
  3. Check for concurrent requests (race condition vs. attack)
  4. If attack: Block user IP, audit account
  5. If race condition: Review application logs, check for retry logic issues
Loki Query:
{service_name="pluggedin-app"}
  | json
  | event="oauth_refresh_token_reuse_detected"
  | line_format "User: {{.userId}}, Server: {{.serverUuid}}, Used: {{.tokenUsedAt}}"
Severity: Critical - Immediate action requiredIndicators:
  • oauth_code_injection_attempt event in logs
  • oauth_code_injection_attempts_total > 0
Automatic Actions:
  • ✅ Authorization code rejected
  • ✅ Security event logged with attacker/victim details
Manual Response:
  1. Identify attacker from logs: attackerUserId
  2. Identify victim: victimUserId
  3. Block attacker account immediately
  4. Notify victim user of attempted account compromise
  5. Audit all OAuth flows for both users in last 24h
  6. Check for pattern of attacks (same IP, same target servers)
Loki Query:
{service_name="pluggedin-app"}
  | json
  | event="oauth_code_injection_attempt"
  | line_format "Attacker: {{.attackerUserId}}, Victim: {{.victimUserId}}"
Severity: High - Investigate within 1 hourIndicators:
  • oauth_integrity_violation events in logs
  • oauth_integrity_violations_total increasing
Possible Causes:
  • State tampering attempt
  • Database corruption
  • Application bug (HMAC calculation mismatch)
Manual Response:
  1. Check violationType: hash_mismatch, state_reuse, user_mismatch
  2. Review affected serverUuid and userId
  3. If single occurrence: Likely user error or network issue
  4. If multiple from same user: Possible attack or client issue
  5. If widespread: Check for application deployment issues
Loki Query:
{service_name="pluggedin-app"}
  | json
  | event="oauth_integrity_violation"
  | line_format "[{{.violationType}}] {{.msg}}"
Severity: High - Investigate within 1 hourIndicators:
  • OAuth success rate < 95%
  • Increased error rates in dashboards
Possible Causes:
  • OAuth server downtime
  • Network issues
  • Configuration changes
  • High user error rate (expired states, etc.)
Manual Response:
  1. Check recent deployments
  2. Review error distribution by type
  3. Check OAuth server status
  4. Review network connectivity
  5. Check for expired PKCE states (>5 minutes)
PromQL Query:
topk(10, sum by (event) (
  rate(oauth_flows_total{status="failure"}[5m])
))

Sensitive Data Redaction

All sensitive OAuth data is automatically redacted in logs:
Redacted Fields (never logged in plaintext):
  • access_token
  • refresh_token
  • code_verifier
  • client_secret
  • authorization_code
Example redacted log:
{
  "event": "token_refresh_success",
  "access_token": "[REDACTED]",
  "refresh_token": "[REDACTED]",
  "serverUuid": "visible-server-id",
  "msg": "Token successfully refreshed"
}

Testing OAuth 2.1 Compliance

Test State Integrity

# Should fail - tampering detected
curl "https://app.plugged.in/api/oauth/callback?code=valid&state=tampered" \
  -H "Cookie: session_token"

# Expected: oauth_error=integrity_violation

Test Refresh Token Reuse

# 1. Valid refresh
curl -X POST /api/refresh-token -d "server_uuid=..."

# 2. Attempt reuse (should revoke all tokens)
curl -X POST /api/refresh-token -d "server_uuid=..."

# Expected: All tokens revoked, forced re-auth

Test PKCE Expiration

# Wait 6 minutes after initiating OAuth
curl "https://app.plugged.in/api/oauth/callback?code=valid&state=expired"

# Expected: oauth_error=state_expired

References


Summary

Plugged.in implements industry-leading OAuth 2.1 security with: 5 Core Security Enhancements beyond OAuth 2.0 ✅ 8 Attack Scenarios prevented ✅ Zero Breaking Changes for existing integrations ✅ Comprehensive Monitoring and alerting ✅ Full RFC Compliance with OAuth 2.1 draft Your MCP server authentication is enterprise-grade secure.