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:
@@ -277,156 +277,7 @@ class TestVersionDisplay:
|
||||
assert b"0.5.0" in response.data or b"StarPunk v" in response.data
|
||||
|
||||
|
||||
class TestOAuthMetadataEndpoint:
|
||||
"""Test OAuth Client ID Metadata Document endpoint (.well-known/oauth-authorization-server)"""
|
||||
|
||||
def test_oauth_metadata_endpoint_exists(self, client):
|
||||
"""Verify metadata endpoint returns 200 OK"""
|
||||
response = client.get("/.well-known/oauth-authorization-server")
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_oauth_metadata_content_type(self, client):
|
||||
"""Verify response is JSON with correct content type"""
|
||||
response = client.get("/.well-known/oauth-authorization-server")
|
||||
assert response.status_code == 200
|
||||
assert response.content_type == "application/json"
|
||||
|
||||
def test_oauth_metadata_required_fields(self, client, app):
|
||||
"""Verify all required fields are present and valid"""
|
||||
response = client.get("/.well-known/oauth-authorization-server")
|
||||
data = response.get_json()
|
||||
|
||||
# Required fields per IndieAuth spec
|
||||
assert "client_id" in data
|
||||
assert "client_name" in data
|
||||
assert "redirect_uris" in data
|
||||
|
||||
# client_id must match SITE_URL exactly (spec requirement)
|
||||
with app.app_context():
|
||||
assert data["client_id"] == app.config["SITE_URL"]
|
||||
|
||||
# redirect_uris must be array
|
||||
assert isinstance(data["redirect_uris"], list)
|
||||
assert len(data["redirect_uris"]) > 0
|
||||
|
||||
def test_oauth_metadata_optional_fields(self, client):
|
||||
"""Verify recommended optional fields are present"""
|
||||
response = client.get("/.well-known/oauth-authorization-server")
|
||||
data = response.get_json()
|
||||
|
||||
# Recommended fields
|
||||
assert "issuer" in data
|
||||
assert "client_uri" in data
|
||||
assert "grant_types_supported" in data
|
||||
assert "response_types_supported" in data
|
||||
assert "code_challenge_methods_supported" in data
|
||||
assert "token_endpoint_auth_methods_supported" in data
|
||||
|
||||
def test_oauth_metadata_field_values(self, client, app):
|
||||
"""Verify field values are correct"""
|
||||
response = client.get("/.well-known/oauth-authorization-server")
|
||||
data = response.get_json()
|
||||
|
||||
with app.app_context():
|
||||
site_url = app.config["SITE_URL"]
|
||||
|
||||
# Verify URLs
|
||||
assert data["issuer"] == site_url
|
||||
assert data["client_id"] == site_url
|
||||
assert data["client_uri"] == site_url
|
||||
|
||||
# Verify redirect_uris contains auth callback
|
||||
assert f"{site_url}/auth/callback" in data["redirect_uris"]
|
||||
|
||||
# Verify supported methods
|
||||
assert "authorization_code" in data["grant_types_supported"]
|
||||
assert "code" in data["response_types_supported"]
|
||||
assert "S256" in data["code_challenge_methods_supported"]
|
||||
assert "none" in data["token_endpoint_auth_methods_supported"]
|
||||
|
||||
def test_oauth_metadata_redirect_uris_is_array(self, client):
|
||||
"""Verify redirect_uris is array, not string (common pitfall)"""
|
||||
response = client.get("/.well-known/oauth-authorization-server")
|
||||
data = response.get_json()
|
||||
|
||||
assert isinstance(data["redirect_uris"], list)
|
||||
assert not isinstance(data["redirect_uris"], str)
|
||||
|
||||
def test_oauth_metadata_cache_headers(self, client):
|
||||
"""Verify appropriate cache headers are set"""
|
||||
response = client.get("/.well-known/oauth-authorization-server")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Should cache for 24 hours (86400 seconds)
|
||||
assert response.cache_control.max_age == 86400
|
||||
assert response.cache_control.public is True
|
||||
|
||||
def test_oauth_metadata_valid_json(self, client):
|
||||
"""Verify response is valid, parseable JSON"""
|
||||
response = client.get("/.well-known/oauth-authorization-server")
|
||||
assert response.status_code == 200
|
||||
|
||||
# get_json() will raise ValueError if JSON is invalid
|
||||
data = response.get_json()
|
||||
assert data is not None
|
||||
assert isinstance(data, dict)
|
||||
|
||||
def test_oauth_metadata_uses_config_values(self, tmp_path):
|
||||
"""Verify metadata uses config values, not hardcoded strings"""
|
||||
test_data_dir = tmp_path / "oauth_test"
|
||||
test_data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create app with custom config
|
||||
test_config = {
|
||||
"TESTING": True,
|
||||
"DATABASE_PATH": test_data_dir / "starpunk.db",
|
||||
"DATA_PATH": test_data_dir,
|
||||
"NOTES_PATH": test_data_dir / "notes",
|
||||
"SESSION_SECRET": "test-secret",
|
||||
"SITE_URL": "https://custom-site.example.com",
|
||||
"SITE_NAME": "Custom Site Name",
|
||||
"DEV_MODE": False,
|
||||
}
|
||||
app = create_app(config=test_config)
|
||||
client = app.test_client()
|
||||
|
||||
response = client.get("/.well-known/oauth-authorization-server")
|
||||
data = response.get_json()
|
||||
|
||||
# Should use custom config values
|
||||
assert data["client_id"] == "https://custom-site.example.com"
|
||||
assert data["client_name"] == "Custom Site Name"
|
||||
assert data["client_uri"] == "https://custom-site.example.com"
|
||||
assert (
|
||||
"https://custom-site.example.com/auth/callback" in data["redirect_uris"]
|
||||
)
|
||||
|
||||
|
||||
class TestIndieAuthMetadataLink:
|
||||
"""Test indieauth-metadata link in HTML head"""
|
||||
|
||||
def test_indieauth_metadata_link_present(self, client):
|
||||
"""Verify discovery link is present in HTML head"""
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert b'rel="indieauth-metadata"' in response.data
|
||||
|
||||
def test_indieauth_metadata_link_points_to_endpoint(self, client):
|
||||
"""Verify link points to correct endpoint"""
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert b"/.well-known/oauth-authorization-server" in response.data
|
||||
|
||||
def test_indieauth_metadata_link_in_head(self, client):
|
||||
"""Verify link is in <head> section"""
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Simple check: link should appear before <body>
|
||||
html = response.data.decode("utf-8")
|
||||
metadata_link_pos = html.find('rel="indieauth-metadata"')
|
||||
body_pos = html.find("<body>")
|
||||
|
||||
assert metadata_link_pos != -1
|
||||
assert body_pos != -1
|
||||
assert metadata_link_pos < body_pos
|
||||
# OAuth metadata endpoint tests removed in Phase 1 of IndieAuth server removal
|
||||
# The /.well-known/oauth-authorization-server endpoint was removed as part of
|
||||
# removing the built-in IndieAuth authorization server functionality.
|
||||
# See: docs/architecture/indieauth-removal-phases.md
|
||||
|
||||
Reference in New Issue
Block a user