feat(auth): implement response_type=id authentication flow

Implements both IndieAuth flows per W3C specification:
- Authentication flow (response_type=id): Code redeemed at authorization endpoint, returns only user identity
- Authorization flow (response_type=code): Code redeemed at token endpoint, returns access token

Changes:
- Authorization endpoint GET: Accept response_type=id (default) and code
- Authorization endpoint POST: Handle code verification for authentication flow
- Token endpoint: Validate response_type=code for authorization flow
- Store response_type in authorization code metadata
- Update metadata endpoint: response_types_supported=[code, id], code_challenge_methods_supported=[S256]

The default behavior now correctly defaults to response_type=id when omitted, per IndieAuth spec section 5.2.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-22 12:23:20 -07:00
parent 9dfa77633a
commit 052d3ad3e1
11 changed files with 684 additions and 28 deletions

View File

@@ -78,11 +78,11 @@ class TestMetadataEndpoint:
assert data["token_endpoint"] == "https://auth.example.com/token"
def test_metadata_response_types_supported(self, client):
"""Test response_types_supported contains only 'code'."""
"""Test response_types_supported contains both 'code' and 'id'."""
response = client.get("/.well-known/oauth-authorization-server")
data = response.json()
assert data["response_types_supported"] == ["code"]
assert data["response_types_supported"] == ["code", "id"]
def test_metadata_grant_types_supported(self, client):
"""Test grant_types_supported contains only 'authorization_code'."""
@@ -91,12 +91,12 @@ class TestMetadataEndpoint:
assert data["grant_types_supported"] == ["authorization_code"]
def test_metadata_code_challenge_methods_empty(self, client):
"""Test code_challenge_methods_supported is empty array."""
def test_metadata_code_challenge_methods_supported(self, client):
"""Test code_challenge_methods_supported contains S256."""
response = client.get("/.well-known/oauth-authorization-server")
data = response.json()
assert data["code_challenge_methods_supported"] == []
assert data["code_challenge_methods_supported"] == ["S256"]
def test_metadata_token_endpoint_auth_methods(self, client):
"""Test token_endpoint_auth_methods_supported contains 'none'."""

View File

@@ -71,11 +71,12 @@ def client(test_config, test_database, test_code_storage, test_token_service):
@pytest.fixture
def valid_auth_code(test_code_storage):
"""Create a valid authorization code."""
"""Create a valid authorization code (authorization flow)."""
code = "test_auth_code_12345"
metadata = {
"client_id": "https://client.example.com",
"redirect_uri": "https://client.example.com/callback",
"response_type": "code", # Authorization flow - exchange at token endpoint
"state": "xyz123",
"me": "https://user.example.com",
"scope": "",