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:
@@ -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:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user