Community edition generated from private repo via sync pipeline. Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with ~587 tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
92 lines
2.5 KiB
Python
92 lines
2.5 KiB
Python
"""
|
|
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
|