2025-07-03 17:02:49 -06:00

107 lines
3.2 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 SQLite database for persistent storage of users and credentials
- Enables true passwordless authentication where users don't need to enter a username
"""
from pathlib import Path
import db
import uuid7
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from passkeyauth.passkey import Passkey
STATIC_DIR = Path(__file__).parent.parent / "static"
passkey = Passkey(
rp_id="localhost",
rp_name="Passkey Auth",
origin="http://localhost:8000",
)
app = FastAPI(title="Passkey Auth")
@app.websocket("/ws/new_user_registration")
async def websocket_register_new(ws: WebSocket):
"""Register a new user and with a new passkey credential."""
await ws.accept()
try:
form = await ws.receive_json()
user_id = uuid7.create().bytes
user_name = form["user_name"]
await register_chat(ws, user_id, username)
# Store the user in the database
await db.create_user(user_name, user_id)
await ws.send_json({"status": "success", "user_id": user_id.hex()})
except WebSocketDisconnect:
pass
async def register_chat(ws: WebSocket, user_id: bytes, username: str):
"""Generate registration options and send them to the client."""
options, challenge = passkey.reg_generate_options(
user_id=user_id,
username=username,
)
await ws.send_text(options)
# Wait for the client to use his authenticator to register
credential = passkey.reg_credential(await ws.receive_json())
passkey.reg_verify(credential, challenge)
@app.websocket("/ws/authenticate")
async def websocket_authenticate(ws: WebSocket):
await ws.accept()
try:
options = passkey.auth_generate_options()
await ws.send_json(options)
# Wait for the client to use his authenticator to authenticate
credential = passkey.auth_credential(await ws.receive_json())
# Fetch from the database by credential ID
stored_cred = await db.fetch_credential(credential.raw_id)
# Verify the credential matches the stored data, that is also updated
passkey.auth_verify(credential, stored_cred)
# Update the credential in the database
await db.update_credential(stored_cred)
except WebSocketDisconnect:
pass
# Serve static files
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
@app.get("/")
async def get_index():
"""Serve the main HTML page"""
return FileResponse(STATIC_DIR / "index.html")
def main():
"""Entry point for the application"""
import uvicorn
uvicorn.run(
"passkeyauth.main:app",
host="0.0.0.0",
port=8000,
reload=True,
log_level="info",
)
# Initialize database on startup
db.init_database()
if __name__ == "__main__":
main()