Files
Gondulf/src/gondulf/main.py
Phil Skentelbery 05b4ff7a6b 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>
2025-11-20 14:24:06 -07:00

173 lines
4.9 KiB
Python

"""
Gondulf IndieAuth Server - Main application entry point.
FastAPI application with health check endpoint and core service initialization.
"""
import logging
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from gondulf.config import Config
from gondulf.database.connection import Database
from gondulf.dns import DNSService
from gondulf.email import EmailService
from gondulf.logging_config import configure_logging
from gondulf.routers import authorization, token, verification
from gondulf.storage import CodeStore
# Load configuration at application startup
Config.load()
Config.validate()
# Configure logging
configure_logging(log_level=Config.LOG_LEVEL, debug=Config.DEBUG)
logger = logging.getLogger("gondulf.main")
# Initialize FastAPI application
app = FastAPI(
title="Gondulf IndieAuth Server",
description="Self-hosted IndieAuth authentication server",
version="0.1.0-dev",
)
# Register routers
app.include_router(authorization.router)
app.include_router(token.router)
app.include_router(verification.router)
# Initialize core services
database: Database = None
code_store: CodeStore = None
email_service: EmailService = None
dns_service: DNSService = None
@app.on_event("startup")
async def startup_event() -> None:
"""
Initialize application on startup.
Initializes database, code storage, email service, and DNS service.
"""
global database, code_store, email_service, dns_service
logger.info("Starting Gondulf IndieAuth Server")
logger.info(f"Configuration: DATABASE_URL={Config.DATABASE_URL}")
logger.info(f"Configuration: SMTP_HOST={Config.SMTP_HOST}:{Config.SMTP_PORT}")
logger.info(f"Configuration: DEBUG={Config.DEBUG}")
try:
# Initialize database
logger.info("Initializing database")
database = Database(Config.DATABASE_URL)
database.initialize()
logger.info("Database initialized successfully")
# Initialize code store
logger.info("Initializing code store")
code_store = CodeStore(ttl_seconds=Config.CODE_EXPIRY)
logger.info(f"Code store initialized with TTL={Config.CODE_EXPIRY}s")
# Initialize email service
logger.info("Initializing email service")
email_service = EmailService(
smtp_host=Config.SMTP_HOST,
smtp_port=Config.SMTP_PORT,
smtp_from=Config.SMTP_FROM,
smtp_username=Config.SMTP_USERNAME,
smtp_password=Config.SMTP_PASSWORD,
smtp_use_tls=Config.SMTP_USE_TLS,
)
logger.info("Email service initialized")
# Initialize DNS service
logger.info("Initializing DNS service")
dns_service = DNSService()
logger.info("DNS service initialized")
logger.info("Gondulf startup complete")
except Exception as e:
logger.critical(f"Failed to initialize application: {e}")
raise
@app.on_event("shutdown")
async def shutdown_event() -> None:
"""Clean up resources on shutdown."""
logger.info("Shutting down Gondulf IndieAuth Server")
@app.get("/health")
async def health_check() -> JSONResponse:
"""
Health check endpoint.
Verifies that the application is running and database is accessible.
Does not require authentication.
Returns:
JSON response with health status:
- 200 OK: {"status": "healthy", "database": "connected"}
- 503 Service Unavailable: {"status": "unhealthy", "database": "error", "error": "..."}
"""
# Check database connectivity
if database is None:
logger.warning("Health check failed: database not initialized")
return JSONResponse(
status_code=503,
content={
"status": "unhealthy",
"database": "error",
"error": "database not initialized",
},
)
is_healthy = database.check_health(timeout_seconds=5)
if is_healthy:
logger.debug("Health check passed")
return JSONResponse(
status_code=200,
content={"status": "healthy", "database": "connected"},
)
else:
logger.warning("Health check failed: unable to connect to database")
return JSONResponse(
status_code=503,
content={
"status": "unhealthy",
"database": "error",
"error": "unable to connect to database",
},
)
@app.get("/")
async def root() -> dict:
"""
Root endpoint.
Returns basic server information.
"""
return {
"service": "Gondulf IndieAuth Server",
"version": "0.1.0-dev",
"status": "operational",
}
# Entry point for uvicorn
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"gondulf.main:app",
host="0.0.0.0",
port=8000,
reload=Config.DEBUG,
log_level=Config.LOG_LEVEL.lower(),
)