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>
122 lines
4.1 KiB
Python
122 lines
4.1 KiB
Python
"""
|
|
Micropub endpoint routes for StarPunk
|
|
|
|
Implements the W3C Micropub specification for creating posts via
|
|
external IndieWeb clients.
|
|
|
|
Endpoints:
|
|
GET/POST /micropub - Main Micropub endpoint
|
|
GET: Query operations (config, source, syndicate-to)
|
|
POST: Action operations (create in V1, update/delete in future)
|
|
|
|
Authentication:
|
|
Bearer token authentication required for all endpoints.
|
|
Token must have appropriate scope for requested operation.
|
|
|
|
References:
|
|
- W3C Micropub Specification: https://www.w3.org/TR/micropub/
|
|
- ADR-028: Micropub Implementation Strategy
|
|
- ADR-029: Micropub IndieAuth Integration Strategy
|
|
"""
|
|
|
|
from flask import Blueprint, current_app, request
|
|
|
|
from starpunk.micropub import (
|
|
MicropubError,
|
|
extract_bearer_token,
|
|
error_response,
|
|
handle_create,
|
|
handle_query,
|
|
)
|
|
from starpunk.auth_external import verify_external_token
|
|
|
|
# Create blueprint
|
|
bp = Blueprint("micropub", __name__)
|
|
|
|
|
|
@bp.route("/micropub", methods=["GET", "POST"])
|
|
def micropub_endpoint():
|
|
"""
|
|
Main Micropub endpoint for all operations
|
|
|
|
GET requests:
|
|
Handle query operations via q= parameter:
|
|
- q=config: Return server capabilities
|
|
- q=source&url={url}: Return post source
|
|
- q=syndicate-to: Return syndication targets
|
|
|
|
POST requests:
|
|
Handle action operations (form-encoded or JSON):
|
|
- action=create (or no action): Create new post
|
|
- action=update: Update existing post (not supported in V1)
|
|
- action=delete: Delete post (not supported in V1)
|
|
|
|
Authentication:
|
|
Requires valid bearer token in Authorization header or
|
|
access_token parameter.
|
|
|
|
Returns:
|
|
GET: JSON response with query results
|
|
POST create: 201 Created with Location header
|
|
POST other: Error responses
|
|
|
|
Error responses follow OAuth 2.0 format:
|
|
{
|
|
"error": "error_code",
|
|
"error_description": "Human-readable description"
|
|
}
|
|
"""
|
|
# Extract and verify token
|
|
token = extract_bearer_token(request)
|
|
if not token:
|
|
return error_response("unauthorized", "No access token provided", 401)
|
|
|
|
token_info = verify_external_token(token)
|
|
if not token_info:
|
|
return error_response("unauthorized", "Invalid or expired access token", 401)
|
|
|
|
# Handle query endpoints (GET requests)
|
|
if request.method == "GET":
|
|
try:
|
|
return handle_query(request.args.to_dict(), token_info)
|
|
except MicropubError as e:
|
|
return error_response(e.error, e.error_description, e.status_code)
|
|
except Exception as e:
|
|
current_app.logger.error(f"Micropub query error: {e}")
|
|
return error_response("server_error", "An unexpected error occurred", 500)
|
|
|
|
# Handle action endpoints (POST requests)
|
|
content_type = request.headers.get("Content-Type", "")
|
|
|
|
try:
|
|
# Parse request based on content type
|
|
if "application/json" in content_type:
|
|
data = request.get_json() or {}
|
|
action = data.get("action", "create")
|
|
else:
|
|
# Form-encoded or multipart (V1 only supports form-encoded)
|
|
data = request.form.to_dict(flat=False)
|
|
action = data.get("action", ["create"])[0]
|
|
|
|
# Route to appropriate handler
|
|
if action == "create":
|
|
return handle_create(data, token_info)
|
|
elif action == "update":
|
|
# V1: Update not supported
|
|
return error_response(
|
|
"invalid_request", "Update action not supported in V1", 400
|
|
)
|
|
elif action == "delete":
|
|
# V1: Delete not supported
|
|
return error_response(
|
|
"invalid_request", "Delete action not supported in V1", 400
|
|
)
|
|
else:
|
|
return error_response("invalid_request", f"Unknown action: {action}", 400)
|
|
|
|
except MicropubError as e:
|
|
return error_response(e.error, e.error_description, e.status_code)
|
|
except Exception as e:
|
|
current_app.logger.error(f"Micropub action error: {e}")
|
|
return error_response("server_error", "An unexpected error occurred", 500)
|