BREAKING: Removes INDIELOGIN_URL config - endpoints are now properly discovered from user's profile URL as required by W3C IndieAuth spec. - auth.py: Uses discover_endpoints() to find authorization_endpoint - config.py: Deprecation warning for obsolete INDIELOGIN_URL setting - auth_external.py: Relaxed validation (allows auth-only flows) - tests: Updated to mock endpoint discovery This fixes a regression where admin login was hardcoded to use indielogin.com instead of respecting the user's declared endpoints. Version: 1.5.0-hotfix.1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
388 lines
14 KiB
Markdown
388 lines
14 KiB
Markdown
# IndieAuth Endpoint Discovery Regression Analysis
|
|
|
|
**Date**: 2025-12-17
|
|
**Author**: Agent-Architect
|
|
**Status**: Analysis Complete - Design Proposed
|
|
**Version**: v1.5.0 (or v1.6.0 depending on scope decision)
|
|
|
|
## Executive Summary
|
|
|
|
StarPunk has a **critical authentication bug** where admin login always redirects to `indielogin.com` instead of the user's declared IndieAuth endpoints. This violates the W3C IndieAuth specification and prevents users with self-hosted IndieAuth servers from logging in.
|
|
|
|
**Root Cause**: The `initiate_login()` function in `starpunk/auth.py` hardcodes `INDIELOGIN_URL` for authorization, bypassing endpoint discovery entirely. While `auth_external.py` correctly implements endpoint discovery for Micropub token verification, the admin login flow was never updated to use it.
|
|
|
|
**Impact**: Users whose site declares their own authorization endpoint (e.g., `https://gondulf.thesatelliteoflove.com/authorize`) cannot log in because StarPunk sends them to `https://indielogin.com/authorize` instead. The error message `code_challenge is required (PKCE)` occurs because indielogin.com requires PKCE but the user's server may not.
|
|
|
|
---
|
|
|
|
## 1. Git History Investigation
|
|
|
|
### Timeline of Changes
|
|
|
|
| Commit | Date | Description | Relevant? |
|
|
|--------|------|-------------|-----------|
|
|
| `d4f1bfb` | Early Nov 2025 | Initial auth module with IndieLogin support | **Origin of bug** |
|
|
| `a3bac86` | 2025-11-24 | Complete IndieAuth server removal (Phases 2-4) | Added `auth_external.py` |
|
|
| `80bd51e` | 2025-11-24 | Implement IndieAuth endpoint discovery (v1.0.0-rc.5) | Fixed discovery for **tokens only** |
|
|
|
|
### Key Finding
|
|
|
|
The endpoint discovery implemented in commit `80bd51e` (ADR-044) only applies to **Micropub token verification** via `auth_external.py`. The **admin login flow** in `auth.py` was never updated to discover endpoints.
|
|
|
|
### Code Locations
|
|
|
|
**Problem Location** - `starpunk/auth.py` lines 306-307:
|
|
```python
|
|
# CORRECT ENDPOINT: /authorize (not /auth)
|
|
auth_url = f"{current_app.config['INDIELOGIN_URL']}/authorize?{urlencode(params)}"
|
|
```
|
|
|
|
**Working Implementation** - `starpunk/auth_external.py` lines 195-247:
|
|
The `discover_endpoints()` function correctly implements W3C IndieAuth discovery but is only used for token verification, not login.
|
|
|
|
### Why This Was Missed
|
|
|
|
1. **Two separate auth flows**: StarPunk has two distinct authentication contexts:
|
|
- **Admin Login**: Delegates to external authorization server (currently hardcoded to indielogin.com)
|
|
- **Micropub Token Verification**: Validates bearer tokens against user's token endpoint (correctly discovers endpoints)
|
|
|
|
2. **ADR-044 scope**: The endpoint discovery ADR focused on token verification for Micropub, not the admin login flow.
|
|
|
|
3. **Working configuration**: indielogin.com works for users who delegate to it, masking the bug for those use cases.
|
|
|
|
---
|
|
|
|
## 2. W3C IndieAuth Specification Requirements
|
|
|
|
Per the [W3C IndieAuth Specification](https://www.w3.org/TR/indieauth/) and the [IndieAuth Living Spec](https://indieauth.spec.indieweb.org/):
|
|
|
|
### 2.1 Discovery Process (Section 4.2)
|
|
|
|
Clients MUST:
|
|
|
|
1. **Fetch the user's profile URL** (the "me" URL they enter at login)
|
|
2. **Look for `indieauth-metadata` link relation first** (modern approach)
|
|
3. **Fall back to `authorization_endpoint` and `token_endpoint` link relations** (legacy)
|
|
4. **Use the DISCOVERED endpoints**, not hardcoded values
|
|
|
|
### 2.2 Discovery Priority
|
|
|
|
1. HTTP Link header with `rel="indieauth-metadata"` (preferred)
|
|
2. HTML `<link rel="indieauth-metadata">` element
|
|
3. Fall back: HTTP Link header with `rel="authorization_endpoint"`
|
|
4. Fall back: HTML `<link rel="authorization_endpoint">` element
|
|
|
|
### 2.3 Metadata Document
|
|
|
|
When `indieauth-metadata` is found, fetch the JSON document which contains:
|
|
```json
|
|
{
|
|
"issuer": "https://auth.example.com/",
|
|
"authorization_endpoint": "https://auth.example.com/authorize",
|
|
"token_endpoint": "https://auth.example.com/token",
|
|
"code_challenge_methods_supported": ["S256"]
|
|
}
|
|
```
|
|
|
|
### 2.4 Key Quote from Spec
|
|
|
|
> "Clients MUST start by making a GET or HEAD request to fetch the user's profile URL to discover the necessary values."
|
|
|
|
---
|
|
|
|
## 3. Design for v1.5.0/v1.6.0
|
|
|
|
### 3.1 Architecture Decision
|
|
|
|
**Recommendation**: Reuse the existing `discover_endpoints()` function from `auth_external.py` for the login flow.
|
|
|
|
This function already:
|
|
- Implements W3C-compliant discovery
|
|
- Handles HTTP Link headers and HTML link elements
|
|
- Resolves relative URLs
|
|
- Validates HTTPS in production
|
|
- Caches results (1-hour TTL)
|
|
- Has comprehensive test coverage (35 tests)
|
|
|
|
### 3.2 Proposed Changes
|
|
|
|
#### 3.2.1 Extend `auth_external.py`
|
|
|
|
Add support for `indieauth-metadata` discovery (currently missing):
|
|
|
|
```python
|
|
def discover_endpoints(profile_url: str) -> Dict[str, str]:
|
|
"""
|
|
Discover IndieAuth endpoints from a profile URL
|
|
|
|
Implements IndieAuth endpoint discovery per W3C spec:
|
|
https://www.w3.org/TR/indieauth/#discovery-by-clients
|
|
|
|
Discovery priority:
|
|
1. indieauth-metadata link (modern approach)
|
|
2. HTTP Link headers for individual endpoints (legacy)
|
|
3. HTML link elements (legacy)
|
|
"""
|
|
# Check cache first
|
|
cached_endpoints = _cache.get_endpoints()
|
|
if cached_endpoints:
|
|
return cached_endpoints
|
|
|
|
# Validate and fetch profile
|
|
_validate_profile_url(profile_url)
|
|
|
|
try:
|
|
endpoints = _fetch_and_parse(profile_url)
|
|
_cache.set_endpoints(endpoints)
|
|
return endpoints
|
|
except Exception as e:
|
|
# Graceful fallback to expired cache
|
|
cached = _cache.get_endpoints(ignore_expiry=True)
|
|
if cached:
|
|
return cached
|
|
raise DiscoveryError(f"Endpoint discovery failed: {e}")
|
|
```
|
|
|
|
**New function needed**:
|
|
```python
|
|
def _fetch_metadata(metadata_url: str) -> Dict[str, str]:
|
|
"""
|
|
Fetch IndieAuth metadata document and extract endpoints.
|
|
|
|
Args:
|
|
metadata_url: URL of the indieauth-metadata document
|
|
|
|
Returns:
|
|
Dict with authorization_endpoint and token_endpoint
|
|
"""
|
|
response = httpx.get(metadata_url, timeout=DISCOVERY_TIMEOUT)
|
|
response.raise_for_status()
|
|
metadata = response.json()
|
|
|
|
return {
|
|
'authorization_endpoint': metadata.get('authorization_endpoint'),
|
|
'token_endpoint': metadata.get('token_endpoint'),
|
|
'issuer': metadata.get('issuer'),
|
|
}
|
|
```
|
|
|
|
#### 3.2.2 Update `auth.py::initiate_login()`
|
|
|
|
Replace hardcoded INDIELOGIN_URL with discovered endpoint:
|
|
|
|
**Before**:
|
|
```python
|
|
def initiate_login(me_url: str) -> str:
|
|
# ... validation ...
|
|
|
|
params = {
|
|
"me": me_url,
|
|
"client_id": current_app.config["SITE_URL"],
|
|
"redirect_uri": redirect_uri,
|
|
"state": state,
|
|
"response_type": "code",
|
|
}
|
|
|
|
# HARDCODED - THE BUG
|
|
auth_url = f"{current_app.config['INDIELOGIN_URL']}/authorize?{urlencode(params)}"
|
|
return auth_url
|
|
```
|
|
|
|
**After**:
|
|
```python
|
|
from starpunk.auth_external import discover_endpoints, DiscoveryError
|
|
|
|
def initiate_login(me_url: str) -> str:
|
|
# ... validation ...
|
|
|
|
# Discover endpoints from user's profile URL
|
|
try:
|
|
endpoints = discover_endpoints(me_url)
|
|
except DiscoveryError as e:
|
|
current_app.logger.error(f"Endpoint discovery failed for {me_url}: {e}")
|
|
raise ValueError(f"Could not discover IndieAuth endpoints for {me_url}")
|
|
|
|
authorization_endpoint = endpoints.get('authorization_endpoint')
|
|
if not authorization_endpoint:
|
|
raise ValueError(f"No authorization endpoint found at {me_url}")
|
|
|
|
params = {
|
|
"me": me_url,
|
|
"client_id": current_app.config["SITE_URL"],
|
|
"redirect_uri": redirect_uri,
|
|
"state": state,
|
|
"response_type": "code",
|
|
}
|
|
|
|
# Use discovered endpoint
|
|
auth_url = f"{authorization_endpoint}?{urlencode(params)}"
|
|
return auth_url
|
|
```
|
|
|
|
#### 3.2.3 Update `auth.py::handle_callback()`
|
|
|
|
The callback handler also references INDIELOGIN_URL for:
|
|
1. Issuer validation (line 350)
|
|
2. Token exchange (line 371)
|
|
|
|
These need to use the discovered endpoints instead. Store discovered endpoints in the auth_state table:
|
|
|
|
**Schema change** - Add column to auth_state:
|
|
```sql
|
|
ALTER TABLE auth_state ADD COLUMN discovered_endpoints TEXT;
|
|
```
|
|
|
|
Or use session storage (simpler, no migration needed):
|
|
```python
|
|
session['discovered_endpoints'] = endpoints
|
|
```
|
|
|
|
#### 3.2.4 Configuration Changes
|
|
|
|
- **Deprecate**: `INDIELOGIN_URL` configuration variable
|
|
- **Keep as fallback**: If discovery fails and user has configured INDIELOGIN_URL, use it as a last resort
|
|
- **Add deprecation warning**: Log warning if INDIELOGIN_URL is set
|
|
|
|
### 3.3 Edge Cases
|
|
|
|
| Scenario | Behavior |
|
|
|----------|----------|
|
|
| No endpoints found | Raise clear error, log details |
|
|
| Only authorization_endpoint found | Use it (token_endpoint not needed for login-only flow) |
|
|
| Discovery timeout | Use cached endpoints if available, else fail |
|
|
| Invalid endpoints (HTTP in production) | Reject with security error |
|
|
| Relative URLs | Resolve against profile URL |
|
|
| Redirected profile URL | Follow redirects, use final URL for resolution |
|
|
|
|
### 3.4 Backward Compatibility
|
|
|
|
Users who currently rely on indielogin.com via `INDIELOGIN_URL`:
|
|
- Their profiles likely don't declare endpoints (they delegate to indielogin.com)
|
|
- Discovery will fail, then fall back to INDIELOGIN_URL if configured
|
|
- Log deprecation warning recommending they add endpoint declarations to their profile
|
|
|
|
### 3.5 PKCE Considerations
|
|
|
|
The error message mentions PKCE (`code_challenge is required`). Current flow:
|
|
- StarPunk does NOT send PKCE parameters in `initiate_login()`
|
|
- indielogin.com requires PKCE
|
|
- User's server (gondulf) may or may not require PKCE
|
|
|
|
**Design Decision**: Add optional PKCE support based on metadata discovery:
|
|
1. If `indieauth-metadata` is found, check `code_challenge_methods_supported`
|
|
2. If S256 is supported, include PKCE parameters
|
|
3. If not discoverable, default to sending PKCE (most servers support it)
|
|
|
|
---
|
|
|
|
## 4. File Changes Summary
|
|
|
|
| File | Change Type | Description |
|
|
|------|-------------|-------------|
|
|
| `starpunk/auth_external.py` | Modify | Add `indieauth-metadata` discovery support |
|
|
| `starpunk/auth.py` | Modify | Use discovered endpoints instead of INDIELOGIN_URL |
|
|
| `starpunk/config.py` | Modify | Deprecate INDIELOGIN_URL with warning |
|
|
| `tests/test_auth.py` | Modify | Update tests for endpoint discovery |
|
|
| `tests/test_auth_external.py` | Modify | Add metadata discovery tests |
|
|
| `.env.example` | Modify | Update INDIELOGIN_URL documentation |
|
|
|
|
---
|
|
|
|
## 5. Testing Strategy
|
|
|
|
### 5.1 Unit Tests
|
|
|
|
```python
|
|
class TestEndpointDiscoveryForLogin:
|
|
"""Test endpoint discovery in login flow"""
|
|
|
|
def test_discover_from_link_header(self):
|
|
"""Authorization endpoint discovered from Link header"""
|
|
|
|
def test_discover_from_html_link(self):
|
|
"""Authorization endpoint discovered from HTML link element"""
|
|
|
|
def test_discover_from_metadata(self):
|
|
"""Endpoints discovered from indieauth-metadata document"""
|
|
|
|
def test_metadata_priority_over_link(self):
|
|
"""Metadata takes priority over individual link relations"""
|
|
|
|
def test_discovery_failure_clear_error(self):
|
|
"""Clear error message when no endpoints found"""
|
|
|
|
def test_fallback_to_indielogin_url(self):
|
|
"""Falls back to INDIELOGIN_URL if configured and discovery fails"""
|
|
|
|
def test_deprecation_warning_for_indielogin_url(self):
|
|
"""Logs deprecation warning when INDIELOGIN_URL used"""
|
|
```
|
|
|
|
### 5.2 Integration Tests
|
|
|
|
```python
|
|
class TestLoginWithDiscovery:
|
|
"""End-to-end login flow with endpoint discovery"""
|
|
|
|
def test_login_to_self_hosted_indieauth(self):
|
|
"""Login works with user's declared authorization endpoint"""
|
|
|
|
def test_login_callback_uses_discovered_token_endpoint(self):
|
|
"""Callback exchanges code at discovered endpoint"""
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Open Questions for User
|
|
|
|
1. **Version Scope**: Should this fix be part of v1.5.0 (quality-focused release) or v1.6.0 (new feature)?
|
|
- **Argument for v1.5.0**: This is a critical bug preventing login
|
|
- **Argument for v1.6.0**: Significant changes to authentication flow
|
|
|
|
2. **PKCE Implementation**: Should we add full PKCE support as part of this fix?
|
|
- Current: No PKCE in login flow
|
|
- indielogin.com requires PKCE
|
|
- Many IndieAuth servers support PKCE
|
|
- W3C spec recommends PKCE
|
|
|
|
3. **Migration Path**: How should existing deployments handle this change?
|
|
- Remove INDIELOGIN_URL from config?
|
|
- Add endpoint declarations to their profile?
|
|
- Both (with fallback period)?
|
|
|
|
---
|
|
|
|
## 7. Recommended Implementation Order
|
|
|
|
1. **Phase A**: Extend `discover_endpoints()` to support `indieauth-metadata`
|
|
2. **Phase B**: Update `initiate_login()` to use discovered authorization_endpoint
|
|
3. **Phase C**: Update `handle_callback()` to use discovered token_endpoint
|
|
4. **Phase D**: Add PKCE support (if approved)
|
|
5. **Phase E**: Deprecate INDIELOGIN_URL configuration
|
|
6. **Phase F**: Update documentation and migration guide
|
|
|
|
---
|
|
|
|
## 8. References
|
|
|
|
- [W3C IndieAuth Specification](https://www.w3.org/TR/indieauth/)
|
|
- [IndieAuth Living Spec](https://indieauth.spec.indieweb.org/)
|
|
- ADR-044: Endpoint Discovery Implementation Details
|
|
- ADR-030: External Token Verification Architecture
|
|
- Commit `80bd51e`: Implement IndieAuth endpoint discovery (v1.0.0-rc.5)
|
|
|
|
---
|
|
|
|
## 9. Conclusion
|
|
|
|
The bug is a straightforward oversight: endpoint discovery was implemented for Micropub token verification but not for admin login. The fix involves:
|
|
|
|
1. Reusing the existing `discover_endpoints()` function
|
|
2. Adding `indieauth-metadata` support
|
|
3. Replacing hardcoded INDIELOGIN_URL references
|
|
4. Optionally adding PKCE support
|
|
|
|
**Estimated effort**: 8-12 hours including tests and documentation.
|
|
|
|
**Risk level**: Medium - changes to authentication flow require careful testing.
|