diff --git a/modules/routes/routeSecurityLocal.py b/modules/routes/routeSecurityLocal.py index 7a7bc0f2..fcc59d1b 100644 --- a/modules/routes/routeSecurityLocal.py +++ b/modules/routes/routeSecurityLocal.py @@ -14,7 +14,7 @@ from pydantic import BaseModel # Import auth modules from modules.security.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM -from modules.security.jwtService import createAccessToken, createRefreshToken, setAccessTokenCookie, setRefreshTokenCookie +from modules.security.jwtService import createAccessToken, createRefreshToken, setAccessTokenCookie, setRefreshTokenCookie, clearAccessTokenCookie, clearRefreshTokenCookie from modules.interfaces.interfaceDbAppObjects import getInterface, getRootInterface from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority, UserPrivilege from modules.datamodels.datamodelSecurity import Token @@ -365,15 +365,18 @@ async def logout(request: Request, response: Response, currentUser: User = Depen # Don't fail if audit logging fails pass - # Clear httpOnly cookies - response.delete_cookie(key="auth_token", httponly=True, samesite="strict") - response.delete_cookie(key="refresh_token", httponly=True, samesite="strict") - - return JSONResponse({ + # Create the JSON response first + json_response = JSONResponse({ "message": "Successfully logged out - cookies cleared", "revokedTokens": revoked }) + # Clear httpOnly cookies on the response we're actually returning + clearAccessTokenCookie(json_response) + clearRefreshTokenCookie(json_response) + + return json_response + except Exception as e: logger.error(f"Error during logout: {str(e)}") raise HTTPException( diff --git a/modules/security/jwtService.py b/modules/security/jwtService.py index 5e09e63e..2726db35 100644 --- a/modules/security/jwtService.py +++ b/modules/security/jwtService.py @@ -17,6 +17,11 @@ ALGORITHM = APP_CONFIG.get("Auth_ALGORITHM") ACCESS_TOKEN_EXPIRE_MINUTES = int(APP_CONFIG.get("APP_TOKEN_EXPIRY")) REFRESH_TOKEN_EXPIRE_DAYS = int(APP_CONFIG.get("APP_REFRESH_TOKEN_EXPIRY", "7")) +# Cookie security settings - use secure cookies only in production (HTTPS) +# In development (HTTP), secure=True would prevent cookies from being set/cleared properly +ENV_TYPE = APP_CONFIG.get("APP_ENV_TYPE", "dev") +USE_SECURE_COOKIES = ENV_TYPE in ["prod", "production"] + def createAccessToken(data: dict, expiresDelta: Optional[timedelta] = None) -> Tuple[str, "datetime"]: """Create a JWT access token and return (token, expiresAt).""" @@ -52,8 +57,9 @@ def setAccessTokenCookie(response: Response, token: str, expiresDelta: Optional[ key="auth_token", value=token, httponly=True, - secure=True, + secure=USE_SECURE_COOKIES, # Only secure in production (HTTPS) samesite="strict", + path="/", max_age=maxAge ) @@ -64,9 +70,40 @@ def setRefreshTokenCookie(response: Response, token: str) -> None: key="refresh_token", value=token, httponly=True, - secure=True, + secure=USE_SECURE_COOKIES, # Only secure in production (HTTPS) samesite="strict", + path="/", max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60 ) +def clearAccessTokenCookie(response: Response) -> None: + """ + Clear access token cookie by setting it to expire immediately. + Uses both raw header manipulation and FastAPI's delete_cookie for maximum browser compatibility. + """ + # Primary method: Raw Set-Cookie header for guaranteed deletion + response.headers.append( + "Set-Cookie", + f"auth_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Strict" + ) + + # Fallback: Also use FastAPI's built-in method + response.delete_cookie(key="auth_token", path="/") + + +def clearRefreshTokenCookie(response: Response) -> None: + """ + Clear refresh token cookie by setting it to expire immediately. + Uses both raw header manipulation and FastAPI's delete_cookie for maximum browser compatibility. + """ + # Primary method: Raw Set-Cookie header for guaranteed deletion + response.headers.append( + "Set-Cookie", + f"refresh_token=deleted; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Strict" + ) + + # Fallback: Also use FastAPI's built-in method + response.delete_cookie(key="refresh_token", path="/") + +