feat(phase-3): implement token endpoint and OAuth 2.0 flow
Phase 3 Implementation: - Token service with secure token generation and validation - Token endpoint (POST /token) with OAuth 2.0 compliance - Database migration 003 for tokens table - Authorization code validation and single-use enforcement Phase 1 Updates: - Enhanced CodeStore to support dict values with JSON serialization - Maintains backward compatibility Phase 2 Updates: - Authorization codes now include PKCE fields, used flag, timestamps - Complete metadata structure for token exchange Security: - 256-bit cryptographically secure tokens (secrets.token_urlsafe) - SHA-256 hashed storage (no plaintext) - Constant-time comparison for validation - Single-use code enforcement with replay detection Testing: - 226 tests passing (100%) - 87.27% coverage (exceeds 80% requirement) - OAuth 2.0 compliance verified This completes the v1.0.0 MVP with full IndieAuth authorization code flow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -216,3 +216,65 @@ class TestCodeStore:
|
||||
|
||||
assert store.verify("test@example.com", "old_code") is False
|
||||
assert store.verify("test@example.com", "new_code") is True
|
||||
|
||||
def test_store_dict_value(self):
|
||||
"""Test storing dict values for authorization code metadata."""
|
||||
store = CodeStore(ttl_seconds=60)
|
||||
|
||||
metadata = {
|
||||
"client_id": "https://client.example.com",
|
||||
"redirect_uri": "https://client.example.com/callback",
|
||||
"state": "xyz123",
|
||||
"me": "https://user.example.com",
|
||||
"scope": "profile",
|
||||
"code_challenge": "abc123",
|
||||
"code_challenge_method": "S256",
|
||||
"created_at": 1234567890,
|
||||
"expires_at": 1234568490,
|
||||
"used": False
|
||||
}
|
||||
|
||||
store.store("auth_code_123", metadata)
|
||||
retrieved = store.get("auth_code_123")
|
||||
|
||||
assert retrieved is not None
|
||||
assert isinstance(retrieved, dict)
|
||||
assert retrieved["client_id"] == "https://client.example.com"
|
||||
assert retrieved["used"] is False
|
||||
|
||||
def test_store_dict_with_custom_ttl(self):
|
||||
"""Test storing dict values with custom TTL."""
|
||||
store = CodeStore(ttl_seconds=60)
|
||||
|
||||
metadata = {"client_id": "https://client.example.com", "used": False}
|
||||
|
||||
store.store("auth_code_123", metadata, ttl=120)
|
||||
retrieved = store.get("auth_code_123")
|
||||
|
||||
assert retrieved is not None
|
||||
assert isinstance(retrieved, dict)
|
||||
|
||||
def test_dict_value_expiration(self):
|
||||
"""Test dict values expire correctly."""
|
||||
store = CodeStore(ttl_seconds=1)
|
||||
|
||||
metadata = {"client_id": "https://client.example.com"}
|
||||
store.store("auth_code_123", metadata)
|
||||
|
||||
# Wait for expiration
|
||||
time.sleep(1.1)
|
||||
|
||||
assert store.get("auth_code_123") is None
|
||||
|
||||
def test_delete_dict_value(self):
|
||||
"""Test deleting dict values."""
|
||||
store = CodeStore(ttl_seconds=60)
|
||||
|
||||
metadata = {"client_id": "https://client.example.com"}
|
||||
store.store("auth_code_123", metadata)
|
||||
|
||||
assert store.get("auth_code_123") is not None
|
||||
|
||||
store.delete("auth_code_123")
|
||||
|
||||
assert store.get("auth_code_123") is None
|
||||
|
||||
Reference in New Issue
Block a user