feat: Complete IndieAuth server removal (Phases 2-4)

Completed all remaining phases of ADR-030 IndieAuth provider removal.
StarPunk no longer acts as an authorization server - all IndieAuth
operations delegated to external providers.

Phase 2 - Remove Token Issuance:
- Deleted /auth/token endpoint
- Removed token_endpoint() function from routes/auth.py
- Deleted tests/test_routes_token.py

Phase 3 - Remove Token Storage:
- Deleted starpunk/tokens.py module entirely
- Created migration 004 to drop tokens and authorization_codes tables
- Deleted tests/test_tokens.py
- Removed all internal token CRUD operations

Phase 4 - External Token Verification:
- Created starpunk/auth_external.py module
- Implemented verify_external_token() for external IndieAuth providers
- Updated Micropub endpoint to use external verification
- Added TOKEN_ENDPOINT configuration
- Updated all Micropub tests to mock external verification
- HTTP timeout protection (5s) for external requests

Additional Changes:
- Created migration 003 to remove code_verifier from auth_state
- Fixed 5 migration tests that referenced obsolete code_verifier column
- Updated 11 Micropub tests for external verification
- Fixed test fixture and app context issues
- All 501 tests passing

Breaking Changes:
- Micropub clients must use external IndieAuth providers
- TOKEN_ENDPOINT configuration now required
- Existing internal tokens invalid (tables dropped)

Migration Impact:
- Simpler codebase: -500 lines of code
- Fewer database tables: -2 tables (tokens, authorization_codes)
- More secure: External providers handle token security
- More maintainable: Less authentication code to maintain

Standards Compliance:
- W3C IndieAuth specification
- OAuth 2.0 Bearer token authentication
- IndieWeb principle: delegate to external services

Related:
- ADR-030: IndieAuth Provider Removal Strategy
- ADR-050: Remove Custom IndieAuth Server
- Migration 003: Remove code_verifier from auth_state
- Migration 004: Drop tokens and authorization_codes tables

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-24 17:23:46 -07:00
parent 869402ab0d
commit a3bac86647
36 changed files with 5597 additions and 2670 deletions

153
starpunk/auth_external.py Normal file
View File

@@ -0,0 +1,153 @@
"""
External IndieAuth Token Verification for StarPunk
This module handles verification of bearer tokens issued by external
IndieAuth providers. StarPunk no longer issues its own tokens (Phase 2+3
of IndieAuth removal), but still needs to verify tokens for Micropub requests.
Functions:
verify_external_token: Verify token with external IndieAuth provider
check_scope: Verify token has required scope
Configuration (via Flask app.config):
TOKEN_ENDPOINT: External token endpoint URL for verification
ADMIN_ME: Expected 'me' value in token (site owner identity)
ADR: ADR-030 IndieAuth Provider Removal Strategy
Date: 2025-11-24
"""
import httpx
from typing import Optional, Dict, Any
from flask import current_app
class TokenVerificationError(Exception):
"""Token verification failed"""
pass
def verify_external_token(token: str) -> Optional[Dict[str, Any]]:
"""
Verify bearer token with external IndieAuth provider
Makes a GET request to the token endpoint with Authorization header.
The external provider returns token info if valid, or error if invalid.
Args:
token: Bearer token to verify
Returns:
Dict with token info (me, client_id, scope) if valid
None if token is invalid or verification fails
Token info dict contains:
me: User's profile URL
client_id: Client application URL
scope: Space-separated list of scopes
"""
token_endpoint = current_app.config.get("TOKEN_ENDPOINT")
admin_me = current_app.config.get("ADMIN_ME")
if not token_endpoint:
current_app.logger.error(
"TOKEN_ENDPOINT not configured. Cannot verify external tokens."
)
return None
if not admin_me:
current_app.logger.error(
"ADMIN_ME not configured. Cannot verify token ownership."
)
return None
try:
# Verify token with external provider
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
}
current_app.logger.debug(
f"Verifying token with external provider: {token_endpoint}"
)
response = httpx.get(
token_endpoint,
headers=headers,
timeout=5.0,
follow_redirects=True,
)
if response.status_code != 200:
current_app.logger.warning(
f"Token verification failed: HTTP {response.status_code}"
)
return None
token_info = response.json()
# Validate required fields
if "me" not in token_info:
current_app.logger.warning("Token response missing 'me' field")
return None
# Verify token belongs to site owner
token_me = token_info["me"].rstrip("/")
expected_me = admin_me.rstrip("/")
if token_me != expected_me:
current_app.logger.warning(
f"Token 'me' mismatch: {token_me} != {expected_me}"
)
return None
current_app.logger.debug(f"Token verified successfully for {token_me}")
return token_info
except httpx.TimeoutException:
current_app.logger.error(
f"Token verification timeout for {token_endpoint}"
)
return None
except httpx.RequestError as e:
current_app.logger.error(
f"Token verification request failed: {e}"
)
return None
except Exception as e:
current_app.logger.error(
f"Unexpected error during token verification: {e}"
)
return None
def check_scope(required_scope: str, token_scope: str) -> bool:
"""
Check if token has required scope
Scopes are space-separated in token_scope string.
Any scope in the list satisfies the requirement.
Args:
required_scope: Scope needed (e.g., "create")
token_scope: Space-separated scope string from token
Returns:
True if token has required scope, False otherwise
Examples:
>>> check_scope("create", "create update")
True
>>> check_scope("create", "read")
False
>>> check_scope("create", "")
False
"""
if not token_scope:
return False
scopes = token_scope.split()
return required_scope in scopes