feat(api): Implement authentication router and secure dependencies
This commit is contained in:
46
src/ea_chatbot/api/dependencies.py
Normal file
46
src/ea_chatbot/api/dependencies.py
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user