Use Case

OAuth PKCE Authentication

Secure OAuth 2.0 authentication with PKCE for public clients and enhanced security

What is OAuth PKCE?

OAuth 2.0 with PKCE (Proof Key for Code Exchange) is an extension that makes the OAuth flow more secure for public clients like mobile apps, SPAs, and desktop applications. It prevents authorization code interception attacks by introducing a dynamically generated code verifier and challenge.

Mobile Apps

Secure authentication without client secrets

SPAs

Browser-based apps with enhanced security

Zero Trust

No client secrets stored in public code

PKCE Flow Overview

1

Generate Code Verifier & Challenge

Client creates a random code verifier and derives a challenge using SHA256

2

Authorization Request

Client redirects to authorization endpoint with code challenge

3

User Authorization

User authenticates and approves the authorization request

4

Token Exchange

Client exchanges authorization code + code verifier for tokens

Complete PKCE Implementationjavascript
// 1. Generate PKCE parameters
function generatePKCEParams() {
  // Generate code verifier (43-128 characters)
  const codeVerifier = generateRandomString(128);
  
  // Generate code challenge using SHA256
  const encoder = new TextEncoder();
  const data = encoder.encode(codeVerifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  
  // Base64url encode the challenge
  const codeChallenge = base64urlEncode(digest);
  
  // Store verifier for later use
  sessionStorage.setItem('pkce_verifier', codeVerifier);
  
  return {
    codeVerifier,
    codeChallenge,
    challengeMethod: 'S256'
  };
}

// 2. Build authorization URL
async function startOAuthFlow() {
  const { codeChallenge, challengeMethod } = await generatePKCEParams();
  
  const params = new URLSearchParams({
    client_id: 'your-client-id',
    redirect_uri: 'https://app.example.com/callback',
    response_type: 'code',
    scope: 'openid profile email api.read api.write',
    state: generateRandomString(16), // CSRF protection
    code_challenge: codeChallenge,
    code_challenge_method: challengeMethod,
    
    // Optional parameters
    prompt: 'consent',  // Force consent screen
    max_age: 300,       // Require recent authentication
    ui_locales: 'en',   // Preferred language
  });
  
  // Redirect to authorization endpoint
  window.location.href = `https://api.parrotrouter.com/oauth/authorize?${params}`;
}

// 3. Handle callback and exchange code for tokens
async function handleCallback() {
  const urlParams = new URLSearchParams(window.location.search);
  const code = urlParams.get('code');
  const state = urlParams.get('state');
  
  // Verify state parameter (CSRF protection)
  const savedState = sessionStorage.getItem('oauth_state');
  if (state !== savedState) {
    throw new Error('Invalid state parameter');
  }
  
  // Get stored code verifier
  const codeVerifier = sessionStorage.getItem('pkce_verifier');
  if (!codeVerifier) {
    throw new Error('Missing PKCE code verifier');
  }
  
  // Exchange code for tokens
  const response = await fetch('https://api.parrotrouter.com/oauth/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      client_id: 'your-client-id',
      code: code,
      redirect_uri: 'https://app.example.com/callback',
      code_verifier: codeVerifier, // PKCE verification
    }),
  });
  
  const tokens = await response.json();
  
  // Clear PKCE data
  sessionStorage.removeItem('pkce_verifier');
  sessionStorage.removeItem('oauth_state');
  
  // Store tokens securely
  await storeTokensSecurely(tokens);
  
  return tokens;
}

// Helper functions
function generateRandomString(length) {
  const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
  const values = crypto.getRandomValues(new Uint8Array(length));
  return Array.from(values)
    .map(x => charset[x % charset.length])
    .join('');
}

function base64urlEncode(buffer) {
  const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
  return base64
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

Mobile App Implementation

Implement PKCE in native mobile applications using platform-specific best practices:

iOS (Swift)

import AuthenticationServices
import CryptoKit

class OAuthManager {
    private var currentAuthSession: ASWebAuthenticationSession?
    private var codeVerifier: String?
    
    func authenticate() {
        // Generate PKCE parameters
        let (verifier, challenge) = generatePKCEPair()
        self.codeVerifier = verifier
        
        // Build authorization URL
        var components = URLComponents(string: "https://api.parrotrouter.com/oauth/authorize")!
        components.queryItems = [
            URLQueryItem(name: "client_id", value: "your-ios-client-id"),
            URLQueryItem(name: "redirect_uri", value: "com.example.app://oauth/callback"),
            URLQueryItem(name: "response_type", value: "code"),
            URLQueryItem(name: "scope", value: "openid profile api.read"),
            URLQueryItem(name: "code_challenge", value: challenge),
            URLQueryItem(name: "code_challenge_method", value: "S256"),
            URLQueryItem(name: "state", value: UUID().uuidString)
        ]
        
        // Start authentication session
        let session = ASWebAuthenticationSession(
            url: components.url!,
            callbackURLScheme: "com.example.app"
        ) { callbackURL, error in
            guard error == nil, let callbackURL = callbackURL else {
                print("Authentication error: \(error?.localizedDescription ?? "Unknown")")
                return
            }
            
            self.handleCallback(url: callbackURL)
        }
        
        session.presentationContextProvider = self
        session.prefersEphemeralWebBrowserSession = false // Allow SSO
        session.start()
        
        self.currentAuthSession = session
    }
    
    private func generatePKCEPair() -> (verifier: String, challenge: String) {
        let verifier = generateRandomString(length: 128)
        let data = verifier.data(using: .utf8)!
        let hash = SHA256.hash(data: data)
        let challenge = Data(hash)
            .base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
        
        return (verifier, challenge)
    }
    
    private func handleCallback(url: URL) {
        let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
        guard let code = components?.queryItems?.first(where: { $0.name == "code" })?.value else {
            print("No authorization code found")
            return
        }
        
        // Exchange code for tokens
        exchangeCodeForTokens(code: code)
    }
    
    private func exchangeCodeForTokens(code: String) {
        guard let verifier = self.codeVerifier else { return }
        
        let tokenURL = URL(string: "https://api.parrotrouter.com/oauth/token")!
        var request = URLRequest(url: tokenURL)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body = [
            "grant_type": "authorization_code",
            "client_id": "your-ios-client-id",
            "code": code,
            "redirect_uri": "com.example.app://oauth/callback",
            "code_verifier": verifier
        ]
        
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            guard let data = data else { return }
            
            if let tokens = try? JSONDecoder().decode(TokenResponse.self, from: data) {
                // Store tokens in keychain
                KeychainHelper.store(tokens: tokens)
                
                DispatchQueue.main.async {
                    // Navigate to authenticated state
                    self.onAuthenticationSuccess()
                }
            }
        }.resume()
    }
}

Android (Kotlin)

import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64

class OAuthManager(private val activity: AppCompatActivity) {
    private var codeVerifier: String? = null
    
    companion object {
        private const val REDIRECT_URI = "com.example.app://oauth/callback"
        private const val AUTH_ENDPOINT = "https://api.parrotrouter.com/oauth/authorize"
        private const val TOKEN_ENDPOINT = "https://api.parrotrouter.com/oauth/token"
    }
    
    fun startAuthentication() {
        // Generate PKCE parameters
        val (verifier, challenge) = generatePKCEPair()
        codeVerifier = verifier
        
        // Save state for CSRF protection
        val state = generateRandomString(16)
        saveState(state)
        
        // Build authorization URL
        val authUri = Uri.parse(AUTH_ENDPOINT).buildUpon()
            .appendQueryParameter("client_id", "your-android-client-id")
            .appendQueryParameter("redirect_uri", REDIRECT_URI)
            .appendQueryParameter("response_type", "code")
            .appendQueryParameter("scope", "openid profile api.read")
            .appendQueryParameter("code_challenge", challenge)
            .appendQueryParameter("code_challenge_method", "S256")
            .appendQueryParameter("state", state)
            .build()
        
        // Launch Chrome Custom Tab
        val customTabsIntent = CustomTabsIntent.Builder()
            .setShowTitle(true)
            .setToolbarColor(activity.getColor(R.color.primary))
            .build()
            
        customTabsIntent.launchUrl(activity, authUri)
    }
    
    fun handleAuthorizationResponse(uri: Uri) {
        val code = uri.getQueryParameter("code")
        val state = uri.getQueryParameter("state")
        
        // Verify state
        if (state != getSavedState()) {
            throw SecurityException("Invalid state parameter")
        }
        
        code?.let { exchangeCodeForTokens(it) }
    }
    
    private fun generatePKCEPair(): Pair<String, String> {
        // Generate code verifier
        val verifier = generateRandomString(128)
        
        // Generate code challenge
        val bytes = verifier.toByteArray(Charsets.US_ASCII)
        val digest = MessageDigest.getInstance("SHA-256").digest(bytes)
        val challenge = Base64.getUrlEncoder()
            .withoutPadding()
            .encodeToString(digest)
        
        return Pair(verifier, challenge)
    }
    
    private fun exchangeCodeForTokens(code: String) {
        val client = OkHttpClient()
        
        val requestBody = FormBody.Builder()
            .add("grant_type", "authorization_code")
            .add("client_id", "your-android-client-id")
            .add("code", code)
            .add("redirect_uri", REDIRECT_URI)
            .add("code_verifier", codeVerifier ?: "")
            .build()
        
        val request = Request.Builder()
            .url(TOKEN_ENDPOINT)
            .post(requestBody)
            .build()
        
        client.newCall(request).enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                if (response.isSuccessful) {
                    val tokens = parseTokenResponse(response.body?.string())
                    
                    // Store tokens securely
                    TokenManager.storeTokens(tokens)
                    
                    // Navigate to authenticated state
                    activity.runOnUiThread {
                        navigateToMainScreen()
                    }
                }
            }
            
            override fun onFailure(call: Call, e: IOException) {
                // Handle error
                handleAuthError(e)
            }
        })
    }
    
    private fun generateRandomString(length: Int): String {
        val charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
        val random = SecureRandom()
        return (1..length)
            .map { charset[random.nextInt(charset.length)] }
            .joinToString("")
    }
}

SPA Implementation

Implement PKCE in single-page applications with proper security considerations:

React SPA with PKCEjavascript
import { useEffect, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';

// OAuth configuration
const OAUTH_CONFIG = {
  clientId: 'your-spa-client-id',
  authEndpoint: 'https://api.parrotrouter.com/oauth/authorize',
  tokenEndpoint: 'https://api.parrotrouter.com/oauth/token',
  redirectUri: window.location.origin + '/callback',
  scopes: ['openid', 'profile', 'email', 'api.read', 'api.write'],
};

// PKCE OAuth Hook
export function useOAuth() {
  const navigate = useNavigate();
  const location = useLocation();
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [tokens, setTokens] = useState(null);

  // Initialize authentication
  const login = async () => {
    // Generate PKCE parameters
    const codeVerifier = generateCodeVerifier();
    const codeChallenge = await generateCodeChallenge(codeVerifier);
    
    // Generate state for CSRF protection
    const state = generateRandomString(16);
    
    // Store in session storage
    sessionStorage.setItem('pkce_code_verifier', codeVerifier);
    sessionStorage.setItem('oauth_state', state);
    
    // Build authorization URL
    const params = new URLSearchParams({
      client_id: OAUTH_CONFIG.clientId,
      redirect_uri: OAUTH_CONFIG.redirectUri,
      response_type: 'code',
      scope: OAUTH_CONFIG.scopes.join(' '),
      state: state,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256',
      
      // Additional security parameters
      nonce: generateRandomString(16), // For ID token validation
      response_mode: 'query', // or 'fragment' for implicit flow
      
      // Optional UX parameters
      prompt: 'select_account', // Force account selection
      display: 'page', // Full page display
      login_hint: 'user@example.com', // Pre-fill username
    });
    
    window.location.href = `${OAUTH_CONFIG.authEndpoint}?${params}`;
  };

  // Handle OAuth callback
  const handleCallback = async () => {
    const params = new URLSearchParams(location.search);
    const code = params.get('code');
    const state = params.get('state');
    const error = params.get('error');
    
    // Handle errors
    if (error) {
      console.error('OAuth error:', error, params.get('error_description'));
      navigate('/login?error=' + error);
      return;
    }
    
    // Verify state
    const savedState = sessionStorage.getItem('oauth_state');
    if (!state || state !== savedState) {
      console.error('Invalid state parameter');
      navigate('/login?error=invalid_state');
      return;
    }
    
    // Get code verifier
    const codeVerifier = sessionStorage.getItem('pkce_code_verifier');
    if (!codeVerifier) {
      console.error('Missing PKCE code verifier');
      navigate('/login?error=missing_verifier');
      return;
    }
    
    try {
      // Exchange code for tokens
      const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          grant_type: 'authorization_code',
          client_id: OAUTH_CONFIG.clientId,
          code: code,
          redirect_uri: OAUTH_CONFIG.redirectUri,
          code_verifier: codeVerifier,
        }),
      });
      
      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error_description || 'Token exchange failed');
      }
      
      const tokens = await response.json();
      
      // Validate ID token (if using OpenID Connect)
      if (tokens.id_token) {
        const isValid = await validateIdToken(tokens.id_token);
        if (!isValid) {
          throw new Error('Invalid ID token');
        }
      }
      
      // Store tokens securely
      await storeTokens(tokens);
      
      // Clean up session storage
      sessionStorage.removeItem('pkce_code_verifier');
      sessionStorage.removeItem('oauth_state');
      
      // Update state
      setTokens(tokens);
      setIsAuthenticated(true);
      
      // Redirect to original destination or home
      const returnTo = sessionStorage.getItem('auth_return_to') || '/';
      sessionStorage.removeItem('auth_return_to');
      navigate(returnTo);
      
    } catch (error) {
      console.error('Token exchange error:', error);
      navigate('/login?error=token_exchange_failed');
    }
  };

  // Token refresh
  const refreshToken = async () => {
    const storedTokens = getStoredTokens();
    if (!storedTokens?.refresh_token) {
      throw new Error('No refresh token available');
    }
    
    const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        grant_type: 'refresh_token',
        client_id: OAUTH_CONFIG.clientId,
        refresh_token: storedTokens.refresh_token,
      }),
    });
    
    if (!response.ok) {
      // Refresh failed, need to re-authenticate
      logout();
      throw new Error('Token refresh failed');
    }
    
    const newTokens = await response.json();
    await storeTokens(newTokens);
    setTokens(newTokens);
    
    return newTokens;
  };

  // Logout
  const logout = async () => {
    // Revoke tokens if supported
    const storedTokens = getStoredTokens();
    if (storedTokens?.access_token) {
      try {
        await fetch('https://api.parrotrouter.com/oauth/revoke', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            token: storedTokens.access_token,
            token_type_hint: 'access_token',
          }),
        });
      } catch (error) {
        console.error('Token revocation failed:', error);
      }
    }
    
    // Clear stored tokens
    clearStoredTokens();
    setTokens(null);
    setIsAuthenticated(false);
    
    // Redirect to logout endpoint (optional)
    const logoutUrl = new URL('https://api.parrotrouter.com/oauth/logout');
    logoutUrl.searchParams.set('post_logout_redirect_uri', window.location.origin);
    window.location.href = logoutUrl.toString();
  };

  // Auto-refresh tokens
  useEffect(() => {
    if (!tokens) return;
    
    const expiresIn = tokens.expires_in * 1000; // Convert to milliseconds
    const refreshBefore = 60000; // Refresh 1 minute before expiry
    
    const timeout = setTimeout(() => {
      refreshToken().catch(error => {
        console.error('Auto-refresh failed:', error);
        logout();
      });
    }, expiresIn - refreshBefore);
    
    return () => clearTimeout(timeout);
  }, [tokens]);

  // Check authentication status on mount
  useEffect(() => {
    const storedTokens = getStoredTokens();
    if (storedTokens) {
      setTokens(storedTokens);
      setIsAuthenticated(true);
    }
  }, []);

  // Handle callback route
  useEffect(() => {
    if (location.pathname === '/callback') {
      handleCallback();
    }
  }, [location]);

  return {
    isAuthenticated,
    tokens,
    login,
    logout,
    refreshToken,
  };
}

// Helper functions
function generateCodeVerifier() {
  const array = new Uint8Array(64);
  crypto.getRandomValues(array);
  return base64urlEncode(array);
}

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));
}

function base64urlEncode(array) {
  return btoa(String.fromCharCode.apply(null, array))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

// Secure token storage using IndexedDB
async function storeTokens(tokens) {
  // Encrypt tokens before storage (implementation depends on your crypto library)
  const encryptedTokens = await encryptData(tokens);
  
  // Store in IndexedDB (more secure than localStorage)
  const db = await openTokenDB();
  const tx = db.transaction(['tokens'], 'readwrite');
  await tx.objectStore('tokens').put(encryptedTokens, 'current');
}

// Protected route component
export function ProtectedRoute({ children }) {
  const { isAuthenticated } = useOAuth();
  const location = useLocation();
  
  useEffect(() => {
    if (!isAuthenticated) {
      // Save current location for redirect after auth
      sessionStorage.setItem('auth_return_to', location.pathname);
    }
  }, [isAuthenticated, location]);
  
  if (!isAuthenticated) {
    return <Navigate to="/login" />;
  }
  
  return children;
}

Security Best Practices

PKCE Parameters

  • Use cryptographically secure random generators for code verifier (minimum 43 characters)
  • Always use SHA256 for code challenge generation (S256 method)
  • Never reuse code verifiers across authentication attempts

Token Storage

// Secure token storage strategies

// 1. Memory-only storage (most secure, doesn't survive refresh)
class InMemoryTokenStore {
  private tokens: TokenSet | null = null;
  
  store(tokens: TokenSet) {
    this.tokens = tokens;
  }
  
  get(): TokenSet | null {
    return this.tokens;
  }
  
  clear() {
    this.tokens = null;
  }
}

// 2. Encrypted IndexedDB storage
class SecureTokenStore {
  private dbName = 'oauth_tokens';
  private encryptionKey: CryptoKey;
  
  async initialize() {
    // Generate or retrieve encryption key
    this.encryptionKey = await this.getOrCreateEncryptionKey();
  }
  
  async store(tokens: TokenSet) {
    const encrypted = await this.encrypt(tokens);
    
    const db = await this.openDB();
    const tx = db.transaction(['tokens'], 'readwrite');
    await tx.objectStore('tokens').put({
      id: 'current',
      data: encrypted,
      timestamp: Date.now()
    });
  }
  
  async get(): Promise<TokenSet | null> {
    const db = await this.openDB();
    const tx = db.transaction(['tokens'], 'readonly');
    const stored = await tx.objectStore('tokens').get('current');
    
    if (!stored) return null;
    
    // Check expiry
    const age = Date.now() - stored.timestamp;
    if (age > 3600000) { // 1 hour
      await this.clear();
      return null;
    }
    
    return await this.decrypt(stored.data);
  }
  
  private async encrypt(data: any): Promise<ArrayBuffer> {
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const encoded = new TextEncoder().encode(JSON.stringify(data));
    
    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      this.encryptionKey,
      encoded
    );
    
    // Combine IV and encrypted data
    const combined = new Uint8Array(iv.length + encrypted.byteLength);
    combined.set(iv, 0);
    combined.set(new Uint8Array(encrypted), iv.length);
    
    return combined.buffer;
  }
}

// 3. Secure cookie storage (server-side)
class CookieTokenStore {
  store(tokens: TokenSet) {
    // Send tokens to backend for secure cookie storage
    fetch('/api/auth/store-tokens', {
      method: 'POST',
      credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(tokens)
    });
  }
  
  async get(): Promise<TokenSet | null> {
    // Retrieve tokens from secure HTTP-only cookies
    const response = await fetch('/api/auth/get-tokens', {
      credentials: 'include'
    });
    
    if (!response.ok) return null;
    return response.json();
  }
}

Additional Security Measures

  • 1.
    State Parameter

    Always use a cryptographically random state parameter to prevent CSRF attacks

  • 2.
    Nonce Validation

    When using OpenID Connect, validate the nonce in ID tokens

  • 3.
    Token Rotation

    Use refresh token rotation for enhanced security

  • 4.
    Redirect URI Validation

    Use exact redirect URI matching, never use wildcards

Advanced Features

Silent Authentication

// Silent authentication for SSO
async function silentAuth() {
  try {
    // Create hidden iframe
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    document.body.appendChild(iframe);
    
    // Generate PKCE parameters
    const { codeVerifier, codeChallenge } = await generatePKCE();
    
    // Build auth URL with prompt=none
    const authUrl = new URL(OAUTH_CONFIG.authEndpoint);
    authUrl.searchParams.set('client_id', OAUTH_CONFIG.clientId);
    authUrl.searchParams.set('redirect_uri', OAUTH_CONFIG.redirectUri);
    authUrl.searchParams.set('response_type', 'code');
    authUrl.searchParams.set('scope', 'openid profile');
    authUrl.searchParams.set('prompt', 'none'); // Silent auth
    authUrl.searchParams.set('code_challenge', codeChallenge);
    authUrl.searchParams.set('code_challenge_method', 'S256');
    
    // Load auth URL in iframe
    iframe.src = authUrl.toString();
    
    // Wait for response
    const code = await waitForAuthCode(iframe, 10000); // 10s timeout
    
    // Exchange code for tokens
    const tokens = await exchangeCode(code, codeVerifier);
    
    // Clean up
    document.body.removeChild(iframe);
    
    return { success: true, tokens };
    
  } catch (error) {
    // Silent auth failed, user needs to log in
    return { success: false, error };
  }
}

Device Authorization Flow

# Device authorization flow for CLI/TV apps
import requests
import time
from urllib.parse import urlencode

class DeviceAuthFlow:
    def __init__(self, client_id):
        self.client_id = client_id
        self.device_endpoint = "https://api.parrotrouter.com/oauth/device"
        self.token_endpoint = "https://api.parrotrouter.com/oauth/token"
    
    def start_device_flow(self):
        # Request device code
        response = requests.post(self.device_endpoint, data={
            'client_id': self.client_id,
            'scope': 'openid profile api.read'
        })
        
        device_data = response.json()
        
        # Display code to user
        print(f"\n=== Device Authorization ===")
        print(f"1. Visit: {device_data['verification_uri']}")
        print(f"2. Enter code: {device_data['user_code']}")
        print(f"\nOr visit: {device_data['verification_uri_complete']}")
        print(f"\nWaiting for authorization...")
        
        # Poll for token
        return self.poll_for_token(
            device_data['device_code'],
            device_data['interval']
        )
    
    def poll_for_token(self, device_code, interval):
        while True:
            time.sleep(interval)
            
            response = requests.post(self.token_endpoint, data={
                'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
                'device_code': device_code,
                'client_id': self.client_id
            })
            
            if response.status_code == 200:
                # Success!
                tokens = response.json()
                print("\nAuthorization successful!")
                return tokens
            
            error = response.json().get('error')
            
            if error == 'authorization_pending':
                # Still waiting for user
                continue
            elif error == 'slow_down':
                # Increase polling interval
                interval += 5
            else:
                # Error occurred
                raise Exception(f"Device auth failed: {error}")

# Usage
device_auth = DeviceAuthFlow('your-cli-client-id')
tokens = device_auth.start_device_flow()
print(f"Access token: {tokens['access_token'][:20]}...")

Related Documentation