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

@@ -131,7 +131,7 @@ def test_code_storage():
@pytest.fixture
def valid_auth_code(test_code_storage) -> tuple[str, dict]:
"""
Create a valid authorization code with metadata.
Create a valid authorization code with metadata (authorization flow).
Args:
test_code_storage: Code storage fixture
@@ -143,6 +143,7 @@ def valid_auth_code(test_code_storage) -> tuple[str, dict]:
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": "",
@@ -159,7 +160,7 @@ def valid_auth_code(test_code_storage) -> tuple[str, dict]:
@pytest.fixture
def expired_auth_code(test_code_storage) -> tuple[str, dict]:
"""
Create an expired authorization code.
Create an expired authorization code (authorization flow).
Returns:
Tuple of (code, metadata) where the code is expired
@@ -169,6 +170,7 @@ def expired_auth_code(test_code_storage) -> tuple[str, dict]:
metadata = {
"client_id": "https://client.example.com",
"redirect_uri": "https://client.example.com/callback",
"response_type": "code", # Authorization flow
"state": "xyz123",
"me": "https://user.example.com",
"scope": "",
@@ -186,7 +188,7 @@ def expired_auth_code(test_code_storage) -> tuple[str, dict]:
@pytest.fixture
def used_auth_code(test_code_storage) -> tuple[str, dict]:
"""
Create an already-used authorization code.
Create an already-used authorization code (authorization flow).
Returns:
Tuple of (code, metadata) where the code is marked as used
@@ -195,6 +197,7 @@ def used_auth_code(test_code_storage) -> tuple[str, dict]:
metadata = {
"client_id": "https://client.example.com",
"redirect_uri": "https://client.example.com/callback",
"response_type": "code", # Authorization flow
"state": "xyz123",
"me": "https://user.example.com",
"scope": "",
@@ -474,13 +477,13 @@ def malicious_client() -> dict[str, Any]:
@pytest.fixture
def valid_auth_request() -> dict[str, str]:
"""
Complete valid authorization request parameters.
Complete valid authorization request parameters (for authorization flow).
Returns:
Dict with all required authorization parameters
"""
return {
"response_type": "code",
"response_type": "code", # Authorization flow - exchange at token endpoint
"client_id": "https://app.example.com",
"redirect_uri": "https://app.example.com/callback",
"state": "random_state_12345",