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)})
|
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 {
|
return {
|
||||||
"id": str(user.id),
|
"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)})
|
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"}
|
return {"access_token": access_token, "token_type": "bearer"}
|
||||||
|
|
||||||
@router.post("/logout")
|
@router.post("/logout")
|
||||||
async def logout(response: Response):
|
async def logout(response: Response):
|
||||||
"""Logout by clearing the auth cookie."""
|
"""Logout by clearing the auth cookies."""
|
||||||
response.delete_cookie(key="access_token")
|
clear_auth_cookies(response)
|
||||||
return {"detail": "Successfully logged out"}
|
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")
|
@router.get("/oidc/login")
|
||||||
async def oidc_login(response: Response):
|
async def oidc_login(response: Response):
|
||||||
"""Get the OIDC authorization URL and set temporary session cookie."""
|
"""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
|
# 4. Sync user and establish session
|
||||||
user = history_manager.sync_user_from_oidc(email=email, display_name=name)
|
user = history_manager.sync_user_from_oidc(email=email, display_name=name)
|
||||||
access_token = create_access_token(data={"sub": str(user.id)})
|
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)
|
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
|
redirect_response.delete_cookie("oidc_session") # Cleanup
|
||||||
|
|
||||||
return redirect_response
|
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