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>
14 KiB
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:
# 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
-
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)
-
ADR-044 scope: The endpoint discovery ADR focused on token verification for Micropub, not the admin login flow.
-
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 and the IndieAuth Living Spec:
2.1 Discovery Process (Section 4.2)
Clients MUST:
- Fetch the user's profile URL (the "me" URL they enter at login)
- Look for
indieauth-metadatalink relation first (modern approach) - Fall back to
authorization_endpointandtoken_endpointlink relations (legacy) - Use the DISCOVERED endpoints, not hardcoded values
2.2 Discovery Priority
- HTTP Link header with
rel="indieauth-metadata"(preferred) - HTML
<link rel="indieauth-metadata">element - Fall back: HTTP Link header with
rel="authorization_endpoint" - Fall back: HTML
<link rel="authorization_endpoint">element
2.3 Metadata Document
When indieauth-metadata is found, fetch the JSON document which contains:
{
"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):
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:
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:
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:
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:
- Issuer validation (line 350)
- 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:
ALTER TABLE auth_state ADD COLUMN discovered_endpoints TEXT;
Or use session storage (simpler, no migration needed):
session['discovered_endpoints'] = endpoints
3.2.4 Configuration Changes
- Deprecate:
INDIELOGIN_URLconfiguration 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:
- If
indieauth-metadatais found, checkcode_challenge_methods_supported - If S256 is supported, include PKCE parameters
- 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
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
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
-
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
-
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
-
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
- Phase A: Extend
discover_endpoints()to supportindieauth-metadata - Phase B: Update
initiate_login()to use discovered authorization_endpoint - Phase C: Update
handle_callback()to use discovered token_endpoint - Phase D: Add PKCE support (if approved)
- Phase E: Deprecate INDIELOGIN_URL configuration
- Phase F: Update documentation and migration guide
8. References
- W3C IndieAuth Specification
- IndieAuth Living Spec
- 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:
- Reusing the existing
discover_endpoints()function - Adding
indieauth-metadatasupport - Replacing hardcoded INDIELOGIN_URL references
- Optionally adding PKCE support
Estimated effort: 8-12 hours including tests and documentation.
Risk level: Medium - changes to authentication flow require careful testing.