Skip to content

Modern API Security: OAuth 2.1 and Beyond

Introduction: The Evolution of API Security

In 2023, the Verizon Data Breach Investigations Report revealed a sobering reality: API-related breaches increased by 200% between 2021 and 2023, with authentication and authorization flaws consistently ranking as the primary attack vector. For security engineers and API developers, this statistic isn't just a number—it's a wake-up call that our authentication infrastructure needs serious attention.

Consider Sarah, a senior security engineer at a fintech startup. Her team inherited a microservices architecture built on OAuth 2.0, but the implementation was a patchwork of different flows, inconsistent security practices, and deprecated patterns that had accumulated over years. The Implicit Flow was still in use for their single-page applications, refresh tokens never rotated, and PKCE was "optional" in their documentation. When a security audit flagged these issues, Sarah faced a daunting question: "How do we modernize our API security without breaking everything?"

This scenario plays out daily across organizations worldwide. OAuth 2.0's flexibility—once considered a strength—created a security minefield. The specification offered multiple grant types and left critical security decisions as optional recommendations. The result? Developers made insecure choices, often unknowingly, and attackers exploited the gaps.

Why This Matters Now:

The numbers tell a compelling story. According to recent industry research, 83% of web traffic is now API-driven. The average enterprise manages over 15,000 APIs, each representing a potential security boundary. As organizations accelerate their digital transformation and adopt microservices architectures, the attack surface has expanded exponentially. OAuth 2.1 arrives at a critical moment, consolidating a decade of hard-learned security lessons into a streamlined, secure-by-default framework.

What You'll Learn:

In this comprehensive guide, we'll explore how OAuth 2.1 transforms API security from a complex maze of optional best practices into a clear, enforceable standard. You'll discover:

  • Core architectural differences between OAuth 2.0 and 2.1, and why these changes eliminate entire categories of vulnerabilities
  • Mandatory security enhancements including universal PKCE requirements and improved token handling
  • Emerging standards that extend beyond OAuth 2.1, including Financial-grade API (FAPI) and Demonstrating Proof-of-Possession (DPoP)
  • Practical implementation patterns with production-ready code examples in multiple languages
  • Real-world architecture patterns for SPAs, mobile apps, and microservices
  • Security-first development practices that prevent common vulnerabilities

Whether you're securing a new API, modernizing legacy authentication systems, or preparing for a security audit, this guide provides the technical depth and practical insights you need. Let's explore how OAuth 2.1 transforms API security from a complex challenge into a manageable, secure foundation for modern applications.


Understanding OAuth 2.1: What Changed and Why

The OAuth 2.1 Security Mandate

OAuth 2.1 isn't a revolutionary reimagining of API security—it's something more important: a consolidation of battle-tested security practices into a mandatory baseline. While OAuth 2.0 served as the foundation for modern API authentication, its flexibility proved to be a double-edged sword.

The original OAuth 2.0 specification (RFC 6749), published in 2012, was designed for maximum flexibility. It offered multiple grant types to accommodate various use cases and left many security considerations as recommendations rather than requirements. This approach made sense at the time—the API ecosystem was still evolving, and the specification needed to support diverse scenarios.

However, over the following decade, a pattern emerged. Security researchers identified vulnerabilities, attackers exploited weak implementations, and the OAuth working group published numerous Best Current Practice (BCP) documents and security extensions. Critical RFCs like RFC 7636 (PKCE), RFC 8252 (OAuth for Native Apps), and the OAuth 2.0 Security Best Current Practice became essential reading, but they existed as separate documents that developers might overlook.

OAuth 2.1 addresses this security debt by incorporating these critical enhancements directly into the core specification. Published as a draft in 2020 and continuously refined, OAuth 2.1 represents the industry's collective security wisdom distilled into mandatory requirements. The philosophy shift is clear: secure-by-default rather than secure-if-configured-correctly.

Critical Changes from OAuth 2.0

1. Removed Grant Types: Eliminating Dangerous Patterns

The most visible change in OAuth 2.1 is the removal of two grant types that proved consistently problematic in production environments.

Implicit Flow Elimination:

The Implicit Flow was designed for browser-based applications, returning access tokens directly in the URL fragment after user authorization. This seemed convenient—no backend required, tokens available immediately in JavaScript. However, this convenience came at a severe security cost.

The fundamental problem: tokens exposed in URLs are vulnerable to leakage through multiple vectors. Browser history, referrer headers, proxy logs, and even shoulder-surfing attacks could expose tokens. In 2019, researchers demonstrated that the Implicit Flow was vulnerable to token theft through malicious browser extensions, a threat model that didn't exist when OAuth 2.0 was designed.

Real-world breach example: A major social media platform using the Implicit Flow for third-party integrations experienced a breach when an attacker registered a malicious OAuth client with a carefully crafted redirect URI. The platform's validation allowed a subdomain takeover attack, and access tokens were harvested from thousands of users. The incident could have been prevented with the Authorization Code Flow and PKCE.

Resource Owner Password Credentials (ROPC) Removal:

The ROPC flow allowed applications to collect user credentials directly and exchange them for tokens. While this simplified migration from legacy authentication systems, it fundamentally violated OAuth's principle of delegated authorization. Users had to trust the client application with their credentials, eliminating the security boundary OAuth was designed to protect.

OAuth 2.1 removes ROPC entirely, forcing developers toward more secure patterns. For legacy migration scenarios, the specification recommends using the Authorization Code Flow with a simplified consent screen or implementing a proper identity provider federation.

2. PKCE Now Mandatory: Universal Protection Against Interception

Proof Key for Code Exchange (PKCE, pronounced "pixie") was originally designed to protect mobile applications from authorization code interception attacks. OAuth 2.1 makes PKCE mandatory for ALL clients, including confidential clients with client secrets.

Why Universal PKCE Matters:

The authorization code interception attack works like this: an attacker intercepts the authorization code during the redirect back to the application and attempts to exchange it for tokens before the legitimate client can. In the original OAuth 2.0, public clients (mobile apps, SPAs) were vulnerable because they couldn't securely store a client secret.

PKCE solves this elegantly through a cryptographic challenge-response mechanism:

  1. The client generates a random code_verifier (high-entropy random string)
  2. It creates a code_challenge by hashing the verifier: code_challenge = SHA256(code_verifier)
  3. The authorization request includes the code_challenge
  4. When exchanging the authorization code for tokens, the client proves it initiated the flow by presenting the original code_verifier
  5. The authorization server verifies: SHA256(code_verifier) == stored_code_challenge

Only the client that started the flow possesses the verifier, making intercepted authorization codes useless to attackers.

OAuth 2.1's mandate extends PKCE to confidential clients as defense-in-depth. Even if a client secret is compromised, PKCE provides an additional layer of protection. This acknowledges a reality of modern development: secrets sometimes leak through misconfigured CI/CD pipelines, container images, or source code repositories.

3. Enhanced Security Requirements: Tightening the Baseline

Beyond removing dangerous flows and mandating PKCE, OAuth 2.1 upgrades several security requirements from recommendations to mandates:

Exact Redirect URI Matching:

OAuth 2.0 allowed flexible redirect URI matching, which led to numerous vulnerabilities. Attackers exploited loose matching to redirect authorization codes to malicious endpoints. OAuth 2.1 requires exact string matching for redirect URIs, with limited exceptions for localhost during development (where port numbers may vary).

Refresh Token Rotation:

Refresh tokens now must be single-use. When a client exchanges a refresh token for new access tokens, the authorization server issues a new refresh token and invalidates the old one. This rotation prevents token replay attacks and provides a mechanism to detect token theft—if an old refresh token is reused, the authorization server knows a security incident occurred and can revoke the entire token family.

Sender-Constrained Tokens:

OAuth 2.1 strongly encourages sender-constrained access tokens, where tokens are cryptographically bound to the client that obtained them. This prevents stolen tokens from being used by attackers. Implementation methods include mutual TLS (mTLS) certificate binding or the newer DPoP (Demonstrating Proof-of-Possession) mechanism, which we'll explore in detail later.

OAuth 2.0 vs 2.1: Quick Reference

Feature OAuth 2.0 OAuth 2.1
Implicit Flow Supported Removed
ROPC Flow Supported Removed
PKCE Optional (recommended for public clients) Mandatory for all clients
Redirect URI Matching Flexible Exact match required
Refresh Token Rotation Optional Mandatory
Sender-Constrained Tokens Not specified Strongly recommended
Authorization Code Lifetime Not specified Maximum 10 minutes recommended
Bearer Token Security Basic guidance Enhanced requirements

Core OAuth 2.1 Flows: What You Need to Know

With dangerous flows removed, OAuth 2.1 focuses on two primary patterns that cover virtually all legitimate use cases. Understanding these flows deeply is essential for secure implementation.

Authorization Code Flow with PKCE

The Authorization Code Flow with PKCE is now the universal standard for any scenario involving user authentication—web applications, single-page applications, mobile apps, and even desktop applications. This consolidation simplifies the OAuth landscape significantly.

When to Use:

  • User-facing web applications (both server-side and SPAs)
  • Mobile applications (iOS, Android)
  • Desktop applications
  • Any scenario where a user needs to authorize access to their resources

Step-by-Step Flow Breakdown:

Let's walk through the complete flow with security considerations at each step:

Step 1: Generate PKCE Parameters

Before initiating the authorization flow, the client generates cryptographic parameters:

// Node.js example: Generating PKCE parameters
const crypto = require

## VI. Real-World Architecture Patterns (Continued)

### A. Backend-for-Frontend (BFF) Pattern

**Architecture Overview:**

The BFF pattern has emerged as the gold standard for securing browser-based applications. Instead of managing OAuth tokens in JavaScript, a dedicated backend service handles all authentication flows:
[Browser] ←Session Cookie→ [BFF Server] ←OAuth Tokens→ [API Gateway] <---> [Microservices]
**Why BFF Solves Modern SPA Security:**

Traditional SPAs face fundamental security challenges:
- **XSS Vulnerability**: Any XSS attack can steal tokens from localStorage or sessionStorage
- **Token Exposure**: Tokens visible in browser DevTools and memory
- **CSRF Risks**: Without proper protection, tokens can be replayed

The BFF pattern eliminates these risks by:
1. Keeping OAuth tokens server-side only
2. Using HttpOnly, Secure cookies for browser sessions
3. Implementing CSRF protection naturally through SameSite cookies
4. Centralizing token refresh logic

**Implementation Example:**

Here's a practical BFF implementation using Express.js:

```javascript
// bff-server.js - Backend-for-Frontend OAuth Handler
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const axios = require('axios');

const app = express();

// Secure session configuration
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,      // Prevents JavaScript access
    secure: true,        // HTTPS only
    sameSite: 'strict',  // CSRF protection
    maxAge: 3600000      // 1 hour
  }
}));

// Initiate OAuth flow
app.get('/auth/login', (req, res) => {
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = generateCodeChallenge(codeVerifier);

  // Store verifier in session (server-side)
  req.session.codeVerifier = codeVerifier;
  req.session.returnTo = req.query.returnTo || '/';

  const authUrl = `${AUTH_SERVER}/authorize?` +
    `client_id=${CLIENT_ID}&` +
    `redirect_uri=${REDIRECT_URI}&` +
    `response_type=code&` +
    `code_challenge=${codeChallenge}&` +
    `code_challenge_method=S256&` +
    `scope=openid profile email api:read`;

  res.redirect(authUrl);
});

// OAuth callback handler
app.get('/auth/callback', async (req, res) => {
  const { code } = req.query;
  const codeVerifier = req.session.codeVerifier;

  try {
    // Exchange code for tokens
    const tokenResponse = await axios.post(`${AUTH_SERVER}/token`, {
      grant_type: 'authorization_code',
      code: code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      code_verifier: codeVerifier
    });

    // Store tokens in secure session (Redis)
    req.session.accessToken = tokenResponse.data.access_token;
    req.session.refreshToken = tokenResponse.data.refresh_token;
    req.session.expiresAt = Date.now() + (tokenResponse.data.expires_in * 1000);

    // Clear PKCE verifier
    delete req.session.codeVerifier;

    res.redirect(req.session.returnTo);
  } catch (error) {
    console.error('Token exchange failed:', error);
    res.redirect('/login?error=auth_failed');
  }
});

// API proxy with automatic token refresh
app.use('/api/*', async (req, res, next) => {
  if (!req.session.accessToken) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  // Check if token needs refresh
  if (Date.now() >= req.session.expiresAt - 60000) { // Refresh 1 min before expiry
    try {
      const refreshResponse = await axios.post(`${AUTH_SERVER}/token`, {
        grant_type: 'refresh_token',
        refresh_token: req.session.refreshToken,
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET
      });

      req.session.accessToken = refreshResponse.data.access_token;
      req.session.refreshToken = refreshResponse.data.refresh_token;
      req.session.expiresAt = Date.now() + (refreshResponse.data.expires_in * 1000);
    } catch (error) {
      req.session.destroy();
      return res.status(401).json({ error: 'Session expired' });
    }
  }

  // Forward request to actual API
  const apiPath = req.path.replace('/api', '');
  try {
    const apiResponse = await axios({
      method: req.method,
      url: `${API_BASE_URL}${apiPath}`,
      headers: {
        'Authorization': `Bearer ${req.session.accessToken}`,
        'Content-Type': 'application/json'
      },
      data: req.body
    });

    res.json(apiResponse.data);
  } catch (error) {
    res.status(error.response?.status || 500).json({
      error: error.response?.data || 'API request failed'
    });
  }
});

Key Benefits of This Pattern:

  1. Zero Token Exposure: Browser never sees OAuth tokens
  2. Automatic Refresh: Server handles token lifecycle transparently
  3. Simplified Frontend: JavaScript only manages UI, not security
  4. Centralized Security: Single point for security policies

B. Mobile Application Pattern

Native Mobile OAuth Best Practices:

Mobile applications require special consideration due to their unique security context. OAuth 2.1 mandates specific patterns for mobile clients:

Architecture Components:

[Mobile App] <---> [System Browser/ASWebAuthenticationSession] <---> [Auth Server]
     |
     v
[OS Secure Storage: Keychain/Keystore]

Critical Requirements:

  1. Use System Browser: Never use embedded WebViews for OAuth
  2. iOS: ASWebAuthenticationSession
  3. Android: Chrome Custom Tabs
  4. Reason: Prevents phishing, shares SSO sessions

  5. App-Claimed HTTPS Schemes: Use universal links/app links

  6. iOS: https://yourapp.com/callback
  7. Android: https://yourapp.com/callback
  8. Fallback: Custom schemes with domain verification

  9. Secure Token Storage:

  10. iOS: Store in Keychain with appropriate accessibility levels
  11. Android: Use EncryptedSharedPreferences or Keystore

iOS Implementation Example:

import AuthenticationServices

class OAuthManager {
    private let authURL = "https://auth.example.com/authorize"
    private let tokenURL = "https://auth.example.com/token"
    private let clientId = "mobile-app-client"
    private let redirectURI = "https://yourapp.com/callback"

    func login(from viewController: UIViewController) {
        // Generate PKCE parameters
        let codeVerifier = generateCodeVerifier()
        let codeChallenge = generateCodeChallenge(from: codeVerifier)

        // Store verifier securely
        KeychainHelper.save(codeVerifier, forKey: "pkce_verifier")

        // Build authorization URL
        var components = URLComponents(string: authURL)!
        components.queryItems = [
            URLQueryItem(name: "client_id", value: clientId),
            URLQueryItem(name: "redirect_uri", value: redirectURI),
            URLQueryItem(name: "response_type", value: "code"),
            URLQueryItem(name: "code_challenge", value: codeChallenge),
            URLQueryItem(name: "code_challenge_method", value: "S256"),
            URLQueryItem(name: "scope", value: "openid profile api:read")
        ]

        // Present authentication session
        let session = ASWebAuthenticationSession(
            url: components.url!,
            callbackURLScheme: "https"
        ) { callbackURL, error in
            guard let callbackURL = callbackURL,
                  let code = self.extractCode(from: callbackURL) else {
                return
            }

            self.exchangeCodeForTokens(code: code)
        }

        session.presentationContextProvider = viewController
        session.prefersEphemeralWebBrowserSession = false // Allow SSO
        session.start()
    }

    private func exchangeCodeForTokens(code: String) {
        guard let codeVerifier = KeychainHelper.load(forKey: "pkce_verifier") else {
            return
        }

        let parameters = [
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": redirectURI,
            "client_id": clientId,
            "code_verifier": codeVerifier
        ]

        // Make token request
        NetworkManager.post(tokenURL, parameters: parameters) { result in
            switch result {
            case .success(let tokens):
                // Store tokens securely in Keychain
                KeychainHelper.save(tokens.accessToken, forKey: "access_token")
                KeychainHelper.save(tokens.refreshToken, forKey: "refresh_token")

                // Clean up PKCE verifier
                KeychainHelper.delete(forKey: "pkce_verifier")

                NotificationCenter.default.post(name: .userDidLogin, object: nil)

            case .failure(let error):
                print("

## VI. Real-World Architecture Patterns (Continued)

### A. Backend-for-Frontend (BFF) Pattern

**Architecture Overview:**
[Browser] ←Session Cookie→ [BFF] ←OAuth Tokens→ [API]
**Why BFF?**
- Tokens never exposed to browser
- Server-side token refresh
- XSS attack surface eliminated
- Simplified client-side code

**Implementation Example (Node.js/Express):**

```javascript
// BFF endpoint handling token management
app.get('/api/user/profile', isAuthenticated, async (req, res) => {
  try {
    // Retrieve access token from secure session
    let accessToken = req.session.accessToken;

    // Check token expiration
    if (isTokenExpired(accessToken)) {
      accessToken = await refreshAccessToken(req.session.refreshToken);
      req.session.accessToken = accessToken;
    }

    // Make API request with token
    const response = await fetch('https://api.example.com/user/profile', {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    });

    const data = await response.json();
    res.json(data);

  } catch (error) {
    if (error.status === 401) {
      // Refresh token expired, require re-authentication
      req.session.destroy();
      return res.status(401).json({ error: 'Session expired' });
    }
    res.status(500).json({ error: 'Internal server error' });
  }
});

Key Benefits: - Session cookies are HttpOnly and SameSite - Token refresh handled transparently - API calls proxied through trusted backend - Centralized security policy enforcement

B. Microservices Authentication Pattern

Challenge: Propagating authentication across service boundaries

Solution: Token Exchange Pattern (RFC 8693)

# Gateway service exchanges user token for service-specific token
def call_downstream_service(user_token, target_service):
    """Exchange user token for service-scoped token"""

    exchange_response = requests.post(
        f"{AUTH_SERVER}/token",
        data={
            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
            "subject_token": user_token,
            "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
            "audience": target_service,
            "scope": "read:orders"
        },
        auth=(CLIENT_ID, CLIENT_SECRET)
    )

    service_token = exchange_response.json()["access_token"]

    # Call downstream service with scoped token
    return requests.get(
        f"https://{target_service}/api/orders",
        headers={"Authorization": f"Bearer {service_token}"}
    )

Architecture Benefits: - Each service receives appropriately scoped token - Audit trail maintained across services - Prevents token over-privileging - Supports zero-trust architecture

C. Mobile App with Biometric Authentication

Pattern: Combine OAuth with platform biometrics for enhanced UX

Implementation Strategy:

  1. Initial OAuth flow - Full authentication with PKCE
  2. Secure token storage - Platform keychain with biometric protection
  3. Biometric unlock - Access tokens without re-authentication
  4. Periodic re-authentication - Full OAuth flow every 30 days

iOS Example:

class SecureTokenManager {

    func storeTokenWithBiometrics(token: String, key: String) {
        let access = SecAccessControlCreateWithFlags(
            nil,
            kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
            .biometryCurrentSet,  // Requires biometric auth
            nil
        )

        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecValueData as String: token.data(using: .utf8)!,
            kSecAttrAccessControl as String: access as Any
        ]

        SecItemAdd(query as CFDictionary, nil)
    }

    func retrieveTokenWithBiometrics(key: String) async throws -> String {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecUseOperationPrompt as String: "Authenticate to access your account"
        ]

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        guard status == errSecSuccess,
              let data = result as? Data,
              let token = String(data: data, encoding: .utf8) else {
            throw TokenError.retrievalFailed
        }

        return token
    }
}

Security Considerations: - Biometrics protect local access only - Tokens still expire on server side - Device compromise requires full re-authentication - Supports remote token revocation


VII. Monitoring and Incident Response

A. Security Observability

Critical Metrics to Track:

  1. Authentication Patterns
  2. Failed login attempts per user/IP
  3. Unusual geographic access patterns
  4. Token refresh frequency anomalies
  5. PKCE validation failures

  6. Token Lifecycle Events

  7. Token issuance rate by client
  8. Average token lifetime before refresh
  9. Revocation events and reasons
  10. Expired token usage attempts

  11. API Access Patterns

  12. Requests per token (detect token sharing)
  13. Scope escalation attempts
  14. Rate limit violations
  15. Unusual endpoint access patterns

Implementation Example (Prometheus metrics):

from prometheus_client import Counter, Histogram, Gauge

# Define metrics
token_issued = Counter('oauth_tokens_issued_total', 
                       'Total tokens issued', 
                       ['client_id', 'grant_type'])

token_refresh_duration = Histogram('oauth_token_refresh_duration_seconds',
                                   'Token refresh operation duration')

active_tokens = Gauge('oauth_active_tokens',
                      'Currently active access tokens',
                      ['client_id'])

failed_auth = Counter('oauth_failed_authentications_total',
                      'Failed authentication attempts',
                      ['client_id', 'error_type'])

# Usage in token endpoint
@app.route('/token', methods=['POST'])
def token_endpoint():
    client_id = request.form.get('client_id')
    grant_type = request.form.get('grant_type')

    try:
        with token_refresh_duration.time():
            token = issue_token(request.form)

        token_issued.labels(client_id=client_id, grant_type=grant_type).inc()
        active_tokens.labels(client_id=client_id).inc()

        return jsonify(token)

    except AuthenticationError as e:
        failed_auth.labels(client_id=client_id, error_type=e.type).inc()
        return jsonify({'error': 'invalid_grant'}), 401

B. Incident Response Playbook

Token Compromise Scenarios:

Scenario 1: Stolen Access Token - Detection: Concurrent usage from different IPs/devices - Response: 1. Revoke specific access token immediately 2. Invalidate associated refresh token 3. Force user re-authentication 4. Audit recent API calls with compromised token 5. Notify user of suspicious activity

Scenario 2: Client Secret Leak - Detection: Secret appears in public repository, logs, or breach - Response: 1. Rotate client secret immediately 2. Revoke all tokens issued to that client 3. Audit token usage patterns for abuse 4. Notify affected users 5. Implement secret scanning in CI/CD

Scenario 3: Authorization Server Breach - Detection: Unauthorized database access, unusual admin activity - Response: 1. Revoke ALL tokens system-wide 2. Force re-authentication for all users 3. Rotate all client secrets 4. Conduct full security audit 5. Implement enhanced monitoring 6. Notify users per breach notification requirements

Automated Response Actions:

class SecurityIncidentHandler:

    def handle_token_compromise(self, token_id: str):
        """Automated response to token compromise"""

        # 1. Immediate revocation
        self.revoke_token(token_id)

        # 2. Get associated tokens
        user_id = self.get_user_from_token(token_id)
        refresh_token = self.get_refresh_token(token_id)

        # 3. Revoke refresh token
        if refresh_token:
            self.revoke_token(refresh_token)

        # 4. Log security event
        self.log_security_event({
            'event_type': 'token_compromise',
            'token_id': token_id,
            'user_id': user_id,
            'timestamp': datetime.utcnow(),
            'action_taken': 'full_revocation'
        })

        # 5. Trigger user notification
        self.notify_user(user_id, 'security_alert')

        # 6. Create incident ticket
        self.create_incident_ticket({
            'severity': 'high',
            'type': 'token_compromise',
            'affected_user': user_id
        })

VIII. Migration Strategy: From OAuth 2.0 to 2.1

Phased Migration Approach

Phase 1: Assessment (Week 1-2) - Inventory all OAuth 2.0 implementations - Identify deprecated grant types in use -