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:
183
docs/designs/response-type-fix.md
Normal file
183
docs/designs/response-type-fix.md
Normal 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.
|
||||
Reference in New Issue
Block a user