Added OIDC client CORS attributes
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""Validation helpers for request data."""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# Special sentinel values allowed in allowed_cors_origins
|
||||
_CORS_SENTINELS = {"+", "*"}
|
||||
|
||||
|
||||
def validate_cors_origins(origins):
|
||||
"""Validate a list of CORS origin values.
|
||||
|
||||
Accepts:
|
||||
None - means "use global CORS config" (pass-through)
|
||||
["+"] - derive origins from the client's redirect_uris
|
||||
["*"] - allow any origin
|
||||
["https://host"] - explicit allow-list of well-formed origins
|
||||
|
||||
Each non-sentinel entry must be a well-formed origin:
|
||||
scheme (http or https) + host + optional port, with NO path,
|
||||
query string, or fragment.
|
||||
|
||||
Returns:
|
||||
(validated_value, None) on success, or
|
||||
(None, error_message) on failure.
|
||||
"""
|
||||
if origins is None:
|
||||
return None, None
|
||||
|
||||
if not isinstance(origins, list):
|
||||
return None, "allowed_cors_origins must be a list or null"
|
||||
|
||||
validated = []
|
||||
for i, entry in enumerate(origins):
|
||||
if not isinstance(entry, str):
|
||||
return None, f"allowed_cors_origins[{i}]: expected a string, got {type(entry).__name__}"
|
||||
|
||||
entry = entry.strip()
|
||||
if not entry:
|
||||
return None, f"allowed_cors_origins[{i}]: empty string is not allowed"
|
||||
|
||||
# Sentinel values are accepted as-is
|
||||
if entry in _CORS_SENTINELS:
|
||||
validated.append(entry)
|
||||
continue
|
||||
|
||||
# Parse and validate as origin
|
||||
error = _validate_single_origin(entry, i)
|
||||
if error:
|
||||
return None, error
|
||||
|
||||
validated.append(entry)
|
||||
|
||||
return validated, None
|
||||
|
||||
|
||||
def _validate_single_origin(origin, index):
|
||||
"""Validate that a string is a well-formed browser origin.
|
||||
|
||||
A valid origin is: scheme://host[:port] with no path, query, or fragment.
|
||||
Only http and https schemes are accepted.
|
||||
|
||||
Returns an error message string on failure, or None on success.
|
||||
"""
|
||||
try:
|
||||
parsed = urlparse(origin)
|
||||
except Exception:
|
||||
return f"allowed_cors_origins[{index}]: '{origin}' is not a valid URL"
|
||||
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
return (
|
||||
f"allowed_cors_origins[{index}]: '{origin}' has an invalid scheme "
|
||||
f"'{parsed.scheme}'; only 'http' and 'https' are allowed"
|
||||
)
|
||||
|
||||
if not parsed.hostname:
|
||||
return f"allowed_cors_origins[{index}]: '{origin}' is missing a hostname"
|
||||
|
||||
# Origins must not have a path (other than empty or "/"), query, or fragment
|
||||
if parsed.path and parsed.path != "/":
|
||||
return (
|
||||
f"allowed_cors_origins[{index}]: '{origin}' must not contain a path "
|
||||
f"(got '{parsed.path}'). Specify only scheme://host[:port]"
|
||||
)
|
||||
|
||||
if parsed.query:
|
||||
return (
|
||||
f"allowed_cors_origins[{index}]: '{origin}' must not contain a query string"
|
||||
)
|
||||
|
||||
if parsed.fragment:
|
||||
return (
|
||||
f"allowed_cors_origins[{index}]: '{origin}' must not contain a fragment"
|
||||
)
|
||||
|
||||
return None
|
||||
Reference in New Issue
Block a user