PKCE was incorrectly required in the /authorize endpoint, contradicting ADR-003 which defers PKCE to v1.1.0. Changes: - PKCE parameters are now optional in /authorize - If code_challenge provided, validates method is S256 - Defaults to S256 if method not specified - Logs when clients don't use PKCE for monitoring - Updated tests for optional PKCE behavior This fixes authentication for clients that don't implement PKCE. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
7.3 KiB
Design: Make PKCE Optional in v1.0.0 (Bug Fix)
Date: 2025-12-17 Status: Ready for Implementation Priority: P0 (Blocking)
Problem Statement
The /authorize endpoint currently requires PKCE parameters (code_challenge and code_challenge_method), which contradicts ADR-003 that explicitly states PKCE is deferred to v1.1.0.
Current behavior (lines 325-343 in authorization.py):
# Validate code_challenge (PKCE required)
if not code_challenge:
return {"error": "invalid_request", "error_description": "code_challenge is required (PKCE)"}
if code_challenge_method != "S256":
return {"error": "invalid_request", "error_description": "code_challenge_method must be S256"}
Expected v1.0.0 behavior per ADR-003:
- PKCE parameters should be optional
- Clients without PKCE should be able to authenticate
- PKCE validation is deferred to v1.1.0
This bug is blocking real-world IndieAuth clients that do not use PKCE.
Design Overview
The fix is straightforward: remove the mandatory PKCE checks from the authorization endpoint while preserving the ability to accept and store PKCE parameters for forward compatibility.
Principle: Minimal Change
This is a bug fix, not a feature. The change should be minimal and surgical:
- Remove the two error-returning conditionals
- Add validation only when PKCE parameters ARE provided
- Preserve all existing storage behavior
Detailed Changes
Change 1: Remove Mandatory PKCE Check
Location: /src/gondulf/routers/authorization.py, lines 325-343
Current Code (to be removed):
# Validate code_challenge (PKCE required)
if not code_challenge:
error_params = {
"error": "invalid_request",
"error_description": "code_challenge is required (PKCE)",
"state": state or ""
}
redirect_url = f"{redirect_uri}?{urlencode(error_params)}"
return RedirectResponse(url=redirect_url, status_code=302)
# Validate code_challenge_method
if code_challenge_method != "S256":
error_params = {
"error": "invalid_request",
"error_description": "code_challenge_method must be S256",
"state": state or ""
}
redirect_url = f"{redirect_uri}?{urlencode(error_params)}"
return RedirectResponse(url=redirect_url, status_code=302)
New Code (replacement):
# PKCE validation (optional in v1.0.0, per ADR-003)
# If code_challenge is provided, validate the method
if code_challenge:
if code_challenge_method and code_challenge_method != "S256":
error_params = {
"error": "invalid_request",
"error_description": "code_challenge_method must be S256",
"state": state or ""
}
redirect_url = f"{redirect_uri}?{urlencode(error_params)}"
return RedirectResponse(url=redirect_url, status_code=302)
# If code_challenge provided without method, default to S256
if not code_challenge_method:
code_challenge_method = "S256"
else:
# Log for future monitoring (per ADR-003 recommendation)
logger.info(f"Client {client_id} not using PKCE")
Change 2: Handle None Values in Session Storage
The AuthSessionService.create_session() already accepts these parameters, and the database schema likely allows NULL values. No changes needed to the service layer.
Verification: The auth_session.py already uses these parameters directly:
"code_challenge": code_challenge,
"code_challenge_method": code_challenge_method,
If code_challenge is None, this will store NULL in the database, which is the desired behavior.
Change 3: Update Template Context (Optional Cleanup)
The templates already receive code_challenge and code_challenge_method - they will now sometimes be None or empty. This should not cause issues as Jinja2 handles None values gracefully in form hidden fields.
Behavior Matrix
| code_challenge | code_challenge_method | Result |
|---|---|---|
| None | None | Proceed without PKCE |
| None | "S256" | Proceed without PKCE (method ignored) |
| "abc123..." | None | Proceed with PKCE, default to S256 |
| "abc123..." | "S256" | Proceed with PKCE |
| "abc123..." | "plain" | ERROR: method must be S256 |
What NOT to Change
- Token endpoint - Already handles PKCE correctly (optional, logged but not validated per ADR-003 lines 200-203)
- POST /authorize - Already handles PKCE correctly (optional, logged but not validated per lines 856-858)
- Auth session service - Already accepts optional code_challenge parameters
- Database schema - Likely already allows NULL for these fields
Security Considerations
No security regression:
- ADR-003 explicitly accepted this risk for v1.0.0
- HTTPS enforcement mitigates code interception
- 10-minute code lifetime limits attack window
- Single-use codes prevent replay
Forward compatibility:
- PKCE parameters are still stored when provided
- v1.1.0 can enable validation without schema changes
- Clients using PKCE today will work in v1.1.0
Testing Strategy
Unit Tests
-
Test authorization without PKCE:
- Call
/authorizewithoutcode_challenge- should succeed - Verify session is created with NULL code_challenge
- Call
-
Test authorization with PKCE:
- Call
/authorizewith validcode_challengeandcode_challenge_method=S256- should succeed - Verify session stores the code_challenge
- Call
-
Test PKCE with default method:
- Call
/authorizewithcode_challengebut nocode_challenge_method - Should succeed, default to S256
- Call
-
Test invalid PKCE method:
- Call
/authorizewithcode_challengeandcode_challenge_method=plain - Should return error (only S256 supported)
- Call
-
End-to-end flow without PKCE:
- Complete full authorization flow without PKCE parameters
- Verify token can be obtained
Manual Testing
- Use a real IndieAuth client that does NOT send PKCE
- Verify authentication completes successfully
Acceptance Criteria
- Clients without PKCE can complete authorization flow
- Clients with PKCE continue to work unchanged
- Invalid PKCE method (not S256) is rejected
- PKCE parameters are stored in auth session when provided
- All existing tests continue to pass
- New tests cover optional PKCE behavior
Implementation Notes
For the Developer
The fix is contained to a single location in authorization.py. The key insight is:
- DELETE the two blocks that return errors for missing PKCE
- ADD a simpler block that only validates method IF code_challenge is provided
- ADD a log statement for clients not using PKCE (monitoring per ADR-003)
The rest of the codebase already handles optional PKCE correctly. This was an error in the GET /authorize validation logic only.
Estimated Effort
S (Small) - 1-2 hours including tests
Files to Modify
/src/gondulf/routers/authorization.py- Remove mandatory PKCE checks (~20 lines changed)
Files to Add
- Tests for optional PKCE behavior (or add to existing authorization tests)
References
- ADR-003:
/docs/decisions/ADR-003-pkce-deferred-to-v1-1-0.md - W3C IndieAuth: https://www.w3.org/TR/indieauth/ (PKCE is not mentioned, making it optional)
- RFC 7636: https://datatracker.ietf.org/doc/html/rfc7636 (PKCE specification)