1
0
mirror of https://github.com/quay/quay.git synced 2026-01-26 06:21:37 +03:00
Files
quay/oauth/pkce.py
Dave O'Connor b9460aa334 feat(oidc): add PKCE (S256/plain) support with session-verifier flow (PROJQUAY-9281) (#4256)
Implement PKCE (Proof Key for Code Exchange) for OIDC authentication to enable
  support for public clients and improve OAuth security.

  Changes:
  - Add oauth/pkce.py with code_verifier generation and S256/plain challenge methods
  - Extend OAuthService to support extra auth/token params and public clients (no client_secret)
  - Implement PKCE in OIDCLoginService with code_verifier token exchange
  - Store PKCE verifier in session during auth initiation (endpoints/api/user.py)
  - Add get_pkce_code_verifier() helper with defensive type checking
    * Encapsulates pkce_enabled check and session data extraction
    * Uses isinstance(data, dict) for safe type validation
    * Centralizes logic across OAuth callbacks (callback, attach, cli)
  - Include example Keycloak PKCE config in local-dev/stack/config.yaml

  Security improvements:
  - PKCE method validation to fail fast on invalid configuration
  - Defensive session data validation in OAuth callbacks
  - Explicit Content-Type headers for form-encoded OAuth requests
  - Optimized non-verified JWT decode (skips unnecessary key fetching)
  - Exponential backoff for token exchange retries (0.5s, 1.0s, 2.0s)

  Configuration:
  - PKCE is opt-in via USE_PKCE config (default: disabled)
  - OIDC_SERVER must end with trailing slash
  - Use host.containers.internal with podman for local dev

  Co-authored-by: Claude <noreply@anthropic.com>
2025-10-01 16:42:25 -04:00

22 lines
731 B
Python

import base64
import hashlib
import secrets
import string
_UNRESERVED = string.ascii_letters + string.digits + "-._~"
def generate_code_verifier(length: int = 64) -> str:
if length < 43 or length > 128:
raise ValueError("PKCE code_verifier length must be between 43 and 128 characters")
return "".join(secrets.choice(_UNRESERVED) for _ in range(length))
def code_challenge(verifier: str, method: str = "S256") -> str:
if method.upper() == "PLAIN":
return verifier
if method.upper() != "S256":
raise ValueError("Unsupported PKCE method: %s" % method)
digest = hashlib.sha256(verifier.encode("ascii")).digest()
return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")