Update cookie
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class AuthSessionResponse(BaseModel):
|
||||
|
||||
user: AuthUserResponse
|
||||
expires_at: datetime
|
||||
csrf_token: str | None = None
|
||||
|
||||
|
||||
class AuthLoginResponse(AuthSessionResponse):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface AuthUser {
|
||||
export interface AuthSessionInfo {
|
||||
user: AuthUser;
|
||||
expires_at: string;
|
||||
csrf_token?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user