feat(auth): Implement /refresh endpoint and update login/register
This commit is contained in:
@@ -59,7 +59,8 @@ async def register(user_in: UserCreate, response: Response):
|
||||
)
|
||||
|
||||
access_token = create_access_token(data={"sub": str(user.id)})
|
||||
set_auth_cookie(response, access_token)
|
||||
refresh_token = create_refresh_token(data={"sub": str(user.id)})
|
||||
set_auth_cookie(response, access_token, refresh_token)
|
||||
|
||||
return {
|
||||
"id": str(user.id),
|
||||
@@ -80,15 +81,64 @@ async def login(response: Response, form_data: OAuth2PasswordRequestForm = Depen
|
||||
)
|
||||
|
||||
access_token = create_access_token(data={"sub": str(user.id)})
|
||||
set_auth_cookie(response, access_token)
|
||||
refresh_token = create_refresh_token(data={"sub": str(user.id)})
|
||||
set_auth_cookie(response, access_token, refresh_token)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(response: Response):
|
||||
"""Logout by clearing the auth cookie."""
|
||||
response.delete_cookie(key="access_token")
|
||||
"""Logout by clearing the auth cookies."""
|
||||
clear_auth_cookies(response)
|
||||
return {"detail": "Successfully logged out"}
|
||||
|
||||
@router.post("/refresh", response_model=Token)
|
||||
async def refresh(request: Request, response: Response):
|
||||
"""Refresh the access token using a valid refresh token."""
|
||||
refresh_token = request.cookies.get("refresh_token")
|
||||
if not refresh_token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Refresh token missing"
|
||||
)
|
||||
|
||||
from ea_chatbot.api.utils import decode_access_token # Using decode_access_token for both
|
||||
payload = decode_access_token(refresh_token)
|
||||
|
||||
if not payload:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token"
|
||||
)
|
||||
|
||||
if payload.get("type") != "refresh":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token type"
|
||||
)
|
||||
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token payload"
|
||||
)
|
||||
|
||||
# Check if user still exists
|
||||
user = history_manager.get_user_by_id(user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Issue new tokens (Rotation)
|
||||
new_access_token = create_access_token(data={"sub": str(user.id)})
|
||||
new_refresh_token = create_refresh_token(data={"sub": str(user.id)})
|
||||
|
||||
set_auth_cookie(response, new_access_token, new_refresh_token)
|
||||
|
||||
return {"access_token": new_access_token, "token_type": "bearer"}
|
||||
|
||||
@router.get("/oidc/login")
|
||||
async def oidc_login(response: Response):
|
||||
"""Get the OIDC authorization URL and set temporary session cookie."""
|
||||
@@ -158,9 +208,10 @@ async def oidc_callback(request: Request, response: Response, code: str, state:
|
||||
# 4. Sync user and establish session
|
||||
user = history_manager.sync_user_from_oidc(email=email, display_name=name)
|
||||
access_token = create_access_token(data={"sub": str(user.id)})
|
||||
refresh_token = create_refresh_token(data={"sub": str(user.id)})
|
||||
|
||||
redirect_response = RedirectResponse(url=settings.frontend_url, status_code=status.HTTP_302_FOUND)
|
||||
set_auth_cookie(redirect_response, access_token)
|
||||
set_auth_cookie(redirect_response, access_token, refresh_token)
|
||||
redirect_response.delete_cookie("oidc_session") # Cleanup
|
||||
|
||||
return redirect_response
|
||||
|
||||
56
backend/tests/api/test_auth_refresh.py
Normal file
56
backend/tests/api/test_auth_refresh.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from ea_chatbot.api.main import app
|
||||
from ea_chatbot.api.utils import create_refresh_token, create_access_token
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
from ea_chatbot.history.models import User
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
return TestClient(app)
|
||||
|
||||
def test_refresh_token_success(client):
|
||||
# 1. Setup: Create a valid refresh token for a user
|
||||
user_id = "test-user-id"
|
||||
refresh_token = create_refresh_token({"sub": user_id})
|
||||
|
||||
# 2. Set the cookie manually in the client
|
||||
client.cookies.set("refresh_token", refresh_token)
|
||||
|
||||
import time
|
||||
time.sleep(1.1) # Wait to ensure iat is different
|
||||
|
||||
# 3. Call the refresh endpoint with mock
|
||||
with patch("ea_chatbot.api.routers.auth.history_manager") as mock_hm:
|
||||
mock_hm.get_user_by_id.return_value = User(id=user_id, username="test@test.com")
|
||||
|
||||
response = client.post("/api/v1/auth/refresh")
|
||||
|
||||
# 4. Assert success
|
||||
assert response.status_code == 200
|
||||
assert "access_token" in response.cookies
|
||||
assert "refresh_token" in response.cookies
|
||||
|
||||
# Verify tokens are rotated (different from original)
|
||||
assert response.cookies["refresh_token"] != refresh_token
|
||||
|
||||
def test_refresh_token_missing_cookie(client):
|
||||
response = client.post("/api/v1/auth/refresh")
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "Refresh token missing"
|
||||
|
||||
def test_refresh_token_invalid(client):
|
||||
client.cookies.set("refresh_token", "invalid-token")
|
||||
response = client.post("/api/v1/auth/refresh")
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "Invalid refresh token"
|
||||
|
||||
def test_refresh_token_wrong_type(client):
|
||||
# Using an access token as a refresh token should fail
|
||||
access_token = create_access_token({"sub": "user123"})
|
||||
client.cookies.set("refresh_token", access_token)
|
||||
|
||||
response = client.post("/api/v1/auth/refresh")
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "Invalid token type"
|
||||
Reference in New Issue
Block a user