""" Authentication Controller Provides authentication endpoints for OIDC token sync and user management. """ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import JSONResponse from sqlalchemy.ext.asyncio import AsyncSession from src.controllers.base import BaseController from src.logging_config import get_logger from src.db import get_async_session from src.auth.schemas import AuthSyncRequest, AuthSyncResponse, UsersListResponse, BulkSyncResultSchema, GroupsListResponse from src.auth.service import AuthService from src.auth.oidc import get_forward_auth_user logger = get_logger(__name__) class AuthController(BaseController): """ Controller for authentication operations Provides endpoints for: - Token synchronization (login) - User profile retrieval """ def __init__(self): super().__init__(prefix="/auth", tags=["Authentication"]) def create_router(self) -> APIRouter: """Create and configure the router""" router = APIRouter(prefix=self.prefix, tags=self.tags) @router.post( "/sync", summary="Sync user from OIDC token", response_model=AuthSyncResponse, responses={ 200: {"description": "User synced successfully"}, 401: {"description": "Invalid or expired token"}, 503: {"description": "Authentication service unavailable"}, }, ) async def sync_user( request: AuthSyncRequest, session: AsyncSession = Depends(get_async_session), ) -> AuthSyncResponse: """ Synchronize user from OIDC access token This endpoint should be called after the client obtains an access token from Authentik. It: 1. Validates the token via Authentik's userinfo endpoint 2. Creates or updates the user in the database 3. Syncs roles from Authentik groups 4. Returns the user profile with roles and preferences The client should store the returned user info for local use. """ service = AuthService(session) try: # Validate token with Authentik token_info = await service.validate_token(request.access_token) except ValueError as e: logger.warning(f"Token validation failed: {e}") raise HTTPException(status_code=401, detail=str(e)) # Sync user to database user, is_new = await service.sync_user(token_info) # Sync roles from groups roles = await service.sync_roles(user, token_info.groups) # Commit the transaction await session.commit() # Refresh to get relationships await session.refresh(user, ["preferences"]) # Build response return AuthSyncResponse( user=service.user_to_schema(user), roles=service.roles_to_schema(roles), preferences=service.preferences_to_schema(user.preferences), is_new_user=is_new, ) @router.get( "/users", summary="List all users", response_model=UsersListResponse, responses={ 200: {"description": "List of users"}, }, ) async def list_users( search: Optional[str] = Query(None, description="Search by name or email"), offset: int = Query(0, ge=0, description="Number of records to skip"), limit: int = Query(50, ge=1, le=100, description="Maximum records to return"), session: AsyncSession = Depends(get_async_session), ) -> UsersListResponse: """ List all users who have logged in via Authentik Returns paginated list of users with their roles. Supports search filtering by name or email. """ service = AuthService(session) items, total = await service.list_users( search=search, offset=offset, limit=limit, ) return UsersListResponse(items=items, total=total) @router.post( "/users/sync-from-authentik", summary="Bulk sync users from Authentik", response_model=BulkSyncResultSchema, responses={ 200: {"description": "Sync completed"}, 401: {"description": "Authentik API token invalid"}, 503: {"description": "Authentik service unavailable"}, }, ) async def sync_users_from_authentik( session: AsyncSession = Depends(get_async_session), ) -> BulkSyncResultSchema: """ Fetch all users from Authentik and sync to local database This endpoint uses the Authentik admin API to fetch all users and create/update them in the local database. Requires AUTHENTIK_CORE_API_TOKEN to be configured. Use this to initially populate users or to re-sync after changes in Authentik. """ service = AuthService(session) try: result = await service.bulk_sync_from_authentik() logger.info( f"Bulk sync completed: {result.created} created, " f"{result.updated} updated, {result.failed} failed" ) return result except ValueError as e: logger.error(f"Bulk sync failed: {e}") raise HTTPException(status_code=401, detail=str(e)) @router.get( "/groups", summary="List all groups", response_model=GroupsListResponse, responses={ 200: {"description": "List of groups"}, }, ) async def list_groups( search: Optional[str] = Query(None, description="Search by group name"), offset: int = Query(0, ge=0, description="Number of records to skip"), limit: int = Query(50, ge=1, le=100, description="Maximum records to return"), session: AsyncSession = Depends(get_async_session), ) -> GroupsListResponse: """ List all groups synced from Authentik Returns paginated list of groups with their details. Supports search filtering by name. """ service = AuthService(session) items, total = await service.list_groups( search=search, offset=offset, limit=limit, ) return GroupsListResponse(items=items, total=total) @router.post( "/groups/sync-from-authentik", summary="Bulk sync groups from Authentik", response_model=BulkSyncResultSchema, responses={ 200: {"description": "Sync completed"}, 401: {"description": "Authentik API credentials invalid"}, 503: {"description": "Authentik service unavailable"}, }, ) async def sync_groups_from_authentik( session: AsyncSession = Depends(get_async_session), ) -> BulkSyncResultSchema: """ Fetch all groups from Authentik and sync to local database This endpoint uses the Authentik admin API to fetch all groups and create/update them in the local database. Requires AUTHENTIK_USERNAME and AUTHENTIK_PASSWORD to be configured. Use this to populate groups or to re-sync after changes in Authentik. """ service = AuthService(session) try: result = await service.bulk_sync_groups_from_authentik() logger.info( f"Groups bulk sync completed: {result.created} created, " f"{result.updated} updated, {result.failed} failed" ) return result except ValueError as e: logger.error(f"Groups bulk sync failed: {e}") raise HTTPException(status_code=401, detail=str(e)) @router.get( "/me", summary="Get current user profile", response_model=AuthSyncResponse, responses={ 200: {"description": "User profile"}, 401: {"description": "Not authenticated"}, 404: {"description": "User not found in database"}, }, ) async def get_me( forward_auth_user: Optional[dict] = Depends(get_forward_auth_user), session: AsyncSession = Depends(get_async_session), ) -> AuthSyncResponse: """ Get the current authenticated user's profile Authentication is handled by NPM forward auth with Authentik. The proxy sets X-authentik-* headers which this endpoint reads. For internal/LAN access (no forward auth headers), returns 401. Use POST /auth/sync with an OIDC token for mobile app authentication. """ # Require forward auth for this endpoint if forward_auth_user is None: raise HTTPException( status_code=401, detail="Authentication required - access via authenticated proxy or use /auth/sync", ) service = AuthService(session) # Try to find user by Authentik UID first, then by email user = None uid = forward_auth_user.get("uid") if uid: try: import uuid authentik_id = uuid.UUID(uid) user = await service.get_user_by_authentik_id(authentik_id) except (ValueError, TypeError): pass # Invalid UUID, try email if user is None: email = forward_auth_user.get("email") if email: user = await service.get_user_by_email(email) if user is None: # User authenticated with Authentik but not synced to database yet # This can happen on first login via web logger.info(f"User {forward_auth_user.get('email')} not found, creating from forward auth") # Create user from forward auth headers from src.auth.schemas import TokenInfoSchema token_info = TokenInfoSchema( sub=forward_auth_user.get("uid", ""), email=forward_auth_user.get("email", ""), name=forward_auth_user.get("name"), groups=forward_auth_user.get("groups", []), ) try: user, _ = await service.sync_user(token_info) await service.sync_roles(user, token_info.groups) await session.commit() await session.refresh(user, ["preferences", "roles"]) except Exception as e: logger.error(f"Failed to create user from forward auth: {e}") raise HTTPException( status_code=500, detail="Failed to create user profile", ) # Sync roles from current groups (in case they changed) groups = forward_auth_user.get("groups", []) roles = await service.sync_roles(user, groups) await session.commit() return AuthSyncResponse( user=service.user_to_schema(user), roles=service.roles_to_schema(roles), preferences=service.preferences_to_schema(user.preferences), is_new_user=False, ) return router # Create controller instance auth_controller = AuthController()