fix(auth): require email authentication every login

CRITICAL SECURITY FIX:
- Email code required EVERY login (authentication, not verification)
- DNS TXT check cached separately (domain verification)
- New auth_sessions table for per-login state
- Codes hashed with SHA-256, constant-time comparison
- Max 3 attempts, 10-minute session expiry
- OAuth params stored server-side (security improvement)

New files:
- services/auth_session.py
- migrations 004, 005
- ADR-010: domain verification vs user authentication

312 tests passing, 86.21% coverage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-22 15:16:26 -07:00
parent 9b50f359a6
commit 9135edfe84
17 changed files with 3457 additions and 529 deletions

View File

@@ -0,0 +1,183 @@
# Fix: Response Type Parameter Default Handling
## Problem Statement
The current authorization endpoint incorrectly requires the `response_type` parameter for all requests. According to the W3C IndieAuth specification:
- **Section 5.2**: When `response_type` is omitted in an authentication request, the authorization endpoint MUST default to `id`
- **Section 6.2.1**: The `response_type=code` is required for authorization (access token) requests
Currently, the endpoint returns an error when `response_type` is missing, instead of defaulting to `id`.
## Design Overview
Modify the authorization endpoint to:
1. Accept `response_type` as optional
2. Default to `id` when omitted
3. Support both `id` (authentication) and `code` (authorization) flows
4. Return appropriate errors for invalid values
## Implementation Changes
### 1. Response Type Validation Logic
**Location**: `/src/gondulf/routers/authorization.py` lines 111-119
**Current implementation**:
```python
# Validate response_type
if response_type != "code":
error_params = {
"error": "unsupported_response_type",
"error_description": "Only response_type=code is supported",
"state": state or ""
}
redirect_url = f"{redirect_uri}?{urlencode(error_params)}"
return RedirectResponse(url=redirect_url, status_code=302)
```
**New implementation**:
```python
# Validate response_type (defaults to 'id' per IndieAuth spec section 5.2)
if response_type is None:
response_type = "id" # Default per W3C spec
if response_type not in ["id", "code"]:
error_params = {
"error": "unsupported_response_type",
"error_description": f"response_type '{response_type}' not supported. Must be 'id' or 'code'",
"state": state or ""
}
redirect_url = f"{redirect_uri}?{urlencode(error_params)}"
return RedirectResponse(url=redirect_url, status_code=302)
```
### 2. Flow-Specific Validation
The authentication flow (`id`) and authorization flow (`code`) have different requirements:
#### Authentication Flow (`response_type=id`)
- PKCE is optional (not required)
- Scope is not applicable
- Returns only user profile URL
#### Authorization Flow (`response_type=code`)
- PKCE is required (current behavior)
- Scope is applicable
- Returns authorization code for token exchange
**Modified PKCE validation** (lines 121-139):
```python
# Validate PKCE (required only for authorization flow)
if response_type == "code":
if not code_challenge:
error_params = {
"error": "invalid_request",
"error_description": "code_challenge is required for authorization requests (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)
```
### 3. Template Context Update
Pass the resolved `response_type` to the consent template (line 177-189):
```python
return templates.TemplateResponse(
"authorize.html",
{
"request": request,
"client_id": normalized_client_id,
"redirect_uri": redirect_uri,
"response_type": response_type, # Add this - resolved value
"state": state or "",
"code_challenge": code_challenge or "", # Make optional
"code_challenge_method": code_challenge_method or "", # Make optional
"scope": scope or "",
"me": me,
"client_metadata": client_metadata
}
)
```
### 4. Consent Form Processing
The consent handler needs to differentiate between authentication and authorization flows:
**Location**: `/src/gondulf/routers/authorization.py` lines 193-245
Add `response_type` parameter to the form submission and handle accordingly:
1. Add `response_type` as a form field (line ~196)
2. Process differently based on flow type
3. For `id` flow: Return simpler response without creating full authorization code
4. For `code` flow: Current behavior (create authorization code)
## Test Requirements
### New Test Cases
1. **Test missing response_type defaults to 'id'**
- Request without `response_type` parameter
- Should NOT return error
- Should render consent page
- Form should have `response_type=id`
2. **Test explicit response_type=id accepted**
- Request with `response_type=id`
- Should render consent page
- PKCE parameters not required
3. **Test response_type=id without PKCE**
- Request with `response_type=id` and no PKCE
- Should succeed (PKCE optional for authentication)
4. **Test response_type=code requires PKCE**
- Request with `response_type=code` without PKCE
- Should redirect with error (current behavior)
5. **Test invalid response_type values**
- Request with `response_type=token` or other invalid values
- Should redirect with error
### Modified Test Cases
Update existing test in `test_authorization_flow.py`:
- Line 115-126: `test_invalid_response_type_redirects_with_error`
- Keep testing invalid values like "token"
- Add new test for missing parameter (should NOT error)
## Acceptance Criteria
1. ✅ Missing `response_type` defaults to `id` (no error)
2.`response_type=id` is accepted and processed
3.`response_type=code` continues to work as before
4. ✅ Invalid response_type values return appropriate error
5. ✅ PKCE is optional for `id` flow
6. ✅ PKCE remains required for `code` flow
7. ✅ Error messages clearly indicate supported values
8. ✅ All existing tests pass with modifications
9. ✅ New tests cover all response_type scenarios
## Security Considerations
- No security degradation: Authentication flow (`id`) has fewer requirements by design
- PKCE remains mandatory for authorization flow (`code`)
- Invalid values still produce errors
- State parameter continues to be preserved in all flows
## Notes
This is a bug fix to bring the implementation into compliance with the W3C IndieAuth specification. The specification is explicit that `response_type` defaults to `id` when omitted, which enables simpler authentication-only flows.