The "Request a new code" and "Try Again" links were outputting code_challenge=None when PKCE was not used, causing the retry to fail with validation errors. Now only includes PKCE parameters if code_challenge has a value. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
6.4 KiB
Client ID Validation Compliance Analysis
W3C IndieAuth Specification Requirements (Section 3.2)
According to the W3C IndieAuth specification, client identifiers MUST meet the following requirements:
- Scheme: MUST have either
httpsorhttpscheme - Path: MUST contain a path component
- Path Segments: MUST NOT contain single-dot or double-dot path segments
- Query String: MAY contain a query string component
- Fragment: MUST NOT contain a fragment component
- User Info: MUST NOT contain username or password component
- Port: MAY contain a port
- Hostname:
- MUST be domain names or a loopback interface
- MUST NOT be IPv4 or IPv6 addresses except for 127.0.0.1 or [::1]
Current Gondulf Implementation Analysis
What We Currently Validate ✅
-
Scheme Validation (PARTIAL)
- ✅ We require HTTPS scheme in
normalize_client_id() - ❌ We reject HTTP completely, even for localhost (spec allows HTTP)
- Location:
/src/gondulf/utils/validation.pylines 48-49
- ✅ We require HTTPS scheme in
-
Query String Preservation
- ✅ We preserve query strings correctly
- Location:
/src/gondulf/utils/validation.pylines 58-59
-
Fragment Preservation (BUT NOT VALIDATED)
- ⚠️ We preserve fragments but don't reject them
- Location:
/src/gondulf/utils/validation.pylines 60-61
What We DON'T Validate ❌
-
Path Component Requirement
- ❌ We don't verify that a path component exists
- Spec requires at least "/" as a path
-
Path Segment Validation
- ❌ We don't check for single-dot (.) or double-dot (..) path segments
- These should be rejected per spec
-
Fragment Component
- ❌ We don't reject URLs with fragments (we actually preserve them)
- Spec says MUST NOT contain fragment
-
User Info Component
- ❌ We don't check for or reject username:password in URLs
- Spec says MUST NOT contain username or password
-
Hostname/IP Validation
- ❌ We don't validate that hostnames are domain names
- ❌ We don't reject IPv4/IPv6 addresses (except 127.0.0.1 and [::1])
- ❌ We don't have special handling for localhost/127.0.0.1/[::1]
-
Port Support
- ✅ We preserve ports correctly
- ⚠️ But we don't allow HTTP for localhost with ports
Reference Implementation (IndieLogin) Comparison
From examining /home/phil/Projects/indielogin.com/app/Authenticate.php:
- Uses
\p3k\url\is_url()for basic URL validation (line 49) - Checks that client_id contains a dot OR is localhost (line 51)
- Validates redirect_uri domain matches client_id domain (lines 75-95)
- Does NOT appear to validate all spec requirements either
Non-Compliance Issues Found
Critical Issues
-
HTTP localhost support missing
# Current code rejects all HTTP if parsed.scheme != 'https': raise ValueError("client_id must use https scheme")Should allow HTTP for localhost/127.0.0.1/[::1]
-
Fragment rejection missing
# Current code preserves fragments instead of rejecting them if parsed.fragment: normalized += f"#{parsed.fragment}" -
Path component not validated
- No check that path exists (at minimum "/")
-
IP address validation missing
- Should reject IPs except 127.0.0.1 and [::1]
Security Implications
- Fragment acceptance - Could lead to confusion about actual client_id
- User info not rejected - Could expose credentials in logs
- IP addresses allowed - Could bypass domain validation
- Path traversal - Accepting ../.. could be security issue
Recommended Fixes
Priority 1: Add proper client_id validation function
Create a new validate_client_id() function that checks ALL spec requirements:
def validate_client_id(client_id: str) -> tuple[bool, str]:
"""
Validate client_id per W3C IndieAuth spec Section 3.2.
Returns:
(is_valid, error_message)
"""
try:
parsed = urlparse(client_id)
# 1. Check scheme
if parsed.scheme not in ['https', 'http']:
return False, "client_id must use https or http scheme"
# 2. For HTTP, only allow localhost/loopback
if parsed.scheme == 'http':
if parsed.hostname not in ['localhost', '127.0.0.1', '[::1]', '::1']:
return False, "HTTP scheme only allowed for localhost/127.0.0.1/[::1]"
# 3. Must have path component
if not parsed.path:
return False, "client_id must contain a path component"
# 4. Check for . or .. segments
path_segments = parsed.path.split('/')
if '.' in path_segments or '..' in path_segments:
return False, "client_id must not contain . or .. path segments"
# 5. Must NOT have fragment
if parsed.fragment:
return False, "client_id must not contain a fragment"
# 6. Must NOT have user info
if parsed.username or parsed.password:
return False, "client_id must not contain username or password"
# 7. Check hostname (no raw IPs except loopback)
if parsed.hostname:
# Check if it's an IP address
import ipaddress
try:
ip = ipaddress.ip_address(parsed.hostname)
# Only allow loopback IPs
if not ip.is_loopback:
return False, f"client_id must not use IP address {parsed.hostname}"
except ValueError:
# Not an IP, that's good (it's a domain)
pass
return True, ""
except Exception as e:
return False, f"Invalid URL: {e}"
Priority 2: Update normalize_client_id()
Should call validate_client_id() first, then normalize.
Priority 3: Add comprehensive tests
Test all validation rules with both positive and negative cases.
Conclusion
Current Status: NOT COMPLIANT ❌
Gondulf's client_id validation is currently not compliant with W3C IndieAuth specification Section 3.2. We have several missing validations that need to be implemented before v1.0.0 release.
Required Actions Before v1.0.0
- Implement complete
validate_client_id()function - Update
normalize_client_id()to validate first - Add support for HTTP on localhost/127.0.0.1/[::1]
- Add tests for all validation rules
- Update authorization endpoint to use new validation
This is a BLOCKING ISSUE for v1.0.0 release as it affects spec compliance.