113 lines
3.0 KiB
Python
113 lines
3.0 KiB
Python
"""
|
|
Minimal FastAPI WebAuthn server with WebSocket support for passkey registration and authentication.
|
|
|
|
This module provides a simple WebAuthn implementation that:
|
|
- Uses WebSocket for real-time communication
|
|
- Supports Resident Keys (discoverable credentials) for passwordless authentication
|
|
- Maintains challenges locally per connection
|
|
- Uses async SQLite database for persistent storage of users and credentials
|
|
- Enables true passwordless authentication where users don't need to enter a user_name
|
|
"""
|
|
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
from fastapi import (
|
|
FastAPI,
|
|
Request,
|
|
Response,
|
|
)
|
|
from fastapi.responses import (
|
|
FileResponse,
|
|
JSONResponse,
|
|
)
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from ..db import sql
|
|
from .api_handlers import (
|
|
register_api_routes,
|
|
validate_token,
|
|
)
|
|
from .reset_handlers import register_reset_routes
|
|
from .ws_handlers import ws_app
|
|
|
|
STATIC_DIR = Path(__file__).parent.parent / "frontend-build"
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
await sql.init_database()
|
|
yield
|
|
|
|
|
|
app = FastAPI(lifespan=lifespan)
|
|
|
|
# Mount the WebSocket subapp
|
|
app.mount("/auth/ws", ws_app)
|
|
|
|
# Register API routes
|
|
register_api_routes(app)
|
|
register_reset_routes(app)
|
|
|
|
|
|
@app.get("/auth/forward-auth")
|
|
async def forward_authentication(request: Request):
|
|
"""A verification endpoint to use with Caddy forward_auth or Nginx auth_request."""
|
|
# Create a dummy response object for internal validation (we won't use it for cookies)
|
|
response = Response()
|
|
|
|
result = await validate_token(request, response)
|
|
if result.get("status") != "success":
|
|
# Serve the index.html of the authentication app if not authenticated
|
|
return FileResponse(
|
|
STATIC_DIR / "index.html",
|
|
status_code=401,
|
|
headers={"www-authenticate": "PrivateToken"},
|
|
)
|
|
|
|
# If authenticated, return a success response
|
|
return Response(
|
|
status_code=204,
|
|
headers={"x-auth-user-id": result["user_id"]},
|
|
)
|
|
|
|
|
|
# Serve static files
|
|
app.mount(
|
|
"/auth/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="static assets"
|
|
)
|
|
|
|
|
|
@app.get("/auth")
|
|
async def redirect_to_index():
|
|
"""Serve the main authentication app."""
|
|
return FileResponse(STATIC_DIR / "index.html")
|
|
|
|
|
|
# Catch-all route for SPA - serve index.html for all non-API routes
|
|
@app.get("/{path:path}")
|
|
async def spa_handler(request: Request, path: str):
|
|
"""Serve the Vue SPA for all routes (except API and static)"""
|
|
if "text/html" not in request.headers.get("accept", ""):
|
|
return JSONResponse({"error": "Not Found"}, status_code=404)
|
|
return FileResponse(STATIC_DIR / "index.html")
|
|
|
|
|
|
def main():
|
|
"""Entry point for the application"""
|
|
import uvicorn
|
|
|
|
uvicorn.run(
|
|
"passkey.fastapi.main:app",
|
|
host="localhost",
|
|
port=4401,
|
|
reload=True,
|
|
log_level="info",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
main()
|