| | """ |
| | FastAPI Dependencies Module |
| | |
| | This module provides reusable dependency functions for FastAPI routes. |
| | The primary dependency is `get_current_user()` which extracts and validates |
| | JWT tokens from the Authorization header. |
| | |
| | Security Rules: |
| | - All protected routes must use get_current_user() dependency |
| | - JWT must be in Authorization: Bearer <token> format |
| | - Invalid/missing tokens result in 401 Unauthorized |
| | - User context is injected into route handlers |
| | """ |
| |
|
| | from typing import Optional |
| | from fastapi import Depends, HTTPException, status |
| | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials |
| | from sqlalchemy.ext.asyncio import AsyncSession |
| |
|
| | from src.models.database import get_db |
| | from src.services.auth_service import get_current_user as get_current_user_service |
| | from src.models.models import User |
| |
|
| |
|
| | |
| | security = HTTPBearer(auto_error=False) |
| |
|
| |
|
| | async def get_current_user( |
| | authorization: Optional[HTTPAuthorizationCredentials] = Depends(security), |
| | db: AsyncSession = Depends(get_db) |
| | ) -> User: |
| | """ |
| | FastAPI dependency to extract and validate JWT from Authorization header. |
| | |
| | This dependency extracts the JWT token from the Authorization header, |
| | verifies it, and returns the user object. It must be used in all |
| | protected routes. |
| | |
| | Args: |
| | authorization: HTTPAuthorizationCredentials object containing the bearer token |
| | (automatically extracted from Authorization header by FastAPI) |
| | db: Database session (automatically injected by FastAPI) |
| | |
| | Returns: |
| | User: Authenticated user object |
| | |
| | Raises: |
| | HTTPException: 401 if token is missing, invalid, or expired |
| | |
| | Example: |
| | >>> from fastapi import APIRouter, Depends |
| | >>> from src.api.deps import get_current_user |
| | >>> |
| | >>> router = APIRouter() |
| | >>> |
| | >>> @router.get("/api/todos") |
| | >>> async def get_todos(user: User = Depends(get_current_user)): |
| | >>> user_id = user.id |
| | >>> # Use user_id to filter todos |
| | """ |
| | |
| | if authorization is None or not authorization.credentials: |
| | raise HTTPException( |
| | status_code=status.HTTP_401_UNAUTHORIZED, |
| | detail={ |
| | "code": "MISSING_AUTH_HEADER", |
| | "message": "Missing authorization header. Format: Authorization: Bearer <token>", |
| | "details": [] |
| | }, |
| | headers={"WWW-Authenticate": "Bearer"}, |
| | ) |
| |
|
| | |
| | token = authorization.credentials |
| |
|
| | try: |
| | |
| | user = await get_current_user_service(db, token) |
| | return user |
| |
|
| | except HTTPException: |
| | |
| | raise |
| |
|
| | except Exception as e: |
| | |
| | raise HTTPException( |
| | status_code=status.HTTP_401_UNAUTHORIZED, |
| | detail={ |
| | "code": "AUTH_FAILED", |
| | "message": f"Authentication failed: {str(e)}", |
| | "details": [] |
| | }, |
| | headers={"WWW-Authenticate": "Bearer"}, |
| | ) |
| |
|
| |
|
| | async def get_current_user_optional( |
| | authorization: Optional[HTTPAuthorizationCredentials] = Depends(security), |
| | db: AsyncSession = Depends(get_db) |
| | ) -> Optional[User]: |
| | """ |
| | Optional version of get_current_user that returns None if no token provided. |
| | |
| | This dependency allows routes to work with or without authentication. |
| | Use this for endpoints that have different behavior for authenticated |
| | vs anonymous users. |
| | |
| | Args: |
| | authorization: HTTPAuthorizationCredentials object containing the bearer token |
| | (automatically extracted from Authorization header by FastAPI) |
| | db: Database session (automatically injected by FastAPI) |
| | |
| | Returns: |
| | Optional[User]: User object if token is valid, None if missing/invalid |
| | |
| | Example: |
| | >>> from fastapi import APIRouter, Depends |
| | >>> from src.api.deps import get_current_user_optional |
| | >>> |
| | >>> router = APIRouter() |
| | >>> |
| | >>> @router.get("/api/public-data") |
| | >>> async def get_public_data(user: Optional[User] = Depends(get_current_user_optional)): |
| | >>> if user: |
| | >>> # Authenticated user - return personalized data |
| | >>> user_id = user.id |
| | >>> else: |
| | >>> # Anonymous user - return generic data |
| | """ |
| | |
| | if authorization is None or not authorization.credentials: |
| | return None |
| |
|
| | |
| | token = authorization.credentials |
| |
|
| | try: |
| | |
| | user = await get_current_user_service(db, token) |
| | return user |
| |
|
| | except HTTPException: |
| | |
| | return None |
| |
|
| | except Exception: |
| | |
| | return None |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | """ |
| | HOW TO USE THESE DEPENDENCIES |
| | ============================== |
| | |
| | 1. PROTECTED ROUTES (Require Authentication) |
| | Use get_current_user() to enforce authentication: |
| | |
| | from fastapi import APIRouter, Depends |
| | from src.api.deps import get_current_user |
| | from models.models import User |
| | |
| | router = APIRouter() |
| | |
| | @router.get("/api/todos") |
| | async def get_todos(user: User = Depends(get_current_user)): |
| | user_id = user.id |
| | email = user.email |
| | # Use user_id to filter data |
| | return {"user_id": str(user_id), "email": email} |
| | |
| | Result: Returns 401 if no token or invalid token provided |
| | |
| | 2. OPTIONAL AUTHENTICATION (Allow Anonymous Access) |
| | Use get_current_user_optional() for routes that work with or without auth: |
| | |
| | from fastapi import APIRouter, Depends |
| | from src.api.deps import get_current_user_optional |
| | from models.models import User |
| | |
| | router = APIRouter() |
| | |
| | @router.get("/api/public-data") |
| | async def get_public_data(user: Optional[User] = Depends(get_current_user_optional)): |
| | if user: |
| | user_id = user.id |
| | # Return personalized data |
| | else: |
| | # Return generic data |
| | |
| | Result: Returns user data if authenticated, None if anonymous |
| | |
| | 3. MULTIPLE DEPENDENCIES |
| | You can combine with other dependencies: |
| | |
| | from fastapi import APIRouter, Depends, Query |
| | from src.api.deps import get_current_user |
| | from models.models import User |
| | |
| | router = APIRouter() |
| | |
| | @router.get("/api/todos/{todo_id}") |
| | async def get_todo( |
| | todo_id: str, |
| | user: User = Depends(get_current_user), |
| | include_deleted: bool = Query(False) |
| | ): |
| | user_id = user.id |
| | # Get specific todo for user |
| | |
| | 4. OPENAPI DOCUMENTATION |
| | These dependencies automatically add security schemes to your API docs: |
| | - /docs endpoint will show "Authorize" button |
| | - Request format is documented as "Bearer <token>" |
| | - 401 responses are documented in schema |
| | |
| | AUTHENTICATION FLOW |
| | =================== |
| | |
| | 1. User logs in via /api/auth/login |
| | 2. Frontend receives JWT access token and refresh token |
| | 3. Frontend stores tokens in localStorage |
| | 4. Frontend makes API request with header: |
| | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... |
| | 5. FastAPI extracts token via get_current_user dependency |
| | 6. Token is verified with JWT (auth_service.py) |
| | 7. User object is injected into route handler |
| | 8. Route uses user.id to filter data |
| | |
| | ERROR RESPONSES |
| | =============== |
| | |
| | All authentication errors return 401 Unauthorized: |
| | |
| | { |
| | "code": "MISSING_AUTH_HEADER", |
| | "message": "Missing authorization header. Format: Authorization: Bearer <token>", |
| | "details": [] |
| | } |
| | |
| | { |
| | "code": "INVALID_TOKEN", |
| | "message": "Invalid token: signature verification failed", |
| | "details": [] |
| | } |
| | |
| | { |
| | "code": "TOKEN_EXPIRED", |
| | "message": "Invalid token: token expired", |
| | "details": [] |
| | } |
| | |
| | SECURITY BEST PRACTICES |
| | ======================== |
| | |
| | ✅ DO: |
| | - Use get_current_user() on all protected routes |
| | - Filter all database queries by user_id (user.id) |
| | - Return 401 for authentication failures |
| | - Document protected routes in OpenAPI |
| | - Use HTTPS in production (required for JWT security) |
| | |
| | ❌ DON'T: |
| | - Skip authentication on user-specific endpoints |
| | - Trust client-provided user_id (always extract from JWT) |
| | - Store tokens in server-side sessions |
| | - Expose sensitive data in error messages |
| | - Use JWT without HTTPS (token can be intercepted) |
| | |
| | EXAMPLE PROTECTED ROUTE |
| | ======================== |
| | |
| | Complete example of a protected endpoint: |
| | |
| | from fastapi import APIRouter, Depends |
| | from src.api.deps import get_current_user |
| | from src.models.schemas import TodoResponse |
| | from models.models import User |
| | |
| | router = APIRouter(prefix="/api/todos", tags=["todos"]) |
| | |
| | @router.get("", response_model=list[TodoResponse]) |
| | async def get_todos( |
| | user: User = Depends(get_current_user) |
| | ): |
| | user_id = user.id |
| | |
| | # Get todos for this user only (defense in depth) |
| | todos = await todo_service.get_todos(user_id=user_id) |
| | |
| | return todos |
| | |
| | This route: |
| | - ✅ Requires valid JWT token |
| | - ✅ Extracts user_id from token |
| | - ✅ Filters todos by user_id |
| | - ✅ Returns 401 if authentication fails |
| | - ✅ Documents security in OpenAPI |
| | |
| | For more information: |
| | - FastAPI Dependencies: https://fastapi.tiangolo.com/tutorial/dependencies/ |
| | - FastAPI Security: https://fastapi.tiangolo.com/tutorial/security/ |
| | - OAuth2 with Bearer: https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/ |
| | """ |
| |
|