Switch refresh token storage from cookies to localStorage

This commit is contained in:
CREDO23 2026-02-05 17:55:21 +02:00
parent f3a9922eb9
commit 233852b681
7 changed files with 160 additions and 88 deletions

View file

@ -2,17 +2,18 @@
import logging
from fastapi import APIRouter, Cookie, Depends, HTTPException, Response, status
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from app.db import User, async_session_maker
from app.schemas.auth import LogoutAllResponse, LogoutResponse, RefreshTokenResponse
from app.users import current_active_user, get_jwt_strategy
from app.utils.auth_cookies import (
REFRESH_TOKEN_COOKIE_NAME,
delete_refresh_token_cookie,
set_refresh_token_cookie,
from app.schemas.auth import (
LogoutAllResponse,
LogoutRequest,
LogoutResponse,
RefreshTokenRequest,
RefreshTokenResponse,
)
from app.users import current_active_user, get_jwt_strategy
from app.utils.refresh_tokens import (
revoke_all_user_tokens,
revoke_refresh_token,
@ -26,21 +27,12 @@ router = APIRouter(prefix="/auth/jwt", tags=["auth"])
@router.post("/refresh", response_model=RefreshTokenResponse)
async def refresh_access_token(
response: Response,
refresh_token: str | None = Cookie(default=None, alias=REFRESH_TOKEN_COOKIE_NAME),
):
async def refresh_access_token(request: RefreshTokenRequest):
"""
Exchange a valid refresh token for a new access token and refresh token.
Reads refresh token from HTTP-only cookie. Implements token rotation for security.
Implements token rotation for security.
"""
if not refresh_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Refresh token not found",
)
token_record = await validate_refresh_token(refresh_token)
token_record = await validate_refresh_token(request.refresh_token)
if not token_record:
raise HTTPException(
@ -68,9 +60,6 @@ async def refresh_access_token(
# Rotate refresh token
new_refresh_token = await rotate_refresh_token(token_record)
# Set the new refresh token in cookie
set_refresh_token_cookie(response, new_refresh_token)
logger.info(f"Refreshed token for user {user.id}")
return RefreshTokenResponse(
@ -80,36 +69,21 @@ async def refresh_access_token(
@router.post("/logout", response_model=LogoutResponse)
async def logout(
response: Response,
refresh_token: str | None = Cookie(default=None, alias=REFRESH_TOKEN_COOKIE_NAME),
):
async def logout(request: LogoutRequest):
"""
Logout current device by revoking the refresh token from cookie.
Logout current device by revoking the provided refresh token.
"""
if refresh_token:
await revoke_refresh_token(refresh_token)
# Always delete the cookie
delete_refresh_token_cookie(response)
await revoke_refresh_token(request.refresh_token)
logger.info("User logged out from current device")
return LogoutResponse()
@router.post("/logout-all", response_model=LogoutAllResponse)
async def logout_all_devices(
response: Response,
user: User = Depends(current_active_user),
):
async def logout_all_devices(user: User = Depends(current_active_user)):
"""
Logout from all devices by revoking all refresh tokens for the user.
Requires valid access token.
"""
await revoke_all_user_tokens(user.id)
# Delete the cookie on current device
delete_refresh_token_cookie(response)
logger.info(f"User {user.id} logged out from all devices")
return LogoutAllResponse()