feat(backend): Implement /api/v1 prefix and HttpOnly cookie-based auth
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import os
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi import Depends, HTTPException, status, Request
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from ea_chatbot.config import Settings
|
||||
from ea_chatbot.history.manager import HistoryManager
|
||||
@@ -21,16 +21,23 @@ if settings.oidc_client_id and settings.oidc_client_secret and settings.oidc_ser
|
||||
redirect_uri=os.getenv("OIDC_REDIRECT_URI", "http://localhost:3000/auth/callback")
|
||||
)
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth/login", auto_error=False)
|
||||
|
||||
async def get_current_user(request: Request, token: str = Depends(oauth2_scheme)) -> User:
|
||||
"""Dependency to get the current authenticated user from the JWT token (cookie or header)."""
|
||||
# Try getting token from cookie first
|
||||
if not token:
|
||||
token = request.cookies.get("access_token")
|
||||
|
||||
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"},
|
||||
)
|
||||
|
||||
if not token:
|
||||
raise credentials_exception
|
||||
|
||||
payload = decode_access_token(token)
|
||||
if payload is None:
|
||||
raise credentials_exception
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, APIRouter
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from ea_chatbot.api.routers import auth, history, artifacts, agent
|
||||
from dotenv import load_dotenv
|
||||
@@ -20,10 +20,16 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(history.router)
|
||||
app.include_router(artifacts.router)
|
||||
app.include_router(agent.router)
|
||||
# API v1 Router
|
||||
api_v1_router = APIRouter(prefix="/api/v1")
|
||||
|
||||
api_v1_router.include_router(auth.router)
|
||||
api_v1_router.include_router(history.router)
|
||||
api_v1_router.include_router(artifacts.router)
|
||||
api_v1_router.include_router(agent.router)
|
||||
|
||||
# Include v1 router in app
|
||||
app.include_router(api_v1_router)
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Response
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from ea_chatbot.api.utils import create_access_token
|
||||
from ea_chatbot.api.dependencies import history_manager, oidc_client, get_current_user
|
||||
from ea_chatbot.history.models import User as UserDB
|
||||
from ea_chatbot.api.schemas import Token, UserCreate, UserResponse
|
||||
import os
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
||||
|
||||
def set_auth_cookie(response: Response, token: str):
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
max_age=1800,
|
||||
expires=1800,
|
||||
samesite="lax",
|
||||
secure=False, # Set to True in production with HTTPS
|
||||
)
|
||||
|
||||
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def register(user_in: UserCreate):
|
||||
async def register(user_in: UserCreate, response: Response):
|
||||
"""Register a new user."""
|
||||
user = history_manager.get_user(user_in.email)
|
||||
if user:
|
||||
@@ -22,6 +37,10 @@ async def register(user_in: UserCreate):
|
||||
password=user_in.password,
|
||||
display_name=user_in.display_name
|
||||
)
|
||||
|
||||
access_token = create_access_token(data={"sub": str(user.id)})
|
||||
set_auth_cookie(response, access_token)
|
||||
|
||||
return {
|
||||
"id": str(user.id),
|
||||
"email": user.username,
|
||||
@@ -29,7 +48,7 @@ async def register(user_in: UserCreate):
|
||||
}
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
async def login(response: Response, form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
"""Login with email and password to get a JWT."""
|
||||
user = history_manager.authenticate_user(form_data.username, form_data.password)
|
||||
if not user:
|
||||
@@ -40,8 +59,15 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
)
|
||||
|
||||
access_token = create_access_token(data={"sub": str(user.id)})
|
||||
set_auth_cookie(response, access_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")
|
||||
return {"detail": "Successfully logged out"}
|
||||
|
||||
@router.get("/oidc/login")
|
||||
async def oidc_login():
|
||||
"""Get the OIDC authorization URL."""
|
||||
@@ -54,9 +80,9 @@ async def oidc_login():
|
||||
url = oidc_client.get_login_url()
|
||||
return {"url": url}
|
||||
|
||||
@router.get("/oidc/callback", response_model=Token)
|
||||
@router.get("/oidc/callback")
|
||||
async def oidc_callback(code: str):
|
||||
"""Handle the OIDC callback and issue a JWT."""
|
||||
"""Handle the OIDC callback, issue a JWT, and redirect to frontend."""
|
||||
if not oidc_client:
|
||||
raise HTTPException(status_code=status.HTTP_510_NOT_EXTENDED, detail="OIDC not configured")
|
||||
|
||||
@@ -72,9 +98,13 @@ async def oidc_callback(code: str):
|
||||
user = history_manager.sync_user_from_oidc(email=email, display_name=name)
|
||||
|
||||
access_token = create_access_token(data={"sub": str(user.id)})
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
response = RedirectResponse(url=f"{FRONTEND_URL}/auth/callback")
|
||||
set_auth_cookie(response, access_token)
|
||||
return response
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"OIDC authentication failed: {str(e)}")
|
||||
# Redirect to frontend with error
|
||||
return RedirectResponse(url=f"{FRONTEND_URL}?error=oidc_failed")
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def get_me(current_user: UserDB = Depends(get_current_user)):
|
||||
|
||||
Reference in New Issue
Block a user