Complete architectural documentation for: - Migration race condition fix with database locking - IndieAuth endpoint discovery implementation - Security considerations and migration guides New documentation: - ADR-030-CORRECTED: IndieAuth endpoint discovery decision - ADR-031: Endpoint discovery implementation details - Architecture docs on endpoint discovery - Migration guide for removed TOKEN_ENDPOINT - Security analysis of endpoint discovery - Implementation and analysis reports
13 KiB
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
# 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
- Not IndieAuth: This completely violates the IndieAuth specification
- No User Choice: Forces all users to use the same provider
- Security Risk: Single point of failure for all authentications
- No Flexibility: Users can't change or choose providers
The Correct Implementation
Step 1: Remove Hardcoded Configuration
Remove from config files:
# DELETE THESE LINES - They are wrong!
TOKEN_ENDPOINT=https://tokens.indieauth.com/token
AUTHORIZATION_ENDPOINT=https://indieauth.com/auth
Keep only:
# CORRECT - Only the admin's identity URL
ADMIN_ME=https://admin.example.com/
Step 2: Implement Endpoint Discovery
Create endpoint_discovery.py:
"""
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: <url>; 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 <link rel="authorization_endpoint" href="...">
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 <link rel="token_endpoint" href="...">
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:
"""
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:
# 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
-
Review current implementation
- Identify all hardcoded endpoint references
- Document current configuration
-
Set up test environment
- Create test profile with IndieAuth links
- Set up test IndieAuth provider
-
Write tests for new implementation
- Unit tests for discovery
- Integration tests for verification
Phase 2: Implementation
-
Implement discovery module
- Create endpoint_discovery.py
- Add comprehensive error handling
- Include logging for debugging
-
Update token verification
- Remove hardcoded endpoints
- Integrate discovery module
- Add caching layer
-
Update configuration
- Remove TOKEN_ENDPOINT from config
- Ensure ADMIN_ME is set correctly
Phase 3: Testing
-
Test discovery with various providers
- indieauth.com
- Self-hosted IndieAuth
- Custom implementations
-
Test error conditions
- Profile URL unreachable
- No endpoints in profile
- Invalid token responses
-
Performance testing
- Measure discovery latency
- Verify cache effectiveness
- Test under load
Phase 4: Deployment
-
Update documentation
- Explain endpoint discovery
- Provide setup instructions
- Include troubleshooting guide
-
Deploy to staging
- Test with real IndieAuth providers
- Monitor for issues
- Verify performance
-
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:
- Check profile URL returns HTML
- Verify link elements are present
- Check for typos in rel attributes
"Token verification failed"
Cause: Various issues with endpoint or token
Solution:
- Check endpoint is reachable
- Verify token hasn't expired
- Ensure 'me' URL matches expected
"Discovery timeout"
Cause: Profile URL slow or unreachable
Solution:
- Increase timeout if needed
- Check network connectivity
- Verify profile URL is correct
Rollback Plan
If issues arise:
-
Keep old code available
- Tag release before migration
- Keep backup of old implementation
-
Quick rollback procedure
# Revert to previous version git checkout tags/pre-discovery-migration # Restore old configuration cp config.ini.backup config.ini # Restart application systemctl restart starpunk -
Document issues for retry
- What failed?
- Error messages
- Affected users
Success Criteria
Migration is successful when:
- All token verifications use discovered endpoints
- No hardcoded endpoints remain
- Performance is acceptable (< 500ms uncached)
- All tests pass
- Documentation is complete
- Users can authenticate successfully
Long-term Benefits
After this migration:
- True IndieAuth Compliance: Finally following the specification
- User Freedom: Users control their authentication
- Better Security: No single point of failure
- Future Proof: Ready for new IndieAuth providers
- 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