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 import APIRouter, Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from pydantic import BaseModel, EmailStr
|
from pydantic import BaseModel, EmailStr
|
||||||
from typing import Optional
|
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
|
from ea_chatbot.api.utils import create_access_token
|
||||||
|
from ea_chatbot.api.dependencies import history_manager, oidc_client, get_current_user
|
||||||
settings = Settings()
|
from ea_chatbot.history.models import User as UserDB
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
|
|
||||||
|
|
||||||
class Token(BaseModel):
|
class Token(BaseModel):
|
||||||
access_token: str
|
access_token: str
|
||||||
token_type: str
|
token_type: str
|
||||||
@@ -79,7 +62,7 @@ async def oidc_login():
|
|||||||
"""Get the OIDC authorization URL."""
|
"""Get the OIDC authorization URL."""
|
||||||
if not oidc_client:
|
if not oidc_client:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
status_code=status.HTTP_510_NOT_EXTENDED,
|
||||||
detail="OIDC is not configured"
|
detail="OIDC is not configured"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -87,12 +70,10 @@ async def oidc_login():
|
|||||||
return {"url": url}
|
return {"url": url}
|
||||||
|
|
||||||
@router.get("/me", response_model=UserResponse)
|
@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."""
|
"""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 {
|
return {
|
||||||
"id": "unknown",
|
"id": str(current_user.id),
|
||||||
"email": "unknown",
|
"email": current_user.username,
|
||||||
"display_name": "unknown"
|
"display_name": current_user.display_name
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ def mock_user():
|
|||||||
|
|
||||||
def test_register_user_success():
|
def test_register_user_success():
|
||||||
"""Test successful user registration."""
|
"""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:
|
with patch("ea_chatbot.api.routers.auth.history_manager") as mock_hm:
|
||||||
mock_hm.get_user.return_value = None
|
mock_hm.get_user.return_value = None
|
||||||
mock_hm.create_user.return_value = User(id="1", username="new@example.com", display_name="New")
|
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"}
|
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.status_code == 201
|
||||||
assert response.json()["email"] == "new@example.com"
|
assert response.json()["email"] == "new@example.com"
|
||||||
|
|
||||||
@@ -74,3 +73,20 @@ def test_oidc_login_redirect():
|
|||||||
response = client.get("/auth/oidc/login")
|
response = client.get("/auth/oidc/login")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json()["url"] == "https://oidc-provider.com/auth"
|
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