fix(auth): make PKCE optional per ADR-003

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>
This commit is contained in:
2025-12-17 15:23:44 -07:00
parent 1ea2afcaa4
commit 404d723ef8
6 changed files with 895 additions and 34 deletions

View File

@@ -0,0 +1,201 @@
# 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):**
```python
# 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:
1. Remove the two error-returning conditionals
2. Add validation only when PKCE parameters ARE provided
3. 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):**
```python
# 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):**
```python
# 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:
```python
"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
1. **Token endpoint** - Already handles PKCE correctly (optional, logged but not validated per ADR-003 lines 200-203)
2. **POST /authorize** - Already handles PKCE correctly (optional, logged but not validated per lines 856-858)
3. **Auth session service** - Already accepts optional code_challenge parameters
4. **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
1. **Test authorization without PKCE:**
- Call `/authorize` without `code_challenge` - should succeed
- Verify session is created with NULL code_challenge
2. **Test authorization with PKCE:**
- Call `/authorize` with valid `code_challenge` and `code_challenge_method=S256` - should succeed
- Verify session stores the code_challenge
3. **Test PKCE with default method:**
- Call `/authorize` with `code_challenge` but no `code_challenge_method`
- Should succeed, default to S256
4. **Test invalid PKCE method:**
- Call `/authorize` with `code_challenge` and `code_challenge_method=plain`
- Should return error (only S256 supported)
5. **End-to-end flow without PKCE:**
- Complete full authorization flow without PKCE parameters
- Verify token can be obtained
### Manual Testing
1. Use a real IndieAuth client that does NOT send PKCE
2. Verify authentication completes successfully
## Acceptance Criteria
1. Clients without PKCE can complete authorization flow
2. Clients with PKCE continue to work unchanged
3. Invalid PKCE method (not S256) is rejected
4. PKCE parameters are stored in auth session when provided
5. All existing tests continue to pass
6. 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:
1. **DELETE** the two blocks that return errors for missing PKCE
2. **ADD** a simpler block that only validates method IF code_challenge is provided
3. **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
1. `/src/gondulf/routers/authorization.py` - Remove mandatory PKCE checks (~20 lines changed)
### Files to Add
1. 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)