feat(api): Implement authentication router and secure dependencies

This commit is contained in:
Yunxiao Xu
2026-02-10 12:37:35 -08:00
parent 979e1ad2d6
commit ff27dee366
3 changed files with 73 additions and 30 deletions

View File

@@ -0,0 +1,46 @@
import os
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from ea_chatbot.config import Settings
from ea_chatbot.history.manager import HistoryManager
from ea_chatbot.auth import OIDCClient
from ea_chatbot.api.utils import decode_access_token
from ea_chatbot.history.models import User
settings = Settings()
# Shared instances
history_manager = HistoryManager(settings.history_db_url)
oidc_client = None
if settings.oidc_client_id and settings.oidc_client_secret and settings.oidc_server_metadata_url:
oidc_client = OIDCClient(
client_id=settings.oidc_client_id,
client_secret=settings.oidc_client_secret,
server_metadata_url=settings.oidc_server_metadata_url,
redirect_uri=os.getenv("OIDC_REDIRECT_URI", "http://localhost:3000/auth/callback")
)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
"""Dependency to get the current authenticated user from the JWT token."""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
payload = decode_access_token(token)
if payload is None:
raise credentials_exception
username: str | None = payload.get("sub")
if username is None:
raise credentials_exception
user = history_manager.get_user(username)
if user is None:
raise credentials_exception
return user

View File

@@ -1,30 +1,13 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel, EmailStr
from typing import Optional
from ea_chatbot.config import Settings
from ea_chatbot.history.manager import HistoryManager
from ea_chatbot.auth import OIDCClient
from ea_chatbot.api.utils import create_access_token
settings = Settings()
history_manager = HistoryManager(settings.history_db_url)
# Initialize OIDC Client if configured
oidc_client = None
if settings.oidc_client_id and settings.oidc_client_secret and settings.oidc_server_metadata_url:
oidc_client = OIDCClient(
client_id=settings.oidc_client_id,
client_secret=settings.oidc_client_secret,
server_metadata_url=settings.oidc_server_metadata_url,
# This will be updated to the frontend URL later
redirect_uri="http://localhost:3000/auth/callback"
)
from ea_chatbot.api.dependencies import history_manager, oidc_client, get_current_user
from ea_chatbot.history.models import User as UserDB
router = APIRouter(prefix="/auth", tags=["auth"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
class Token(BaseModel):
access_token: str
token_type: str
@@ -79,7 +62,7 @@ async def oidc_login():
"""Get the OIDC authorization URL."""
if not oidc_client:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
status_code=status.HTTP_510_NOT_EXTENDED,
detail="OIDC is not configured"
)
@@ -87,12 +70,10 @@ async def oidc_login():
return {"url": url}
@router.get("/me", response_model=UserResponse)
async def get_me(token: str = Depends(oauth2_scheme)):
async def get_me(current_user: UserDB = Depends(get_current_user)):
"""Get the current authenticated user's profile."""
# This currently only validates the token exists via oauth2_scheme
# The next task will implement the full dependency to decode JWT and fetch user
return {
"id": "unknown",
"email": "unknown",
"display_name": "unknown"
"id": str(current_user.id),
"email": current_user.username,
"display_name": current_user.display_name
}

View File

@@ -20,7 +20,7 @@ def mock_user():
def test_register_user_success():
"""Test successful user registration."""
# We'll need to mock history_manager.get_user and create_user
# We mock it where it is used in the router
with patch("ea_chatbot.api.routers.auth.history_manager") as mock_hm:
mock_hm.get_user.return_value = None
mock_hm.create_user.return_value = User(id="1", username="new@example.com", display_name="New")
@@ -30,7 +30,6 @@ def test_register_user_success():
json={"email": "new@example.com", "password": "password123", "display_name": "New"}
)
# This will fail now because the router doesn't exist
assert response.status_code == 201
assert response.json()["email"] == "new@example.com"
@@ -74,3 +73,20 @@ def test_oidc_login_redirect():
response = client.get("/auth/oidc/login")
assert response.status_code == 200
assert response.json()["url"] == "https://oidc-provider.com/auth"
def test_get_me_success():
"""Test getting current user with a valid token."""
from ea_chatbot.api.utils import create_access_token
token = create_access_token(data={"sub": "test@example.com", "user_id": "123"})
with patch("ea_chatbot.api.dependencies.history_manager") as mock_hm:
mock_hm.get_user.return_value = User(id="123", username="test@example.com", display_name="Test")
response = client.get(
"/auth/me",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
assert response.json()["email"] == "test@example.com"
assert response.json()["id"] == "123"