Changed logic for auth token refresh

This commit is contained in:
gulimabr
2025-12-02 15:37:37 -03:00
parent a65719b631
commit 5b7c499212
8 changed files with 351 additions and 4 deletions

View File

@@ -18,7 +18,7 @@ class Settings(BaseSettings):
cookie_secure: bool = Field(default=False, env="COOKIE_SECURE")
cookie_samesite: str = Field(default="lax", env="COOKIE_SAMESITE")
cookie_domain: str | None = Field(default=None, env="COOKIE_DOMAIN")
cookie_max_age: int = Field(default=3600, env="COOKIE_MAX_AGE")
cookie_max_age: int = Field(default=28800, env="COOKIE_MAX_AGE") # 8 hours
cookie_name: str = Field(default="access_token", env="COOKIE_NAME")
# Database settings

View File

@@ -6,6 +6,9 @@ from src.models import TokenResponse, UserInfo
from src.service import AuthService, UserService
from src.config import get_settings
from src.database import get_db
import logging
logger = logging.getLogger(__name__)
# Initialize HTTPBearer security dependency
bearer_scheme = HTTPBearer()
@@ -13,6 +16,9 @@ bearer_scheme = HTTPBearer()
# Get settings
settings = get_settings()
# Cookie name for refresh token
REFRESH_TOKEN_COOKIE = "refresh_token"
class AuthController:
"""
@@ -85,6 +91,20 @@ class AuthController:
path="/",
)
# Also store the refresh token if available
refresh_token = token_response.get("refresh_token")
if refresh_token:
response.set_cookie(
key=REFRESH_TOKEN_COOKIE,
value=refresh_token,
httponly=True,
secure=settings.cookie_secure,
samesite=settings.cookie_samesite,
max_age=86400 * 7, # 7 days for refresh token
domain=settings.cookie_domain,
path="/",
)
return response
@staticmethod
@@ -148,15 +168,100 @@ class AuthController:
status_code=status.HTTP_200_OK
)
# Clear the authentication cookie
# Clear the authentication cookies
response.delete_cookie(
key=settings.cookie_name,
path="/",
domain=settings.cookie_domain,
)
response.delete_cookie(
key=REFRESH_TOKEN_COOKIE,
path="/",
domain=settings.cookie_domain,
)
return response
@staticmethod
def refresh_token(request: Request) -> JSONResponse:
"""
Silently refresh the access token using the refresh token.
This endpoint is called by the frontend before the access token expires.
Args:
request (Request): The FastAPI request object.
Returns:
JSONResponse: Contains success status and new expiration time.
"""
# Get the refresh token from cookies
refresh_token = request.cookies.get(REFRESH_TOKEN_COOKIE)
if not refresh_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="No refresh token available",
)
try:
# Get new tokens from Keycloak
token_response = AuthService.refresh_access_token(refresh_token)
new_access_token = token_response.get("access_token")
new_refresh_token = token_response.get("refresh_token")
expires_in = token_response.get("expires_in", settings.cookie_max_age)
if not new_access_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Failed to refresh token",
)
# Create response with new tokens
response = JSONResponse(
content={
"success": True,
"expires_in": expires_in,
},
status_code=status.HTTP_200_OK
)
# Set new access token cookie
response.set_cookie(
key=settings.cookie_name,
value=new_access_token,
httponly=True,
secure=settings.cookie_secure,
samesite=settings.cookie_samesite,
max_age=expires_in,
domain=settings.cookie_domain,
path="/",
)
# Set new refresh token cookie if provided
if new_refresh_token:
response.set_cookie(
key=REFRESH_TOKEN_COOKIE,
value=new_refresh_token,
httponly=True,
secure=settings.cookie_secure,
samesite=settings.cookie_samesite,
max_age=86400 * 7, # 7 days
domain=settings.cookie_domain,
path="/",
)
logger.info("Token refreshed successfully")
return response
except HTTPException:
raise
except Exception as exc:
logger.error(f"Token refresh failed: {exc}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token refresh failed",
) from exc
@staticmethod
def protected_endpoint(
credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),

View File

@@ -196,6 +196,19 @@ async def logout(request: Request):
return AuthController.logout()
# Define the token refresh endpoint
@app.post("/api/auth/refresh")
async def refresh_token(request: Request):
"""
Silently refresh the access token using the refresh token cookie.
This should be called by the frontend before the access token expires.
Returns:
dict: Success status and new expiration time.
"""
return AuthController.refresh_token(request)
# Define the protected endpoint (kept for API token-based access)
@app.get("/api/protected", response_model=UserInfo)
async def protected_endpoint(

View File

@@ -115,6 +115,35 @@ class AuthService:
detail="Could not decode token",
) from exc
@staticmethod
def refresh_access_token(refresh_token: str) -> dict:
"""
Use a refresh token to get a new access token.
Args:
refresh_token: The refresh token from Keycloak
Returns:
dict containing new access_token, refresh_token, and expires_in
"""
try:
keycloak_openid = get_keycloak_openid()
token = keycloak_openid.refresh_token(refresh_token)
logger.info("Token refresh successful")
return token
except KeycloakAuthenticationError as exc:
logger.error(f"Token refresh failed - KeycloakAuthenticationError: {exc}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Refresh token expired or invalid",
) from exc
except Exception as exc:
logger.error(f"Token refresh failed: {exc}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not refresh token",
) from exc
class UserService:
"""Service for user-related operations."""