Add complete NexaMFA push MFA system with: - FastAPI backend with PostgreSQL, Redis, OIDC provider, and Prometheus metrics - React TypeScript admin console - Android Kotlin/Jetpack Compose app with biometric authentication - Docker Compose deployment configuration - Gitea CI workflow for backend, frontend, and Android builds - Environment configuration template with security settings - Documentation for security model, deployment
68 lines
2.2 KiB
Python
68 lines
2.2 KiB
Python
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, Request, Response
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from prometheus_client import CONTENT_TYPE_LATEST, Counter, Histogram, generate_latest
|
|
from slowapi import Limiter
|
|
from slowapi.errors import RateLimitExceeded
|
|
from slowapi.middleware import SlowAPIMiddleware
|
|
from slowapi.util import get_remote_address
|
|
from starlette.responses import JSONResponse
|
|
|
|
from app.api import admin, oidc, public
|
|
from app.core.config import get_settings
|
|
from app.core.database import create_all
|
|
from app.core.security import load_or_create_oidc_key
|
|
|
|
settings = get_settings()
|
|
REQUESTS = Counter("nexamfa_http_requests_total", "HTTP requests", ["method", "path", "status"])
|
|
LATENCY = Histogram("nexamfa_http_request_duration_seconds", "HTTP request latency", ["method", "path"])
|
|
limiter = Limiter(key_func=get_remote_address, default_limits=[settings.rate_limit_default])
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
oidc.set_oidc_private_key(load_or_create_oidc_key(settings.oidc_signing_key_pem))
|
|
await create_all()
|
|
yield
|
|
|
|
|
|
app = FastAPI(title="NexaMFA", version="0.1.0", lifespan=lifespan)
|
|
app.state.limiter = limiter
|
|
app.add_middleware(SlowAPIMiddleware)
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.cors_origin_list,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
@app.exception_handler(RateLimitExceeded)
|
|
async def rate_limit_handler(_: Request, exc: RateLimitExceeded):
|
|
return JSONResponse({"detail": "Rate limit exceeded"}, status_code=429)
|
|
|
|
|
|
@app.middleware("http")
|
|
async def metrics_middleware(request: Request, call_next):
|
|
with LATENCY.labels(request.method, request.url.path).time():
|
|
response = await call_next(request)
|
|
REQUESTS.labels(request.method, request.url.path, str(response.status_code)).inc()
|
|
return response
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "ok", "service": "NexaMFA"}
|
|
|
|
|
|
@app.get("/metrics")
|
|
async def metrics():
|
|
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
|
|
|
|
|
|
app.include_router(public.router)
|
|
app.include_router(admin.router)
|
|
app.include_router(oidc.router)
|