feat: Implement OAuth2 authentication with Google and GitHub
- Added OAuth2 endpoints for Google and GitHub authentication. - Created OAuth service to handle provider interactions and user info retrieval. - Implemented user OAuth repository for managing user OAuth links in the database. - Updated auth service to support linking existing users and creating new users via OAuth. - Added CORS middleware to allow frontend access. - Created tests for OAuth endpoints and service functionality. - Introduced environment configuration for OAuth client IDs and secrets. - Added logging for OAuth operations and error handling.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""FastAPI dependencies."""
|
||||
|
||||
from typing import Annotated, NoReturn, cast
|
||||
from typing import Annotated, cast
|
||||
|
||||
from fastapi import Cookie, Depends, HTTPException, status
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
@@ -9,27 +9,12 @@ from app.core.database import get_db
|
||||
from app.core.logging import get_logger
|
||||
from app.models.user import User
|
||||
from app.services.auth import AuthService
|
||||
from app.services.oauth import OAuthService
|
||||
from app.utils.auth import JWTUtils
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def _raise_invalid_token_error() -> NoReturn:
|
||||
"""Raise an invalid token HTTP exception."""
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token payload",
|
||||
)
|
||||
|
||||
|
||||
def _raise_auth_error() -> NoReturn:
|
||||
"""Raise an authentication HTTP exception."""
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
|
||||
|
||||
async def get_auth_service(
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
) -> AuthService:
|
||||
@@ -37,6 +22,13 @@ async def get_auth_service(
|
||||
return AuthService(session)
|
||||
|
||||
|
||||
async def get_oauth_service(
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
) -> OAuthService:
|
||||
"""Get the OAuth service."""
|
||||
return OAuthService(session)
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
access_token: Annotated[str | None, Cookie()],
|
||||
auth_service: Annotated[AuthService, Depends(get_auth_service)],
|
||||
@@ -46,7 +38,10 @@ async def get_current_user(
|
||||
# Check if access token cookie exists
|
||||
if not access_token:
|
||||
logger.warning("No access token cookie found")
|
||||
_raise_auth_error()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
|
||||
# Decode the JWT token
|
||||
payload = JWTUtils.decode_access_token(access_token)
|
||||
@@ -54,7 +49,10 @@ async def get_current_user(
|
||||
# Extract user ID from token
|
||||
user_id_str = payload.get("sub")
|
||||
if not user_id_str:
|
||||
_raise_invalid_token_error()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token payload",
|
||||
)
|
||||
|
||||
# At this point user_id_str is guaranteed to be truthy, safe to cast
|
||||
user_id_str = cast("str", user_id_str)
|
||||
@@ -74,9 +72,12 @@ async def get_current_user(
|
||||
except HTTPException:
|
||||
# Re-raise HTTPExceptions without wrapping them
|
||||
raise
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.exception("Failed to authenticate user")
|
||||
_raise_auth_error()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
) from e
|
||||
|
||||
|
||||
async def get_current_active_user(
|
||||
|
||||
Reference in New Issue
Block a user