Add signed cookie auth
This commit is contained in:
106
app/auth.py
106
app/auth.py
@@ -1 +1,107 @@
|
||||
"""Cookie signing helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
import time
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import HTTPException, Request, Response, WebSocket, status
|
||||
from itsdangerous import BadSignature, URLSafeSerializer
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
STUDENT_COOKIE = "qz_student"
|
||||
ADMIN_COOKIE = "qz_admin"
|
||||
STUDENT_MAX_AGE = 31_536_000
|
||||
ADMIN_MAX_AGE = 86_400
|
||||
|
||||
|
||||
def serializer(settings: Settings) -> URLSafeSerializer:
|
||||
if not settings.secret_key:
|
||||
raise HTTPException(status_code=500, detail="QUIZ_SECRET_KEY is not configured")
|
||||
return URLSafeSerializer(settings.secret_key, salt="quiz-cookie-v1")
|
||||
|
||||
|
||||
def sign_student(settings: Settings, sid: str, student_id: str, name: str, cookie_id: str | None = None) -> str:
|
||||
payload = {
|
||||
"sid": sid,
|
||||
"student_id": student_id.strip(),
|
||||
"name": name.strip(),
|
||||
"cookie_id": cookie_id or str(uuid4()),
|
||||
}
|
||||
return serializer(settings).dumps(payload)
|
||||
|
||||
|
||||
def sign_admin(settings: Settings) -> str:
|
||||
return serializer(settings).dumps({"is_admin": True, "ts": int(time.time())})
|
||||
|
||||
|
||||
def loads_cookie(settings: Settings, value: str | None) -> dict[str, Any] | None:
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
payload = serializer(settings).loads(value)
|
||||
except BadSignature:
|
||||
return None
|
||||
return payload if isinstance(payload, dict) else None
|
||||
|
||||
|
||||
def get_student_identity(settings: Settings, request: Request, sid: str | None = None) -> dict[str, Any] | None:
|
||||
payload = loads_cookie(settings, request.cookies.get(STUDENT_COOKIE))
|
||||
if not payload:
|
||||
return None
|
||||
if sid is not None and payload.get("sid") != sid:
|
||||
return None
|
||||
required = {"sid", "student_id", "name", "cookie_id"}
|
||||
return payload if required.issubset(payload) else None
|
||||
|
||||
|
||||
def get_student_identity_ws(settings: Settings, websocket: WebSocket, sid: str) -> dict[str, Any] | None:
|
||||
payload = loads_cookie(settings, websocket.cookies.get(STUDENT_COOKIE))
|
||||
if not payload or payload.get("sid") != sid:
|
||||
return None
|
||||
required = {"sid", "student_id", "name", "cookie_id"}
|
||||
return payload if required.issubset(payload) else None
|
||||
|
||||
|
||||
def require_admin_request(settings: Settings, request: Request) -> None:
|
||||
payload = loads_cookie(settings, request.cookies.get(ADMIN_COOKIE))
|
||||
if not payload or payload.get("is_admin") is not True:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Admin login required")
|
||||
|
||||
|
||||
def is_admin_ws(settings: Settings, websocket: WebSocket) -> bool:
|
||||
payload = loads_cookie(settings, websocket.cookies.get(ADMIN_COOKIE))
|
||||
return bool(payload and payload.get("is_admin") is True)
|
||||
|
||||
|
||||
def verify_admin_password(settings: Settings, password: str) -> bool:
|
||||
if not settings.admin_password:
|
||||
return False
|
||||
return secrets.compare_digest(password, settings.admin_password)
|
||||
|
||||
|
||||
def set_student_cookie(settings: Settings, response: Response, value: str) -> None:
|
||||
response.set_cookie(
|
||||
STUDENT_COOKIE,
|
||||
value,
|
||||
max_age=STUDENT_MAX_AGE,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
secure=settings.secure_cookies,
|
||||
path="/",
|
||||
)
|
||||
|
||||
|
||||
def set_admin_cookie(settings: Settings, response: Response, value: str) -> None:
|
||||
response.set_cookie(
|
||||
ADMIN_COOKIE,
|
||||
value,
|
||||
max_age=ADMIN_MAX_AGE,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
secure=settings.secure_cookies,
|
||||
path="/",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user