Skip to content

Commit c4cdd3c

Browse files
committed
fix tests 4 (tokens stuff)
1 parent 8f821d4 commit c4cdd3c

File tree

2 files changed

+43
-42
lines changed

2 files changed

+43
-42
lines changed

backend/app/api/routes/auth.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ async def login(
8181
data={"sub": user.username}, expires_delta=access_token_expires
8282
)
8383

84-
session_id = security_service.get_session_id_from_request(request)
85-
csrf_token = security_service.generate_csrf_token(session_id)
84+
csrf_token = security_service.generate_csrf_token()
8685

8786
# Set httpOnly cookie for secure token storage
8887
response.set_cookie(
@@ -95,6 +94,17 @@ async def login(
9594
path="/",
9695
)
9796

97+
# Set CSRF token cookie (readable by JavaScript for header inclusion)
98+
response.set_cookie(
99+
key="csrf_token",
100+
value=csrf_token,
101+
max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
102+
httponly=False, # JavaScript needs to read this
103+
secure=True,
104+
samesite="strict",
105+
path="/",
106+
)
107+
98108
return {"message": "Login successful", "username": user.username, "csrf_token": csrf_token}
99109

100110

@@ -180,9 +190,8 @@ async def verify_token(
180190
"user_agent": request.headers.get("user-agent"),
181191
},
182192
)
183-
# Generate fresh CSRF token for authenticated session
184-
session_id = security_service.get_session_id_from_request(request)
185-
csrf_token = security_service.generate_csrf_token(session_id)
193+
# Return existing CSRF token from cookie
194+
csrf_token = request.cookies.get("csrf_token", "")
186195

187196
return {"valid": True, "username": current_user.username, "csrf_token": csrf_token}
188197

@@ -226,6 +235,15 @@ async def logout(
226235
samesite="strict",
227236
)
228237

238+
# Clear the CSRF cookie
239+
response.delete_cookie(
240+
key="csrf_token",
241+
path="/",
242+
secure=True,
243+
httponly=False,
244+
samesite="strict",
245+
)
246+
229247
logger.info(
230248
"Logout successful",
231249
extra={

backend/app/core/security.py

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from app.schemas.user import UserInDB
88
from fastapi import Depends, HTTPException, Request, status
99
from fastapi.security import OAuth2PasswordBearer
10-
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
10+
from itsdangerous import URLSafeTimedSerializer
1111
from passlib.context import CryptContext
1212

1313
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/login")
@@ -28,10 +28,6 @@ class SecurityService:
2828
def __init__(self) -> None:
2929
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
3030
self.settings = get_settings()
31-
self.csrf_serializer = URLSafeTimedSerializer(
32-
secret_key=self.settings.SECRET_KEY,
33-
salt="csrf-token"
34-
)
3531

3632
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
3733
return self.pwd_context.verify(plain_password, hashed_password) # type: ignore
@@ -77,39 +73,25 @@ async def get_current_user(
7773
raise credentials_exception
7874
return user
7975

80-
def generate_csrf_token(self, session_id: str) -> str:
81-
"""Generate a CSRF token for the given session"""
82-
data = {
83-
"session_id": session_id,
84-
"timestamp": datetime.utcnow().isoformat()
85-
}
86-
return self.csrf_serializer.dumps(data)
76+
def generate_csrf_token(self) -> str:
77+
"""Generate a CSRF token using secure random"""
78+
import secrets
79+
return secrets.token_urlsafe(32)
8780

88-
def validate_csrf_token(self, token: str, session_id: str) -> bool:
89-
"""Validate a CSRF token"""
90-
try:
91-
data = self.csrf_serializer.loads(token, max_age=3600) # 1 hour
92-
return bool(data.get("session_id") == session_id)
93-
except (BadSignature, SignatureExpired):
81+
def validate_csrf_token(self, header_token: str, cookie_token: str) -> bool:
82+
"""Validate CSRF token using double-submit cookie pattern"""
83+
if not header_token or not cookie_token:
9484
return False
95-
96-
def get_session_id_from_request(self, request: Request) -> str:
97-
"""Get session ID from request (using access token as session identifier)"""
98-
token = request.cookies.get("access_token")
99-
if token:
100-
return token[:32] # Use first 32 chars as session ID
101-
102-
# Fallback to client fingerprint
103-
client_ip = request.client.host if request.client else "unknown"
104-
user_agent = request.headers.get("user-agent", "unknown")
105-
return f"{client_ip}:{user_agent}"[:32]
85+
# Constant-time comparison to prevent timing attacks
86+
import hmac
87+
return hmac.compare_digest(header_token, cookie_token)
10688

10789

10890
security_service = SecurityService()
10991

11092

11193
def validate_csrf_token(request: Request) -> str:
112-
"""FastAPI dependency to validate CSRF token"""
94+
"""FastAPI dependency to validate CSRF token using double-submit cookie pattern"""
11395
# Skip CSRF validation for safe methods
11496
if request.method in ["GET", "HEAD", "OPTIONS"]:
11597
return "skip"
@@ -128,20 +110,21 @@ def validate_csrf_token(request: Request) -> str:
128110
# If not authenticated, skip CSRF validation (auth will be handled by other dependencies)
129111
return "skip"
130112

131-
# Get CSRF token from request
132-
csrf_token = request.headers.get("X-CSRF-Token")
133-
if not csrf_token:
113+
# Get CSRF token from header and cookie
114+
header_token = request.headers.get("X-CSRF-Token")
115+
cookie_token = request.cookies.get("csrf_token")
116+
117+
if not header_token:
134118
raise HTTPException(
135119
status_code=status.HTTP_403_FORBIDDEN,
136120
detail="CSRF token missing"
137121
)
138122

139-
# Validate CSRF token
140-
session_id = security_service.get_session_id_from_request(request)
141-
if not security_service.validate_csrf_token(csrf_token, session_id):
123+
# Validate using double-submit cookie pattern
124+
if not security_service.validate_csrf_token(header_token, cookie_token):
142125
raise HTTPException(
143126
status_code=status.HTTP_403_FORBIDDEN,
144127
detail="CSRF token invalid"
145128
)
146129

147-
return csrf_token
130+
return header_token

0 commit comments

Comments
 (0)