Refactor auth service to improve refresh token revocation handling and logging
This commit is contained in:
@@ -8,12 +8,9 @@ from app.core.config import settings
|
|||||||
from app.core.dependencies import get_auth_service, get_current_active_user
|
from app.core.dependencies import get_auth_service, get_current_active_user
|
||||||
from app.core.logging import get_logger
|
from app.core.logging import get_logger
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.auth import (
|
from app.schemas.auth import UserLoginRequest, UserRegisterRequest, UserResponse
|
||||||
UserLoginRequest,
|
|
||||||
UserRegisterRequest,
|
|
||||||
UserResponse,
|
|
||||||
)
|
|
||||||
from app.services.auth import AuthService
|
from app.services.auth import AuthService
|
||||||
|
from app.utils.auth import JWTUtils
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -31,11 +28,11 @@ async def register(
|
|||||||
"""Register a new user account."""
|
"""Register a new user account."""
|
||||||
try:
|
try:
|
||||||
auth_response = await auth_service.register(request)
|
auth_response = await auth_service.register(request)
|
||||||
|
|
||||||
# Create and store refresh token - need to get User object from service
|
# Create and store refresh token - need to get User object from service
|
||||||
user = await auth_service.get_current_user(auth_response.user.id)
|
user = await auth_service.get_current_user(auth_response.user.id)
|
||||||
refresh_token = await auth_service.create_and_store_refresh_token(user)
|
refresh_token = await auth_service.create_and_store_refresh_token(user)
|
||||||
|
|
||||||
# Set HTTP-only cookies for both tokens
|
# Set HTTP-only cookies for both tokens
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="access_token",
|
key="access_token",
|
||||||
@@ -48,12 +45,15 @@ async def register(
|
|||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="refresh_token",
|
key="refresh_token",
|
||||||
value=refresh_token,
|
value=refresh_token,
|
||||||
max_age=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60, # Convert days to seconds
|
max_age=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS
|
||||||
|
* 24
|
||||||
|
* 60
|
||||||
|
* 60, # Convert days to seconds
|
||||||
httponly=True,
|
httponly=True,
|
||||||
secure=settings.COOKIE_SECURE,
|
secure=settings.COOKIE_SECURE,
|
||||||
samesite=settings.COOKIE_SAMESITE,
|
samesite=settings.COOKIE_SAMESITE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return only user data, tokens are now in cookies
|
# Return only user data, tokens are now in cookies
|
||||||
return auth_response.user
|
return auth_response.user
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
@@ -75,11 +75,11 @@ async def login(
|
|||||||
"""Authenticate a user and return access token."""
|
"""Authenticate a user and return access token."""
|
||||||
try:
|
try:
|
||||||
auth_response = await auth_service.login(request)
|
auth_response = await auth_service.login(request)
|
||||||
|
|
||||||
# Create and store refresh token - need to get User object from service
|
# Create and store refresh token - need to get User object from service
|
||||||
user = await auth_service.get_current_user(auth_response.user.id)
|
user = await auth_service.get_current_user(auth_response.user.id)
|
||||||
refresh_token = await auth_service.create_and_store_refresh_token(user)
|
refresh_token = await auth_service.create_and_store_refresh_token(user)
|
||||||
|
|
||||||
# Set HTTP-only cookies for both tokens
|
# Set HTTP-only cookies for both tokens
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="access_token",
|
key="access_token",
|
||||||
@@ -92,12 +92,15 @@ async def login(
|
|||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="refresh_token",
|
key="refresh_token",
|
||||||
value=refresh_token,
|
value=refresh_token,
|
||||||
max_age=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60, # Convert days to seconds
|
max_age=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS
|
||||||
|
* 24
|
||||||
|
* 60
|
||||||
|
* 60, # Convert days to seconds
|
||||||
httponly=True,
|
httponly=True,
|
||||||
secure=settings.COOKIE_SECURE,
|
secure=settings.COOKIE_SECURE,
|
||||||
samesite=settings.COOKIE_SAMESITE,
|
samesite=settings.COOKIE_SAMESITE,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return only user data, tokens are now in cookies
|
# Return only user data, tokens are now in cookies
|
||||||
return auth_response.user
|
return auth_response.user
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
@@ -139,10 +142,10 @@ async def refresh_token(
|
|||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="No refresh token provided",
|
detail="No refresh token provided",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get new access token
|
# Get new access token
|
||||||
token_response = await auth_service.refresh_access_token(refresh_token)
|
token_response = await auth_service.refresh_access_token(refresh_token)
|
||||||
|
|
||||||
# Set new access token cookie
|
# Set new access token cookie
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="access_token",
|
key="access_token",
|
||||||
@@ -152,7 +155,7 @@ async def refresh_token(
|
|||||||
secure=settings.COOKIE_SECURE,
|
secure=settings.COOKIE_SECURE,
|
||||||
samesite=settings.COOKIE_SAMESITE,
|
samesite=settings.COOKIE_SAMESITE,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"message": "Token refreshed successfully"}
|
return {"message": "Token refreshed successfully"}
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
@@ -167,32 +170,56 @@ async def refresh_token(
|
|||||||
@router.post("/logout")
|
@router.post("/logout")
|
||||||
async def logout(
|
async def logout(
|
||||||
response: Response,
|
response: Response,
|
||||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
access_token: Annotated[str | None, Cookie()],
|
||||||
|
refresh_token: Annotated[str | None, Cookie()],
|
||||||
auth_service: Annotated[AuthService, Depends(get_auth_service)],
|
auth_service: Annotated[AuthService, Depends(get_auth_service)],
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""Logout endpoint - clears cookies and revokes refresh token."""
|
"""Logout endpoint - clears cookies and revokes refresh token."""
|
||||||
try:
|
user = None
|
||||||
# Revoke refresh token from database
|
|
||||||
await auth_service.revoke_refresh_token(current_user)
|
# Try to get user from access token first
|
||||||
|
if access_token:
|
||||||
# Clear both cookies
|
try:
|
||||||
response.delete_cookie(
|
payload = JWTUtils.decode_access_token(access_token)
|
||||||
key="access_token",
|
user_id_str = payload.get("sub")
|
||||||
httponly=True,
|
if user_id_str:
|
||||||
secure=settings.COOKIE_SECURE,
|
user_id = int(user_id_str)
|
||||||
samesite=settings.COOKIE_SAMESITE,
|
user = await auth_service.get_current_user(user_id)
|
||||||
)
|
logger.info("Found user from access token: %s", user.email)
|
||||||
response.delete_cookie(
|
except (HTTPException, Exception) as e:
|
||||||
key="refresh_token",
|
logger.info("Access token validation failed: %s", str(e))
|
||||||
httponly=True,
|
|
||||||
secure=settings.COOKIE_SECURE,
|
# If no user found, try refresh token
|
||||||
samesite=settings.COOKIE_SAMESITE,
|
if not user and refresh_token:
|
||||||
)
|
try:
|
||||||
|
payload = JWTUtils.decode_refresh_token(refresh_token)
|
||||||
return {"message": "Successfully logged out"}
|
user_id_str = payload.get("sub")
|
||||||
except Exception as e:
|
if user_id_str:
|
||||||
logger.exception("Logout failed")
|
user_id = int(user_id_str)
|
||||||
raise HTTPException(
|
user = await auth_service.get_current_user(user_id)
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
logger.info("Found user from refresh token: %s", user.email)
|
||||||
detail="Logout failed",
|
except (HTTPException, Exception) as e:
|
||||||
) from e
|
logger.info("Refresh token validation failed: %s", str(e))
|
||||||
|
|
||||||
|
# If we found a user, revoke their refresh token
|
||||||
|
if user:
|
||||||
|
await auth_service.revoke_refresh_token(user)
|
||||||
|
logger.info("Successfully revoked refresh token for user: %s", user.email)
|
||||||
|
else:
|
||||||
|
logger.info("No user found, skipping token revocation")
|
||||||
|
|
||||||
|
# Always clear both cookies regardless of token validity
|
||||||
|
response.delete_cookie(
|
||||||
|
key="access_token",
|
||||||
|
httponly=True,
|
||||||
|
secure=settings.COOKIE_SECURE,
|
||||||
|
samesite=settings.COOKIE_SAMESITE,
|
||||||
|
)
|
||||||
|
response.delete_cookie(
|
||||||
|
key="refresh_token",
|
||||||
|
httponly=True,
|
||||||
|
secure=settings.COOKIE_SECURE,
|
||||||
|
samesite=settings.COOKIE_SAMESITE,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"message": "Successfully logged out"}
|
||||||
|
|||||||
@@ -231,11 +231,17 @@ class AuthService:
|
|||||||
|
|
||||||
async def revoke_refresh_token(self, user: User) -> None:
|
async def revoke_refresh_token(self, user: User) -> None:
|
||||||
"""Revoke a user's refresh token."""
|
"""Revoke a user's refresh token."""
|
||||||
user.refresh_token_hash = None
|
try:
|
||||||
user.refresh_token_expires_at = None
|
# Use the repository to update the user to ensure proper session handling
|
||||||
self.session.add(user)
|
update_data = {
|
||||||
await self.session.commit()
|
"refresh_token_hash": None,
|
||||||
logger.info("Refresh token revoked for user: %s", user.email)
|
"refresh_token_expires_at": None,
|
||||||
|
}
|
||||||
|
await self.user_repo.update(user, update_data)
|
||||||
|
logger.info("Refresh token revoked for user: %s", user.email)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to revoke refresh token for user: %s", user.email)
|
||||||
|
raise
|
||||||
|
|
||||||
async def create_user_response(self, user: User) -> UserResponse:
|
async def create_user_response(self, user: User) -> UserResponse:
|
||||||
"""Create a user response from a user model."""
|
"""Create a user response from a user model."""
|
||||||
|
|||||||
Reference in New Issue
Block a user