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:
2025-11-20 14:24:06 -07:00
parent 074f74002c
commit 05b4ff7a6b
18 changed files with 4049 additions and 26 deletions

View File

@@ -5,8 +5,10 @@ Provides simple dict-based storage for email verification codes and authorizatio
codes with automatic expiration checking on access.
"""
import json
import logging
import time
from typing import Union
logger = logging.getLogger("gondulf.storage")
@@ -26,21 +28,22 @@ class CodeStore:
Args:
ttl_seconds: Time-to-live for codes in seconds (default: 600 = 10 minutes)
"""
self._store: dict[str, tuple[str, float]] = {}
self._store: dict[str, tuple[Union[str, dict], float]] = {}
self._ttl = ttl_seconds
logger.debug(f"CodeStore initialized with TTL={ttl_seconds}s")
def store(self, key: str, code: str) -> None:
def store(self, key: str, value: Union[str, dict], ttl: int | None = None) -> None:
"""
Store verification code with expiry timestamp.
Store value (string or dict) with expiry timestamp.
Args:
key: Storage key (typically email address or similar identifier)
code: Verification code to store
key: Storage key (typically email address or code identifier)
value: Value to store (string for simple codes, dict for authorization code metadata)
ttl: Optional TTL override in seconds (default: use instance TTL)
"""
expiry = time.time() + self._ttl
self._store[key] = (code, expiry)
logger.debug(f"Code stored for key={key} expires_in={self._ttl}s")
expiry = time.time() + (ttl if ttl is not None else self._ttl)
self._store[key] = (value, expiry)
logger.debug(f"Value stored for key={key} expires_in={ttl if ttl is not None else self._ttl}s")
def verify(self, key: str, code: str) -> bool:
"""
@@ -78,29 +81,29 @@ class CodeStore:
logger.info(f"Code verified successfully for key={key}")
return True
def get(self, key: str) -> str | None:
def get(self, key: str) -> Union[str, dict, None]:
"""
Get code without removing it (for testing/debugging).
Get value without removing it.
Checks expiration and removes expired codes.
Checks expiration and removes expired values.
Args:
key: Storage key to retrieve
Returns:
Code if exists and not expired, None otherwise
Value (str or dict) if exists and not expired, None otherwise
"""
if key not in self._store:
return None
stored_code, expiry = self._store[key]
stored_value, expiry = self._store[key]
# Check expiration
if time.time() > expiry:
del self._store[key]
return None
return stored_code
return stored_value
def delete(self, key: str) -> None:
"""