cyber security

OAuth and JWT Security: What Your Logs Should Track

Your OAuth implementation looks secure on paper. You’re using industry-standard flows, validating JWT signatures, and enforcing proper scopes. But at 2:47 AM, your security team discovers that attackers have been using stolen refresh tokens to maintain persistent access for three weeks. Your authentication logs show successful logins, but they completely missed the token replay attacks.

OAuth and JWT have become the backbone of modern authentication, but they’ve also created new attack vectors that traditional logging doesn’t capture. While developers focus on implementing the flows correctly, they often miss the critical security events that reveal when those flows are being abused.

The difference between OAuth implementations that detect attacks quickly and those that remain compromised for months often comes down to one factor: comprehensive logging that tracks token lifecycles, not just authentication outcomes.

The Hidden Attack Surface

OAuth and JWT create a complex authentication ecosystem with multiple potential failure points:

  • Authorization Code Interception: Attackers steal codes before they’re exchanged for tokens
  • Token Replay Attacks: Stolen access tokens used from different locations
  • Refresh Token Theft: Long-lived tokens providing persistent access
  • Scope Escalation: Tokens used beyond their intended permissions
  • Client Impersonation: Malicious apps mimicking legitimate clients

Traditional authentication logging captures login success/failure but misses these nuanced attacks that exploit the token-based nature of OAuth.

Case Study: The Invisible Token Theft

Company: SaaS platform with OAuth-based API access
Breach Duration: 3 weeks undetected
Attack Vector: Stolen refresh tokens used to maintain access

What Traditional Logging Showed

2024-03-15 14:30:22 INFO OAuth login successful - user_12345
2024-03-15 14:35:18 INFO API request authorized - user_12345
2024-03-15 14:42:33 INFO Token refresh successful - user_12345
2024-03-15 15:15:44 INFO API request authorized - user_12345
        

What Security-Aware OAuth Logging Revealed:

// Comprehensive OAuth flow logging
logger.log('oauth_authorization_requested', {
  userId: null, // Don't know yet
  resource: 'oauth_authorization',
  metadata: {
    client_id: 'app_mobile_v2.1',
    requested_scopes: ['read:profile', 'write:data'],
    response_type: 'code',
    redirect_uri: 'https://app.example.com/callback',
    state_parameter: hashState(state),
    code_challenge_present: !!codeChallenge,
    ip_address: req.ip,
    user_agent: req.get('User-Agent'),
    referrer: req.get('Referer')
  },
  ipAddress: req.ip,
  userAgent: req.get('User-Agent')
});

// Authorization code generation
logger.log('authorization_code_issued', {
  userId: user.id,
  resource: 'oauth_authorization_code',
  metadata: {
    client_id: 'app_mobile_v2.1',
    code_id: hashAuthCode(authCode), // Hash for correlation
    granted_scopes: ['read:profile', 'write:data'],
    expires_in_seconds: 600,
    pkce_challenge_verified: true,
    user_consent_given: true,
    ip_address: req.ip
  },
  ipAddress: req.ip,
  userAgent: req.get('User-Agent')
});

// Token exchange - the critical moment
logger.log('oauth_token_exchange', {
  userId: user.id,
  resource: 'oauth_token_exchange',
  metadata: {
    client_id: 'app_mobile_v2.1',
    code_id: hashAuthCode(authCode),
    exchange_ip: req.ip, // Compare to auth IP
    exchange_user_agent: req.get('User-Agent'),
    time_since_auth_seconds: timeSinceAuth,
    client_authenticated: true,
    pkce_verifier_valid: true,
    access_token_id: hashToken(accessToken),
    refresh_token_id: hashToken(refreshToken),
    token_expires_in: 3600,
    ip_location_match: compareIPLocations(authIP, req.ip)
  },
  ipAddress: req.ip,
  userAgent: req.get('User-Agent')
});

// Access token usage
logger.log('api_request_with_token', {
  userId: user.id,
  resource: 'api_access',
  metadata: {
    access_token_id: hashToken(accessToken),
    requested_endpoint: '/api/v1/user/data',
    required_scope: 'read:profile',
    token_age_minutes: tokenAgeMinutes,
    ip_address: req.ip,
    user_agent: req.get('User-Agent'),
    geographic_location: getLocationFromIP(req.ip),
    is_suspicious_location: isSuspiciousLocation(user.id, req.ip),
    rate_limit_remaining: rateLimitRemaining
  },
  ipAddress: req.ip,
  userAgent: req.get('User-Agent')
});

// Refresh token usage - high security event
logger.log('refresh_token_used', {
  userId: user.id,
  resource: 'oauth_refresh_token',
  metadata: {
    refresh_token_id: hashToken(refreshToken),
    old_access_token_id: hashToken(oldAccessToken),
    new_access_token_id: hashToken(newAccessToken),
    refresh_ip: req.ip,
    refresh_user_agent: req.get('User-Agent'),
    original_issue_ip: token.originalIP,
    original_issue_location: token.originalLocation,
    days_since_issue: daysSinceTokenIssue,
    location_change: detectLocationChange(token.originalIP, req.ip),
    device_fingerprint_match: compareDeviceFingerprints(token.deviceFingerprint, currentFingerprint)
  },
  ipAddress: req.ip,
  userAgent: req.get('User-Agent')
});
        

The Attack Pattern Revealed

With comprehensive logging, the attack became obvious:

  1. Normal Login: User authenticated from New York office (IP: 198.51.100.1)
  2. Token Theft: Refresh token somehow compromised
  3. Malicious Usage: Same refresh token used from Romania (IP: 185.23.45.67) three days later
  4. Persistence: Attacker refreshed tokens every 55 minutes to maintain access

Essential OAuth Security Events

Authorization Flow Events

// Authorization request initiation
'oauth_auth_requested', 'oauth_consent_displayed', 'oauth_consent_granted'
'oauth_consent_denied', 'authorization_code_issued', 'authorization_code_expired'

// Token lifecycle events  
'oauth_token_exchange', 'access_token_issued', 'refresh_token_issued'
'access_token_expired', 'refresh_token_expired', 'token_revoked'

// Security-specific events
'oauth_code_replay_attempt', 'oauth_invalid_client', 'oauth_scope_violation'
'suspicious_token_usage', 'token_geographic_anomaly', 'rapid_token_refresh'
        

JWT-Specific Security Events

// JWT validation events
'jwt_signature_invalid', 'jwt_expired', 'jwt_not_yet_valid'
'jwt_issuer_mismatch', 'jwt_audience_mismatch', 'jwt_algorithm_mismatch'

// JWT usage patterns
'jwt_reuse_detected', 'jwt_from_suspicious_location', 'jwt_scope_escalation'
'jwt_claim_manipulation_attempt', 'jwt_weak_signature_algorithm'
        

Critical OAuth/JWT Metadata to Capture

Client and Application Context

{
  eventType: "oauth_token_exchange",
  userId: "user_12345",
  resource: "oauth_token_exchange",
  metadata: {
    // Client identification
    client_id: "mobile_app_v2.1",
    client_type: "public", // public, confidential
    client_authenticated: true,
    redirect_uri: "https://app.example.com/callback",
    
    // Flow security
    grant_type: "authorization_code",
    pkce_challenge_method: "S256",
    pkce_verifier_valid: true,
    state_parameter_valid: true,
    
    // Timing and correlation
    authorization_time: "2024-03-15T14:30:22Z",
    exchange_time: "2024-03-15T14:31:15Z",
    time_between_auth_exchange_seconds: 53,
    code_id: "code_abc123_hash",
    
    // Geographic and device context
    auth_ip: "192.168.1.100",
    auth_location: "New York, NY",
    exchange_ip: "192.168.1.100", 
    exchange_location: "New York, NY",
    location_match: true,
    device_fingerprint: "device_fp_xyz789",
    user_agent_match: true
  }
}
        

Token Lifecycle Tracking

{
  eventType: "access_token_used",
  userId: "user_12345", 
  resource: "api_access",
  metadata: {
    // Token identification
    access_token_id: "token_def456_hash",
    token_type: "Bearer",
    token_age_minutes: 45,
    token_expires_in_minutes: 15,
    
    // Scope and permissions
    granted_scopes: ["read:profile", "write:data"],
    required_scope: "read:profile",
    scope_sufficient: true,
    
    // Usage context
    api_endpoint: "/api/v1/user/profile",
    http_method: "GET",
    request_size_bytes: 0,
    response_size_bytes: 1247,
    
    // Security indicators
    ip_address: "192.168.1.100",
    geographic_location: "New York, NY", 
    is_known_location: true,
    device_fingerprint: "device_fp_xyz789",
    rate_limit_bucket: "user_api_calls",
    rate_limit_remaining: 975,
    
    // Anomaly detection
    unusual_time_of_day: false,
    high_frequency_usage: false,
    suspicious_endpoint_pattern: false
  }
}
        

Advanced Security Detection Patterns

Token Replay Detection

// Detect simultaneous token usage from different locations
async function detectTokenReplay(tokenId, currentIP, userAgent) {
  const recentUsage = await getRecentTokenUsage(tokenId, 300); // Last 5 minutes
  
  if (recentUsage.length > 0) {
    const locationMismatch = recentUsage.some(usage => 
      !isSameGeographicRegion(usage.ip, currentIP)
    );
    
    if (locationMismatch) {
      logger.log('token_replay_suspected', {
        userId: tokenInfo.userId,
        resource: 'security_violation',
        metadata: {
          access_token_id: hashToken(tokenId),
          current_ip: currentIP,
          current_location: getLocationFromIP(currentIP),
          recent_ips: recentUsage.map(u => u.ip),
          recent_locations: recentUsage.map(u => getLocationFromIP(u.ip)),
          time_window_minutes: 5,
          geographic_distance_km: calculateDistance(recentUsage[0].ip, currentIP),
          violation_type: 'impossible_travel'
        },
        ipAddress: currentIP,
        userAgent: userAgent
      });
    }
  }
}

// Detect refresh token abuse
async function detectRefreshTokenAbuse(refreshTokenId, userId, currentIP) {
  const refreshHistory = await getRefreshTokenHistory(refreshTokenId, 24); // Last 24 hours
  
  const suspiciousPatterns = {
    high_frequency: refreshHistory.length > 20, // More than 20 refreshes per day
    geographic_anomaly: hasUnusualGeographicPattern(refreshHistory),
    rapid_succession: hasRapidSuccessionPattern(refreshHistory),
    device_switching: hasDeviceSwitchingPattern(refreshHistory)
  };
  
  if (Object.values(suspiciousPatterns).some(Boolean)) {
    logger.log('refresh_token_abuse_detected', {
      userId: userId,
      resource: 'security_violation',
      metadata: {
        refresh_token_id: hashToken(refreshTokenId),
        refresh_count_24h: refreshHistory.length,
        suspicious_patterns: suspiciousPatterns,
        unique_ips_24h: getUniqueIPs(refreshHistory).length,
        unique_locations_24h: getUniqueLocations(refreshHistory).length,
        violation_severity: calculateViolationSeverity(suspiciousPatterns)
      },
      ipAddress: currentIP
    });
  }
}
        

Scope Escalation Monitoring

// Monitor for unauthorized scope usage
function validateAndLogScopeUsage(token, requiredScope, endpoint) {
  const hasScope = token.scopes.includes(requiredScope);
  
  logger.log('api_scope_validation', {
    userId: token.userId,
    resource: 'api_authorization',
    metadata: {
      access_token_id: hashToken(token.id),
      required_scope: requiredScope,
      granted_scopes: token.scopes,
      scope_present: hasScope,
      endpoint: endpoint,
      scope_hierarchy_level: getScopeHierarchyLevel(requiredScope),
      is_elevated_scope: isElevatedScope(requiredScope),
      user_role: token.userRole
    }
  });
  
  if (!hasScope) {
    logger.log('scope_violation_attempt', {
      userId: token.userId,
      resource: 'security_violation',
      metadata: {
        access_token_id: hashToken(token.id),
        attempted_scope: requiredScope,
        available_scopes: token.scopes,
        endpoint: endpoint,
        violation_type: 'insufficient_scope',
        client_id: token.clientId,
        user_role: token.userRole
      }
    });
  }
  
  return hasScope;
}

// Detect scope creep over time
async function detectScopeCreep(userId, newScopes) {
  const historicalScopes = await getUserHistoricalScopes(userId, 30); // Last 30 days
  const scopeEvolution = analyzeScopeEvolution(historicalScopes, newScopes);
  
  if (scopeEvolution.hasEscalation) {
    logger.log('scope_escalation_detected', {
      userId: userId,
      resource: 'security_monitoring',
      metadata: {
        new_scopes: newScopes,
        previous_max_scopes: scopeEvolution.previousMaxScopes,
        escalated_scopes: scopeEvolution.escalatedScopes,
        escalation_severity: scopeEvolution.severity,
        time_since_last_escalation_days: scopeEvolution.daysSinceLastEscalation,
        user_role: scopeEvolution.userRole
      }
    });
  }
}
        

JWT-Specific Security Logging

JWT Validation Events

// Comprehensive JWT validation logging
function validateAndLogJWT(token, req) {
  const validationStart = Date.now();
  const decoded = jwt.decode(token, { complete: true });
  
  logger.log('jwt_validation_started', {
    userId: null, // Don't know yet
    resource: 'jwt_validation',
    metadata: {
      jwt_header_alg: decoded?.header?.alg,
      jwt_header_typ: decoded?.header?.typ,
      jwt_issuer: decoded?.payload?.iss,
      jwt_audience: decoded?.payload?.aud,
      jwt_subject: decoded?.payload?.sub,
      jwt_expires: decoded?.payload?.exp,
      jwt_issued_at: decoded?.payload?.iat,
      jwt_not_before: decoded?.payload?.nbf,
      token_age_seconds: decoded?.payload?.iat ? Math.floor(Date.now() / 1000) - decoded.payload.iat : null,
      validation_ip: req.ip,
      validation_user_agent: req.get('User-Agent')
    },
    ipAddress: req.ip,
    userAgent: req.get('User-Agent')
  });
  
  try {
    const verified = jwt.verify(token, getPublicKey(), {
      issuer: process.env.JWT_ISSUER,
      audience: process.env.JWT_AUDIENCE,
      algorithms: ['RS256']
    });
    
    const validationTime = Date.now() - validationStart;
    
    logger.log('jwt_validation_success', {
      userId: verified.sub,
      resource: 'jwt_validation',
      metadata: {
        jwt_id: verified.jti,
        jwt_subject: verified.sub,
        jwt_scopes: verified.scope?.split(' ') || [],
        validation_time_ms: validationTime,
        token_age_seconds: Math.floor(Date.now() / 1000) - verified.iat,
        time_until_expiry_seconds: verified.exp - Math.floor(Date.now() / 1000),
        client_id: verified.client_id,
        issuer_verified: true,
        audience_verified: true,
        signature_verified: true
      },
      ipAddress: req.ip,
      userAgent: req.get('User-Agent')
    });
    
    return verified;
    
  } catch (error) {
    const validationTime = Date.now() - validationStart;
    
    logger.log('jwt_validation_failed', {
      userId: decoded?.payload?.sub || null,
      resource: 'jwt_validation',
      metadata: {
        error_type: error.name,
        error_message: error.message,
        validation_time_ms: validationTime,
        jwt_expired: error.name === 'TokenExpiredError',
        jwt_malformed: error.name === 'JsonWebTokenError',
        jwt_signature_invalid: error.message.includes('signature'),
        jwt_issuer_mismatch: error.message.includes('issuer'),
        jwt_audience_mismatch: error.message.includes('audience'),
        token_provided: !!token,
        token_format_valid: !!decoded
      },
      ipAddress: req.ip,
      userAgent: req.get('User-Agent')
    });
    
    throw error;
  }
}
        

JWT Anomaly Detection

// Detect JWT reuse patterns
async function detectJWTReuse(jwtId, currentIP, userAgent) {
  const recentUsage = await getJWTUsageHistory(jwtId, 3600); // Last hour
  
  if (recentUsage.length > 1) {
    const ipVariations = [...new Set(recentUsage.map(u => u.ip))];
    const userAgentVariations = [...new Set(recentUsage.map(u => u.userAgent))];
    
    if (ipVariations.length > 1 || userAgentVariations.length > 1) {
      logger.log('jwt_reuse_anomaly', {
        userId: recentUsage[0].userId,
        resource: 'security_violation',
        metadata: {
          jwt_id: jwtId,
          usage_count_1h: recentUsage.length,
          unique_ips: ipVariations,
          unique_user_agents: userAgentVariations.length,
          current_ip: currentIP,
          current_user_agent: userAgent,
          geographic_spread_km: calculateGeographicSpread(ipVariations),
          violation_type: 'jwt_reuse_across_devices'
        },
        ipAddress: currentIP,
        userAgent: userAgent
      });
    }
  }
}

// Monitor for weak JWT configurations
function auditJWTConfiguration(decoded) {
  const securityIssues = {
    weak_algorithm: !['RS256', 'ES256', 'PS256'].includes(decoded.header.alg),
    no_expiration: !decoded.payload.exp,
    long_expiration: decoded.payload.exp && (decoded.payload.exp - decoded.payload.iat) > 86400, // > 24 hours
    no_audience: !decoded.payload.aud,
    no_issuer: !decoded.payload.iss,
    symmetric_algorithm: ['HS256', 'HS384', 'HS512'].includes(decoded.header.alg)
  };
  
  if (Object.values(securityIssues).some(Boolean)) {
    logger.log('jwt_security_issue_detected', {
      userId: decoded.payload.sub,
      resource: 'jwt_security_audit',
      metadata: {
        jwt_id: decoded.payload.jti,
        security_issues: securityIssues,
        algorithm_used: decoded.header.alg,
        token_lifetime_seconds: decoded.payload.exp - decoded.payload.iat,
        issuer_present: !!decoded.payload.iss,
        audience_present: !!decoded.payload.aud,
        issue_severity: calculateSecurityIssueSeverity(securityIssues)
      }
    });
  }
}
        

Real-Time OAuth/JWT Security Alerts

// Token replay detection
{
  eventType: "token_replay_suspected",
  threshold: 1,
  timeWindow: 1,
  alertType: "critical",
  description: "Possible token replay attack - same token used from multiple locations"
}

// Refresh token abuse
{
  eventType: "refresh_token_abuse_detected",
  threshold: 1,
  timeWindow: 5,
  alertType: "warning",
  description: "Suspicious refresh token usage pattern detected"
}

// Scope violation attempts
{
  eventType: "scope_violation_attempt",
  threshold: 5,
  timeWindow: 10,
  alertType: "warning",
  description: "Multiple attempts to access unauthorized scopes"
}

// JWT validation failures
{
  eventType: "jwt_validation_failed",
  metadata_filter: {
    jwt_signature_invalid: true
  },
  threshold: 10,
  timeWindow: 5,
  alertType: "critical",
  description: "High rate of JWT signature validation failures"
}

// Geographic anomalies
{
  eventType: "oauth_token_exchange",
  metadata_filter: {
    location_match: false,
    geographic_distance_km: { ">": 1000 }
  },
  threshold: 1,
  timeWindow: 1,
  alertType: "warning",
  description: "Token exchange from geographically distant location"
}
        

OAuth/JWT Security Analytics

Token Lifecycle Analysis

-- Analyze token usage patterns
SELECT 
  metadata->>'client_id' as client,
  AVG((metadata->>'token_age_minutes')::int) as avg_token_age,
  COUNT(*) as usage_count,
  COUNT(DISTINCT metadata->>'access_token_id') as unique_tokens,
  COUNT(DISTINCT ip_address) as unique_ips
FROM log_events 
WHERE event_type = 'access_token_used'
  AND created_date >= NOW() - INTERVAL '7 days'
GROUP BY metadata->>'client_id'
ORDER BY usage_count DESC;
        

Security Violation Tracking

-- Track security violations by type
SELECT 
  metadata->>'violation_type' as violation,
  COUNT(*) as occurrence_count,
  COUNT(DISTINCT user_id) as affected_users,
  COUNT(DISTINCT ip_address) as source_ips,
  MAX(created_date) as last_occurrence
FROM log_events 
WHERE event_type IN ('token_replay_suspected', 'scope_violation_attempt', 'refresh_token_abuse_detected')
  AND created_date >= NOW() - INTERVAL '30 days'
GROUP BY metadata->>'violation_type'
ORDER BY occurrence_count DESC;
        

Implementation Strategy

Week 1: Basic OAuth Flow Logging

  • Implement authorization request/response logging
  • Add token exchange event tracking
  • Set up basic token usage logging

Week 2: Security Event Detection

  • Add geographic anomaly detection
  • Implement token replay monitoring
  • Set up scope violation tracking

Week 3: JWT Security Enhancement

  • Add comprehensive JWT validation logging
  • Implement JWT reuse detection
  • Set up configuration security auditing

Week 4: Advanced Analytics and Alerts

  • Create real-time security alerts
  • Build security analytics dashboards
  • Implement automated response workflows

The Security Advantage

Organizations with comprehensive OAuth/JWT logging typically see:

  • 78% faster detection of token-based attacks
  • 64% reduction in successful account takeovers
  • 45% improvement in OAuth flow security posture
  • 89% faster incident response for authentication issues

More importantly, they gain visibility into attack patterns that traditional authentication logging completely misses.

Your OAuth Security Story

Every OAuth flow tells a story: a user requested access, tokens were issued and used, and either everything worked securely or someone exploited the system. The question is whether your logs can tell that story completely enough to distinguish between legitimate use and sophisticated attacks.

OAuth and JWT security isn’t just about implementing the specifications correctly—it’s about monitoring how those implementations behave in the real world. Start logging the complete token lifecycle, not just the authentication outcomes.


Ready to secure your OAuth and JWT implementation with comprehensive logging? Trailonix provides OAuth-aware security logging with built-in anomaly detection and real-time threat monitoring. Start free and protect your authentication flows from sophisticated attacks.