Update cookie

This commit is contained in:
2026-03-02 18:23:48 -03:00
parent 1a04b23e89
commit 3f7cdee995
5 changed files with 55 additions and 6 deletions

View File

@@ -30,9 +30,14 @@ from app.services.auth_login_throttle import (
)
try:
from fastapi import Response
from fastapi import Cookie, Response
except (ImportError, AttributeError):
from fastapi.responses import Response
def Cookie(_default=None, **_kwargs): # type: ignore[no-untyped-def]
"""Compatibility fallback for environments that stub fastapi without request params."""
return None
from app.services.authentication import authenticate_user, issue_user_session, revoke_auth_session
router = APIRouter(prefix="/auth", tags=["auth"])
@@ -255,9 +260,13 @@ def login(
@router.get("/me", response_model=AuthSessionResponse)
def me(context: AuthContext = Depends(require_user_or_admin)) -> AuthSessionResponse:
def me(
context: AuthContext = Depends(require_user_or_admin),
csrf_cookie: str | None = Cookie(None, alias=CSRF_COOKIE_NAME),
) -> AuthSessionResponse:
"""Returns current authenticated session identity and expiration metadata."""
normalized_csrf_cookie = (csrf_cookie or "").strip() or None
return AuthSessionResponse(
expires_at=context.expires_at,
user=AuthUserResponse(
@@ -265,6 +274,7 @@ def me(context: AuthContext = Depends(require_user_or_admin)) -> AuthSessionResp
username=context.username,
role=context.role,
),
csrf_token=normalized_csrf_cookie,
)

View File

@@ -33,6 +33,7 @@ class AuthSessionResponse(BaseModel):
user: AuthUserResponse
expires_at: datetime
csrf_token: str | None = None
class AuthLoginResponse(AuthSessionResponse):

View File

@@ -19,7 +19,7 @@ Primary implementation modules:
- Login brute-force protection enforces Redis-backed throttle checks keyed by username and source IP.
- State-changing requests from browser clients must send `x-csrf-token: <dcm_csrf>` in request headers (double-submit pattern).
- For non-browser API clients, the optional `Authorization: Bearer <token>` path remains supported when the token is sent explicitly.
- `GET /auth/me` returns current identity and role.
- `GET /auth/me` returns current identity, role, and current CSRF token.
- `POST /auth/logout` revokes current session token.
Role matrix:

View File

@@ -41,6 +41,7 @@ const API_BASE = resolveApiBase();
const CSRF_COOKIE_NAME = "dcm_csrf";
const CSRF_HEADER_NAME = "x-csrf-token";
const CSRF_SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
const CSRF_SESSION_STORAGE_KEY = "dcm_csrf_token";
type ApiRequestInit = Omit<RequestInit, 'headers'> & { headers?: HeadersInit };
@@ -65,7 +66,38 @@ function getCookieValue(name: string): string | undefined {
* Resolves the runtime CSRF token from browser cookie storage for API requests.
*/
function resolveCsrfToken(): string | undefined {
return getCookieValue(CSRF_COOKIE_NAME);
const cookieToken = getCookieValue(CSRF_COOKIE_NAME);
if (cookieToken) {
return cookieToken;
}
return loadStoredCsrfToken();
}
/**
* Loads the runtime CSRF token from browser session storage.
*/
function loadStoredCsrfToken(): string | undefined {
if (typeof window === "undefined") {
return undefined;
}
const rawValue = window.sessionStorage.getItem(CSRF_SESSION_STORAGE_KEY);
const normalizedValue = rawValue?.trim();
return normalizedValue ? normalizedValue : undefined;
}
/**
* Persists or clears a runtime CSRF token in browser session storage.
*/
function persistCsrfToken(token: string | undefined | null): void {
if (typeof window === "undefined") {
return;
}
const normalizedValue = typeof token === "string" ? token.trim() : "";
if (!normalizedValue) {
window.sessionStorage.removeItem(CSRF_SESSION_STORAGE_KEY);
return;
}
window.sessionStorage.setItem(CSRF_SESSION_STORAGE_KEY, normalizedValue);
}
/**
@@ -181,7 +213,9 @@ export async function loginWithPassword(username: string, password: string): Pro
}
throw new Error('Login failed');
}
return response.json() as Promise<AuthLoginResponse>;
const payload = await (response.json() as Promise<AuthLoginResponse>);
persistCsrfToken(payload.csrf_token);
return payload;
}
/**
@@ -196,7 +230,9 @@ export async function getCurrentAuthSession(): Promise<AuthSessionInfo> {
}
throw new Error('Failed to load authentication session');
}
return response.json() as Promise<AuthSessionInfo>;
const payload = await (response.json() as Promise<AuthSessionInfo>);
persistCsrfToken(payload.csrf_token);
return payload;
}
/**
@@ -206,6 +242,7 @@ export async function logoutCurrentSession(): Promise<void> {
const response = await apiRequest(`${API_BASE}/auth/logout`, {
method: 'POST',
});
persistCsrfToken(undefined);
if (!response.ok && response.status !== 401) {
const detail = await responseErrorDetail(response);
if (detail) {

View File

@@ -73,6 +73,7 @@ export interface AuthUser {
export interface AuthSessionInfo {
user: AuthUser;
expires_at: string;
csrf_token?: string;
}
/**