security: httpOnly cookies, rate limiting, nginx headers, SSE sanitization
Auth / token storage: - JWT is now set as an httpOnly Secure SameSite=Lax cookie on login - Add POST /auth/logout endpoint that clears the cookie - get_current_member falls back to rh_token cookie when no Authorization header - WebSocket auth now accepts cookie (rh_token) or optional ?token= query param - Frontend removes all localStorage JWT access; uses credentials:"include" on every fetch so the httpOnly cookie is sent automatically - Replace clearToken() with logout() that calls the server logout endpoint - Non-sensitive rh_session flag in localStorage used only for client-side routing Rate limiting: - Add slowapi>=0.1.9 dependency - /auth/login limited to 10 req/min per IP - /auth/register limited to 5 req/min per IP Nginx security headers: - Add X-Frame-Options, X-Content-Type-Options, Referrer-Policy, X-XSS-Protection, Permissions-Policy to all responses SSE error leakage: - songs.py nc-scan/stream no longer leaks str(exc) to clients Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -13,18 +13,25 @@ from rehearsalhub.db.models import Member
|
||||
from rehearsalhub.services.auth import decode_token
|
||||
from rehearsalhub.repositories.member import MemberRepository
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
||||
# auto_error=False so we can fall back to cookie auth without a 401 from the scheme itself
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", auto_error=False)
|
||||
|
||||
|
||||
async def get_current_member(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
request: Request,
|
||||
bearer_token: str | None = Depends(oauth2_scheme),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> Member:
|
||||
# Prefer Authorization: Bearer header; fall back to httpOnly cookie
|
||||
token = bearer_token or request.cookies.get("rh_token")
|
||||
|
||||
credentials_exc = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid or expired token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
if not token:
|
||||
raise credentials_exc
|
||||
try:
|
||||
payload = decode_token(token)
|
||||
member_id_str: str | None = payload.get("sub")
|
||||
|
||||
Reference in New Issue
Block a user