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:
@@ -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",
|
||||
|
||||
@@ -76,6 +76,7 @@ class TestCompleteAuthorizationFlow:
|
||||
consent_data = {
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"response_type": "code", # Authorization flow - exchange at token endpoint
|
||||
"state": "e2e_test_state_12345",
|
||||
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
||||
"code_challenge_method": "S256",
|
||||
@@ -139,6 +140,7 @@ class TestCompleteAuthorizationFlow:
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"response_type": "code", # For state preservation test
|
||||
"state": state,
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
@@ -163,6 +165,7 @@ class TestCompleteAuthorizationFlow:
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"response_type": "code", # Authorization flow - exchange at token endpoint
|
||||
"state": f"flow_{i}",
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
@@ -217,6 +220,7 @@ class TestErrorScenariosE2E:
|
||||
metadata = {
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"response_type": "code", # Authorization flow - exchange at token endpoint
|
||||
"state": "test",
|
||||
"me": "https://user.example.com",
|
||||
"scope": "",
|
||||
@@ -255,6 +259,7 @@ class TestErrorScenariosE2E:
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"response_type": "code", # Authorization flow - exchange at token endpoint
|
||||
"state": "test",
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
@@ -292,6 +297,7 @@ class TestErrorScenariosE2E:
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"response_type": "code", # Authorization flow - exchange at token endpoint
|
||||
"state": "test",
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
@@ -327,6 +333,7 @@ class TestTokenUsageE2E:
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"response_type": "code", # Authorization flow - exchange at token endpoint
|
||||
"state": "test",
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
@@ -362,6 +369,7 @@ class TestTokenUsageE2E:
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"response_type": "code", # Authorization flow - exchange at token endpoint
|
||||
"state": "test",
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
|
||||
433
tests/integration/api/test_response_type_flows.py
Normal file
433
tests/integration/api/test_response_type_flows.py
Normal file
@@ -0,0 +1,433 @@
|
||||
"""
|
||||
Integration tests for IndieAuth response_type flows.
|
||||
|
||||
Tests the two IndieAuth flows per W3C specification:
|
||||
- Authentication flow (response_type=id): Code redeemed at authorization endpoint
|
||||
- Authorization flow (response_type=code): Code redeemed at token endpoint
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flow_app(monkeypatch, tmp_path):
|
||||
"""Create app for flow testing."""
|
||||
db_path = tmp_path / "test.db"
|
||||
|
||||
monkeypatch.setenv("GONDULF_SECRET_KEY", "a" * 32)
|
||||
monkeypatch.setenv("GONDULF_BASE_URL", "https://auth.example.com")
|
||||
monkeypatch.setenv("GONDULF_DATABASE_URL", f"sqlite:///{db_path}")
|
||||
monkeypatch.setenv("GONDULF_DEBUG", "true")
|
||||
|
||||
from gondulf.main import app
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flow_client(flow_app):
|
||||
"""Create test client for flow tests."""
|
||||
with TestClient(flow_app) as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_happ_fetch():
|
||||
"""Mock h-app parser to avoid network calls."""
|
||||
from gondulf.services.happ_parser import ClientMetadata
|
||||
|
||||
metadata = ClientMetadata(
|
||||
name="Test Application",
|
||||
url="https://app.example.com",
|
||||
logo="https://app.example.com/logo.png"
|
||||
)
|
||||
|
||||
with patch('gondulf.services.happ_parser.HAppParser.fetch_and_parse', new_callable=AsyncMock) as mock:
|
||||
mock.return_value = metadata
|
||||
yield mock
|
||||
|
||||
|
||||
class TestResponseTypeValidation:
|
||||
"""Tests for response_type parameter validation."""
|
||||
|
||||
@pytest.fixture
|
||||
def base_params(self):
|
||||
"""Base authorization parameters without response_type."""
|
||||
return {
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"state": "test123",
|
||||
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
||||
"code_challenge_method": "S256",
|
||||
"me": "https://user.example.com",
|
||||
}
|
||||
|
||||
def test_response_type_id_accepted(self, flow_client, base_params, mock_happ_fetch):
|
||||
"""Test response_type=id is accepted."""
|
||||
params = base_params.copy()
|
||||
params["response_type"] = "id"
|
||||
|
||||
response = flow_client.get("/authorize", params=params)
|
||||
assert response.status_code == 200
|
||||
assert "text/html" in response.headers["content-type"]
|
||||
|
||||
def test_response_type_code_accepted(self, flow_client, base_params, mock_happ_fetch):
|
||||
"""Test response_type=code is accepted."""
|
||||
params = base_params.copy()
|
||||
params["response_type"] = "code"
|
||||
|
||||
response = flow_client.get("/authorize", params=params)
|
||||
assert response.status_code == 200
|
||||
assert "text/html" in response.headers["content-type"]
|
||||
|
||||
def test_response_type_defaults_to_id(self, flow_client, base_params, mock_happ_fetch):
|
||||
"""Test missing response_type defaults to 'id'."""
|
||||
# No response_type in params
|
||||
response = flow_client.get("/authorize", params=base_params)
|
||||
assert response.status_code == 200
|
||||
# Form should contain response_type=id
|
||||
assert 'value="id"' in response.text
|
||||
|
||||
def test_invalid_response_type_rejected(self, flow_client, base_params, mock_happ_fetch):
|
||||
"""Test invalid response_type redirects with error."""
|
||||
params = base_params.copy()
|
||||
params["response_type"] = "token" # Invalid
|
||||
|
||||
response = flow_client.get("/authorize", params=params, follow_redirects=False)
|
||||
|
||||
assert response.status_code == 302
|
||||
location = response.headers["location"]
|
||||
assert "error=unsupported_response_type" in location
|
||||
assert "state=test123" in location
|
||||
|
||||
def test_consent_form_includes_response_type(self, flow_client, base_params, mock_happ_fetch):
|
||||
"""Test consent form includes response_type hidden field."""
|
||||
params = base_params.copy()
|
||||
params["response_type"] = "code"
|
||||
|
||||
response = flow_client.get("/authorize", params=params)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert 'name="response_type"' in response.text
|
||||
assert 'value="code"' in response.text
|
||||
|
||||
|
||||
class TestAuthenticationFlow:
|
||||
"""Tests for authentication flow (response_type=id)."""
|
||||
|
||||
@pytest.fixture
|
||||
def auth_code_id_flow(self, flow_client):
|
||||
"""Create an authorization code for the authentication flow."""
|
||||
consent_data = {
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"response_type": "id", # Authentication flow
|
||||
"state": "test123",
|
||||
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
||||
"code_challenge_method": "S256",
|
||||
"scope": "",
|
||||
"me": "https://user.example.com",
|
||||
}
|
||||
|
||||
response = flow_client.post(
|
||||
"/authorize/consent",
|
||||
data=consent_data,
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
location = response.headers["location"]
|
||||
|
||||
from tests.conftest import extract_code_from_redirect
|
||||
code = extract_code_from_redirect(location)
|
||||
return code, consent_data
|
||||
|
||||
def test_auth_code_redemption_at_authorization_endpoint(self, flow_client, auth_code_id_flow):
|
||||
"""Test authentication flow code is redeemed at authorization endpoint."""
|
||||
code, consent_data = auth_code_id_flow
|
||||
|
||||
response = flow_client.post(
|
||||
"/authorize",
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": consent_data["client_id"],
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "me" in data
|
||||
assert data["me"] == "https://user.example.com"
|
||||
# Should NOT have access_token
|
||||
assert "access_token" not in data
|
||||
|
||||
def test_auth_flow_returns_only_me(self, flow_client, auth_code_id_flow):
|
||||
"""Test authentication response contains only 'me' field."""
|
||||
code, consent_data = auth_code_id_flow
|
||||
|
||||
response = flow_client.post(
|
||||
"/authorize",
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": consent_data["client_id"],
|
||||
}
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
assert set(data.keys()) == {"me"}
|
||||
|
||||
def test_auth_flow_code_single_use(self, flow_client, auth_code_id_flow):
|
||||
"""Test authentication code can only be used once."""
|
||||
code, consent_data = auth_code_id_flow
|
||||
|
||||
# First use - should succeed
|
||||
response1 = flow_client.post(
|
||||
"/authorize",
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": consent_data["client_id"],
|
||||
}
|
||||
)
|
||||
assert response1.status_code == 200
|
||||
|
||||
# Second use - should fail
|
||||
response2 = flow_client.post(
|
||||
"/authorize",
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": consent_data["client_id"],
|
||||
}
|
||||
)
|
||||
assert response2.status_code == 400
|
||||
assert response2.json()["error"] == "invalid_grant"
|
||||
|
||||
def test_auth_flow_client_id_mismatch_rejected(self, flow_client, auth_code_id_flow):
|
||||
"""Test wrong client_id is rejected."""
|
||||
code, _ = auth_code_id_flow
|
||||
|
||||
response = flow_client.post(
|
||||
"/authorize",
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": "https://wrong.example.com",
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["error"] == "invalid_client"
|
||||
|
||||
def test_auth_flow_redirect_uri_mismatch_rejected(self, flow_client, auth_code_id_flow):
|
||||
"""Test wrong redirect_uri is rejected when provided."""
|
||||
code, consent_data = auth_code_id_flow
|
||||
|
||||
response = flow_client.post(
|
||||
"/authorize",
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": consent_data["client_id"],
|
||||
"redirect_uri": "https://wrong.example.com/callback",
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["error"] == "invalid_grant"
|
||||
|
||||
def test_auth_flow_id_code_rejected_at_token_endpoint(self, flow_client, auth_code_id_flow):
|
||||
"""Test authentication flow code is rejected at token endpoint."""
|
||||
code, consent_data = auth_code_id_flow
|
||||
|
||||
response = flow_client.post(
|
||||
"/token",
|
||||
data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": consent_data["client_id"],
|
||||
"redirect_uri": consent_data["redirect_uri"],
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
# Should indicate wrong endpoint
|
||||
data = response.json()["detail"]
|
||||
assert data["error"] == "invalid_grant"
|
||||
assert "authorization endpoint" in data["error_description"]
|
||||
|
||||
def test_auth_flow_cache_headers(self, flow_client, auth_code_id_flow):
|
||||
"""Test authentication response has no-cache headers."""
|
||||
code, consent_data = auth_code_id_flow
|
||||
|
||||
response = flow_client.post(
|
||||
"/authorize",
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": consent_data["client_id"],
|
||||
}
|
||||
)
|
||||
|
||||
assert response.headers.get("Cache-Control") == "no-store"
|
||||
assert response.headers.get("Pragma") == "no-cache"
|
||||
|
||||
|
||||
class TestAuthorizationFlow:
|
||||
"""Tests for authorization flow (response_type=code)."""
|
||||
|
||||
@pytest.fixture
|
||||
def auth_code_code_flow(self, flow_client):
|
||||
"""Create an authorization code for the authorization flow."""
|
||||
consent_data = {
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"response_type": "code", # Authorization flow
|
||||
"state": "test456",
|
||||
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
||||
"code_challenge_method": "S256",
|
||||
"scope": "profile",
|
||||
"me": "https://user.example.com",
|
||||
}
|
||||
|
||||
response = flow_client.post(
|
||||
"/authorize/consent",
|
||||
data=consent_data,
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
location = response.headers["location"]
|
||||
|
||||
from tests.conftest import extract_code_from_redirect
|
||||
code = extract_code_from_redirect(location)
|
||||
return code, consent_data
|
||||
|
||||
def test_code_flow_redemption_at_token_endpoint(self, flow_client, auth_code_code_flow):
|
||||
"""Test authorization flow code is redeemed at token endpoint."""
|
||||
code, consent_data = auth_code_code_flow
|
||||
|
||||
response = flow_client.post(
|
||||
"/token",
|
||||
data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": consent_data["client_id"],
|
||||
"redirect_uri": consent_data["redirect_uri"],
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
assert "me" in data
|
||||
assert data["me"] == "https://user.example.com"
|
||||
assert data["token_type"] == "Bearer"
|
||||
|
||||
def test_code_flow_code_rejected_at_authorization_endpoint(self, flow_client, auth_code_code_flow):
|
||||
"""Test authorization flow code is rejected at authorization endpoint."""
|
||||
code, consent_data = auth_code_code_flow
|
||||
|
||||
response = flow_client.post(
|
||||
"/authorize",
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": consent_data["client_id"],
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
# Should indicate wrong endpoint
|
||||
data = response.json()
|
||||
assert data["error"] == "invalid_grant"
|
||||
assert "token endpoint" in data["error_description"]
|
||||
|
||||
def test_code_flow_single_use(self, flow_client, auth_code_code_flow):
|
||||
"""Test authorization code can only be used once."""
|
||||
code, consent_data = auth_code_code_flow
|
||||
|
||||
# First use - should succeed
|
||||
response1 = flow_client.post(
|
||||
"/token",
|
||||
data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": consent_data["client_id"],
|
||||
"redirect_uri": consent_data["redirect_uri"],
|
||||
}
|
||||
)
|
||||
assert response1.status_code == 200
|
||||
|
||||
# Second use - should fail
|
||||
response2 = flow_client.post(
|
||||
"/token",
|
||||
data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": consent_data["client_id"],
|
||||
"redirect_uri": consent_data["redirect_uri"],
|
||||
}
|
||||
)
|
||||
assert response2.status_code == 400
|
||||
|
||||
|
||||
class TestMetadataEndpoint:
|
||||
"""Tests for server metadata endpoint."""
|
||||
|
||||
def test_metadata_includes_both_response_types(self, flow_client):
|
||||
"""Test metadata advertises both response types."""
|
||||
response = flow_client.get("/.well-known/oauth-authorization-server")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "response_types_supported" in data
|
||||
assert "code" in data["response_types_supported"]
|
||||
assert "id" in data["response_types_supported"]
|
||||
|
||||
def test_metadata_includes_code_challenge_method(self, flow_client):
|
||||
"""Test metadata advertises S256 code challenge method."""
|
||||
response = flow_client.get("/.well-known/oauth-authorization-server")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "code_challenge_methods_supported" in data
|
||||
assert "S256" in data["code_challenge_methods_supported"]
|
||||
|
||||
|
||||
class TestErrorScenarios:
|
||||
"""Tests for error handling in both flows."""
|
||||
|
||||
def test_invalid_code_at_authorization_endpoint(self, flow_client):
|
||||
"""Test invalid code returns error at authorization endpoint."""
|
||||
response = flow_client.post(
|
||||
"/authorize",
|
||||
data={
|
||||
"code": "invalid_code_12345",
|
||||
"client_id": "https://app.example.com",
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["error"] == "invalid_grant"
|
||||
|
||||
def test_missing_code_at_authorization_endpoint(self, flow_client):
|
||||
"""Test missing code returns validation error."""
|
||||
response = flow_client.post(
|
||||
"/authorize",
|
||||
data={
|
||||
"client_id": "https://app.example.com",
|
||||
}
|
||||
)
|
||||
|
||||
# FastAPI returns 422 for missing required form field
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_missing_client_id_at_authorization_endpoint(self, flow_client):
|
||||
"""Test missing client_id returns validation error."""
|
||||
response = flow_client.post(
|
||||
"/authorize",
|
||||
data={
|
||||
"code": "some_code",
|
||||
}
|
||||
)
|
||||
|
||||
# FastAPI returns 422 for missing required form field
|
||||
assert response.status_code == 422
|
||||
@@ -32,13 +32,14 @@ def token_client(token_app):
|
||||
|
||||
@pytest.fixture
|
||||
def setup_auth_code(token_app, test_code_storage):
|
||||
"""Setup a valid authorization code for testing."""
|
||||
"""Setup a valid authorization code for testing (authorization flow)."""
|
||||
from gondulf.dependencies import get_code_storage
|
||||
|
||||
code = "integration_test_code_12345"
|
||||
metadata = {
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"response_type": "code", # Authorization flow - exchange at token endpoint
|
||||
"state": "xyz123",
|
||||
"me": "https://user.example.com",
|
||||
"scope": "",
|
||||
@@ -212,6 +213,7 @@ class TestTokenExchangeErrors:
|
||||
metadata = {
|
||||
"client_id": "https://app.example.com",
|
||||
"redirect_uri": "https://app.example.com/callback",
|
||||
"response_type": "code", # Authorization flow
|
||||
"state": "xyz123",
|
||||
"me": "https://user.example.com",
|
||||
"scope": "",
|
||||
|
||||
@@ -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'."""
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
Reference in New Issue
Block a user