""" PKCE (Proof Key for Code Exchange) Implementation RFC 7636 - OAuth 2.1 Mandatory """ import base64 import hashlib import secrets from typing import Literal def generate_code_verifier(length: int = 64) -> str: """ Generate PKCE code verifier. Args: length: Length of verifier (43-128 characters) Returns: URL-safe random string OAuth 2.1 Spec: - Length: 43-128 characters - Character set: [A-Za-z0-9-._~] (unreserved characters) """ if not 43 <= length <= 128: raise ValueError("Code verifier length must be between 43-128") # Generate random bytes and encode as URL-safe base64 random_bytes = secrets.token_bytes(length) verifier = base64.urlsafe_b64encode(random_bytes).decode("utf-8") # Remove padding and truncate to desired length verifier = verifier.rstrip("=")[:length] return verifier def generate_code_challenge(code_verifier: str, method: Literal["S256"] = "S256") -> str: """ Generate PKCE code challenge from verifier. Args: code_verifier: Code verifier string method: Challenge method (OAuth 2.1: only S256) Returns: Base64 URL-encoded SHA256 hash of verifier OAuth 2.1 Spec: - Only S256 method is allowed (plain is removed) - code_challenge = BASE64URL(SHA256(code_verifier)) """ if method != "S256": raise ValueError("OAuth 2.1 only supports S256 method (plain is removed)") if not code_verifier: raise ValueError("Code verifier cannot be empty") # SHA256 hash digest = hashlib.sha256(code_verifier.encode("utf-8")).digest() # Base64 URL encode (no padding) challenge = base64.urlsafe_b64encode(digest).decode("utf-8").rstrip("=") return challenge def validate_code_challenge( code_verifier: str, code_challenge: str, method: Literal["S256"] = "S256" ) -> bool: """ Validate PKCE code verifier against challenge. Args: code_verifier: Code verifier from client code_challenge: Code challenge from authorization request method: Challenge method Returns: True if valid, False otherwise """ if method != "S256": raise ValueError("Only S256 method is supported") try: # Generate challenge from verifier expected_challenge = generate_code_challenge(code_verifier, method) # Constant-time comparison to prevent timing attacks return secrets.compare_digest(expected_challenge, code_challenge) except Exception: return False