# Migration Guide: Fixing Hardcoded IndieAuth Endpoints ## Overview This guide explains how to migrate from the **incorrect** hardcoded endpoint implementation to the **correct** dynamic endpoint discovery implementation that actually follows the IndieAuth specification. ## The Problem We're Fixing ### What's Currently Wrong ```python # WRONG - auth_external.py (hypothetical incorrect implementation) class ExternalTokenVerifier: def __init__(self): # FATAL FLAW: Hardcoded endpoint self.token_endpoint = "https://tokens.indieauth.com/token" def verify_token(self, token): # Uses hardcoded endpoint for ALL users response = requests.get( self.token_endpoint, headers={'Authorization': f'Bearer {token}'} ) return response.json() ``` ### Why It's Wrong 1. **Not IndieAuth**: This completely violates the IndieAuth specification 2. **No User Choice**: Forces all users to use the same provider 3. **Security Risk**: Single point of failure for all authentications 4. **No Flexibility**: Users can't change or choose providers ## The Correct Implementation ### Step 1: Remove Hardcoded Configuration **Remove from config files:** ```ini # DELETE THESE LINES - They are wrong! TOKEN_ENDPOINT=https://tokens.indieauth.com/token AUTHORIZATION_ENDPOINT=https://indieauth.com/auth ``` **Keep only:** ```ini # CORRECT - Only the admin's identity URL ADMIN_ME=https://admin.example.com/ ``` ### Step 2: Implement Endpoint Discovery **Create `endpoint_discovery.py`:** ```python """ IndieAuth Endpoint Discovery Implements: https://www.w3.org/TR/indieauth/#discovery-by-clients """ import re from typing import Dict, Optional from urllib.parse import urljoin, urlparse import httpx from bs4 import BeautifulSoup class EndpointDiscovery: """Discovers IndieAuth endpoints from profile URLs""" def __init__(self, timeout: int = 5): self.timeout = timeout self.client = httpx.Client( timeout=timeout, follow_redirects=True, limits=httpx.Limits(max_redirects=5) ) def discover(self, profile_url: str) -> Dict[str, str]: """ Discover IndieAuth endpoints from a profile URL Args: profile_url: The user's profile URL (their identity) Returns: Dictionary with 'authorization_endpoint' and 'token_endpoint' Raises: DiscoveryError: If discovery fails """ # Ensure HTTPS in production if not self._is_development() and not profile_url.startswith('https://'): raise DiscoveryError("Profile URL must use HTTPS") try: response = self.client.get(profile_url) response.raise_for_status() except Exception as e: raise DiscoveryError(f"Failed to fetch profile: {e}") endpoints = {} # 1. Check HTTP Link headers (highest priority) link_header = response.headers.get('Link', '') if link_header: endpoints.update(self._parse_link_header(link_header, profile_url)) # 2. Check HTML link elements if 'text/html' in response.headers.get('Content-Type', ''): endpoints.update(self._extract_from_html( response.text, profile_url )) # Validate we found required endpoints if 'token_endpoint' not in endpoints: raise DiscoveryError("No token endpoint found in profile") return endpoints def _parse_link_header(self, header: str, base_url: str) -> Dict[str, str]: """Parse HTTP Link header for endpoints""" endpoints = {} # Parse Link: ; rel="relation" pattern = r'<([^>]+)>;\s*rel="([^"]+)"' matches = re.findall(pattern, header) for url, rel in matches: if rel == 'authorization_endpoint': endpoints['authorization_endpoint'] = urljoin(base_url, url) elif rel == 'token_endpoint': endpoints['token_endpoint'] = urljoin(base_url, url) return endpoints def _extract_from_html(self, html: str, base_url: str) -> Dict[str, str]: """Extract endpoints from HTML link elements""" endpoints = {} soup = BeautifulSoup(html, 'html.parser') # Find auth_link = soup.find('link', rel='authorization_endpoint') if auth_link and auth_link.get('href'): endpoints['authorization_endpoint'] = urljoin( base_url, auth_link['href'] ) # Find token_link = soup.find('link', rel='token_endpoint') if token_link and token_link.get('href'): endpoints['token_endpoint'] = urljoin( base_url, token_link['href'] ) return endpoints def _is_development(self) -> bool: """Check if running in development mode""" # Implementation depends on your config system return False class DiscoveryError(Exception): """Raised when endpoint discovery fails""" pass ``` ### Step 3: Update Token Verification **Update `auth_external.py`:** ```python """ External Token Verification with Dynamic Discovery """ import hashlib import time from typing import Dict, Optional import httpx from .endpoint_discovery import EndpointDiscovery, DiscoveryError class ExternalTokenVerifier: """Verifies tokens using discovered IndieAuth endpoints""" def __init__(self, admin_me: str, cache_ttl: int = 300): self.admin_me = admin_me self.discovery = EndpointDiscovery() self.cache = TokenCache(ttl=cache_ttl) def verify_token(self, token: str) -> Dict: """ Verify a token using endpoint discovery Args: token: Bearer token to verify Returns: Token info dict with 'me', 'scope', 'client_id' Raises: TokenVerificationError: If verification fails """ # Check cache first token_hash = self._hash_token(token) cached = self.cache.get(token_hash) if cached: return cached # Discover endpoints for admin try: endpoints = self.discovery.discover(self.admin_me) except DiscoveryError as e: raise TokenVerificationError(f"Endpoint discovery failed: {e}") # Verify with discovered endpoint token_endpoint = endpoints['token_endpoint'] try: response = httpx.get( token_endpoint, headers={'Authorization': f'Bearer {token}'}, timeout=5.0 ) response.raise_for_status() except Exception as e: raise TokenVerificationError(f"Token verification failed: {e}") token_info = response.json() # Validate response if 'me' not in token_info: raise TokenVerificationError("Invalid token response: missing 'me'") # Ensure token is for our admin if self._normalize_url(token_info['me']) != self._normalize_url(self.admin_me): raise TokenVerificationError( f"Token is for {token_info['me']}, expected {self.admin_me}" ) # Check scope scopes = token_info.get('scope', '').split() if 'create' not in scopes: raise TokenVerificationError("Token missing 'create' scope") # Cache successful verification self.cache.store(token_hash, token_info) return token_info def _hash_token(self, token: str) -> str: """Hash token for secure caching""" return hashlib.sha256(token.encode()).hexdigest() def _normalize_url(self, url: str) -> str: """Normalize URL for comparison""" # Add trailing slash if missing if not url.endswith('/'): url += '/' return url.lower() class TokenCache: """Simple in-memory cache for token verifications""" def __init__(self, ttl: int = 300): self.ttl = ttl self.cache = {} def get(self, token_hash: str) -> Optional[Dict]: """Get cached token info if still valid""" if token_hash in self.cache: info, expiry = self.cache[token_hash] if time.time() < expiry: return info else: del self.cache[token_hash] return None def store(self, token_hash: str, info: Dict): """Cache token info""" expiry = time.time() + self.ttl self.cache[token_hash] = (info, expiry) class TokenVerificationError(Exception): """Raised when token verification fails""" pass ``` ### Step 4: Update Micropub Integration **Update Micropub to use discovery-based verification:** ```python # micropub.py from ..auth.auth_external import ExternalTokenVerifier class MicropubEndpoint: def __init__(self, config): self.verifier = ExternalTokenVerifier( admin_me=config['ADMIN_ME'], cache_ttl=config.get('TOKEN_CACHE_TTL', 300) ) def handle_request(self, request): # Extract token auth_header = request.headers.get('Authorization', '') if not auth_header.startswith('Bearer '): return error_response(401, "No bearer token provided") token = auth_header[7:] # Remove 'Bearer ' prefix # Verify using discovery try: token_info = self.verifier.verify_token(token) except TokenVerificationError as e: return error_response(403, str(e)) # Process Micropub request # ... ``` ## Migration Steps ### Phase 1: Preparation 1. **Review current implementation** - Identify all hardcoded endpoint references - Document current configuration 2. **Set up test environment** - Create test profile with IndieAuth links - Set up test IndieAuth provider 3. **Write tests for new implementation** - Unit tests for discovery - Integration tests for verification ### Phase 2: Implementation 1. **Implement discovery module** - Create endpoint_discovery.py - Add comprehensive error handling - Include logging for debugging 2. **Update token verification** - Remove hardcoded endpoints - Integrate discovery module - Add caching layer 3. **Update configuration** - Remove TOKEN_ENDPOINT from config - Ensure ADMIN_ME is set correctly ### Phase 3: Testing 1. **Test discovery with various providers** - indieauth.com - Self-hosted IndieAuth - Custom implementations 2. **Test error conditions** - Profile URL unreachable - No endpoints in profile - Invalid token responses 3. **Performance testing** - Measure discovery latency - Verify cache effectiveness - Test under load ### Phase 4: Deployment 1. **Update documentation** - Explain endpoint discovery - Provide setup instructions - Include troubleshooting guide 2. **Deploy to staging** - Test with real IndieAuth providers - Monitor for issues - Verify performance 3. **Deploy to production** - Clear any existing caches - Monitor closely for first 24 hours - Be ready to roll back if needed ## Verification Checklist After migration, verify: - [ ] No hardcoded endpoints remain in code - [ ] Discovery works with test profiles - [ ] Token verification uses discovered endpoints - [ ] Cache improves performance - [ ] Error messages are clear - [ ] Logs contain useful debugging info - [ ] Documentation is updated - [ ] Tests pass ## Troubleshooting ### Common Issues #### "No token endpoint found" **Cause**: Profile URL doesn't have IndieAuth links **Solution**: 1. Check profile URL returns HTML 2. Verify link elements are present 3. Check for typos in rel attributes #### "Token verification failed" **Cause**: Various issues with endpoint or token **Solution**: 1. Check endpoint is reachable 2. Verify token hasn't expired 3. Ensure 'me' URL matches expected #### "Discovery timeout" **Cause**: Profile URL slow or unreachable **Solution**: 1. Increase timeout if needed 2. Check network connectivity 3. Verify profile URL is correct ## Rollback Plan If issues arise: 1. **Keep old code available** - Tag release before migration - Keep backup of old implementation 2. **Quick rollback procedure** ```bash # Revert to previous version git checkout tags/pre-discovery-migration # Restore old configuration cp config.ini.backup config.ini # Restart application systemctl restart starpunk ``` 3. **Document issues for retry** - What failed? - Error messages - Affected users ## Success Criteria Migration is successful when: 1. All token verifications use discovered endpoints 2. No hardcoded endpoints remain 3. Performance is acceptable (< 500ms uncached) 4. All tests pass 5. Documentation is complete 6. Users can authenticate successfully ## Long-term Benefits After this migration: 1. **True IndieAuth Compliance**: Finally following the specification 2. **User Freedom**: Users control their authentication 3. **Better Security**: No single point of failure 4. **Future Proof**: Ready for new IndieAuth providers 5. **Maintainable**: Cleaner, spec-compliant code --- **Document Version**: 1.0 **Created**: 2024-11-24 **Purpose**: Fix critical IndieAuth implementation error **Priority**: CRITICAL - Must be fixed before V1 release