Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions carbonserver/carbonserver/api/routers/authenticate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from dependency_injector.wiring import Provide, inject
from fastapi import APIRouter, Depends, Query, Request, Response
from fastapi.responses import RedirectResponse
from fief_client import FiefAsync

from carbonserver.api.services.auth_providers.auth_provider import AuthProvider
from carbonserver.api.services.auth_service import (
OptionalUserWithAuthDependency,
UserWithAuthDependency,
Expand All @@ -24,16 +24,13 @@

router = APIRouter()

fief = FiefAsync(
settings.fief_url, settings.fief_client_id, settings.fief_client_secret
)


@router.get("/auth/check", name="auth-check")
@inject
def check_login(
auth_user: UserWithAuthDependency = Depends(OptionalUserWithAuthDependency),
sign_up_service: SignUpService = Depends(Provide[ServerContainer.sign_up_service]),
auth_provider: AuthProvider = Depends(Provide[ServerContainer.auth_provider]),
):
"""
return user data or redirect to login screen
Expand All @@ -44,9 +41,15 @@ def check_login(


@router.get("/auth/auth-callback", name="auth_callback")
async def auth_callback(request: Request, response: Response, code: str = Query(...)):
@inject
async def auth_callback(
request: Request,
response: Response,
code: str = Query(...),
auth_provider: AuthProvider = Depends(Provide[ServerContainer.auth_provider]),
):
redirect_uri = request.url_for("auth_callback")
tokens, _ = await fief.auth_callback(code, redirect_uri)
tokens, _ = await auth_provider.handle_auth_callback(code, str(redirect_uri))
response = RedirectResponse(request.url_for("auth-user"))
response.set_cookie(
SESSION_COOKIE_NAME,
Expand All @@ -65,33 +68,32 @@ async def get_login(
state: Optional[str] = None,
code: Optional[str] = None,
sign_up_service: SignUpService = Depends(Provide[ServerContainer.sign_up_service]),
auth_provider: AuthProvider = Depends(Provide[ServerContainer.auth_provider]),
):
"""
login and redirect to frontend app with token
"""
login_url = request.url_for("login")

if code:
client_id, client_secret = auth_provider.get_client_credentials()
res = requests.post(
f"{settings.fief_url}/api/token",
auth_provider.get_token_endpoint(),
data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": login_url,
"client_id": settings.fief_client_id,
"client_secret": settings.fief_client_secret,
"client_id": client_id,
"client_secret": client_secret,
},
)

# check if the user exists in local DB ; create if needed
if "id_token" not in res.json():
if "access_token" not in res.json():
return Response(content="Invalid code", status_code=400)
# get profile data from fief server if not present in response
id_token = requests.get(
settings.fief_url + "/api/userinfo",
headers={"Authorization": "Bearer " + res.json()["access_token"]},
).json()
# get profile data from auth provider if not present in response
id_token = await auth_provider.get_user_info(res.json()["access_token"])
sign_up_service.check_jwt_user(id_token)
else:
sign_up_service.check_jwt_user(res.json()["id_token"], create=True)
Expand Down Expand Up @@ -123,5 +125,7 @@ async def get_login(
return response

state = str(int(random.random() * 1000))
url = f"{settings.fief_url}/authorize?response_type=code&client_id={settings.fief_client_id}&redirect_uri={login_url}&scope={' '.join(OAUTH_SCOPES)}&state={state}"
client_id, _ = auth_provider.get_client_credentials()
authorize_url = auth_provider.get_authorize_endpoint()
url = f"{authorize_url}?response_type=code&client_id={client_id}&redirect_uri={login_url}&scope={' '.join(OAUTH_SCOPES)}&state={state}"
return RedirectResponse(url=url)
106 changes: 106 additions & 0 deletions carbonserver/carbonserver/api/services/auth_providers/auth_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
Authentication Provider Interface

This module defines an abstract interface for authentication providers.
To implement a custom authentication provider, create a class that inherits
from AuthProvider and implements all the required methods.
"""

from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Tuple


class AuthProvider(ABC):
"""
Abstract base class for authentication providers.

This interface allows CodeCarbon to support multiple authentication providers
(Fief, Auth0, Keycloak, custom OAuth2, etc.) by implementing this interface.
"""

@abstractmethod
async def get_auth_url(
self, redirect_uri: str, scope: List[str], state: Optional[str] = None
) -> str:
"""
Generate the authorization URL for the OAuth2 flow.

Args:
redirect_uri: The URI to redirect to after authentication
scope: List of OAuth2 scopes to request
state: Optional state parameter for CSRF protection

Returns:
The authorization URL to redirect the user to
"""

@abstractmethod
async def handle_auth_callback(
self, code: str, redirect_uri: str
) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]]]:
"""
Handle the OAuth2 callback and exchange the code for tokens.

Args:
code: The authorization code from the OAuth2 provider
redirect_uri: The redirect URI used in the initial auth request

Returns:
A tuple of (tokens, user_info) where:
- tokens: Dict containing access_token, refresh_token, expires_in, etc.
- user_info: Optional dict containing user information
"""

@abstractmethod
async def validate_access_token(self, token: str) -> bool:
"""
Validate an access token.

Args:
token: The access token to validate

Returns:
True if the token is valid, False otherwise

Raises:
Exception if validation fails
"""

@abstractmethod
async def get_user_info(self, access_token: str) -> Dict[str, Any]:
"""
Get user information from the authentication provider.

Args:
access_token: The access token for the user

Returns:
Dict containing user information (sub, email, name, etc.)
"""

@abstractmethod
def get_token_endpoint(self) -> str:
"""
Get the token endpoint URL for the provider.

Returns:
The token endpoint URL
"""

@abstractmethod
def get_authorize_endpoint(self) -> str:
"""
Get the authorization endpoint URL for the provider.

Returns:
The authorization endpoint URL
"""

@abstractmethod
def get_client_credentials(self) -> Tuple[str, str]:
"""
Get the client ID and client secret.

Returns:
A tuple of (client_id, client_secret)
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Authentication Provider Factory

This module provides a factory function to create the appropriate authentication
provider based on configuration settings.
"""

from typing import Optional

from carbonserver.api.services.auth_providers.auth_provider import AuthProvider
from carbonserver.api.services.auth_providers.no_auth_provider import NoAuthProvider
from carbonserver.api.services.auth_providers.oidc_auth_provider import OIDCAuthProvider
from carbonserver.config import settings


def create_auth_provider(provider_name: Optional[str] = None) -> AuthProvider:
"""
Factory function to create an authentication provider based on configuration.

Args:
provider_name: Optional provider name override. If not provided,
uses the AUTH_PROVIDER setting from config.

Returns:
An instance of AuthProvider

Raises:
ValueError: If the provider name is not recognized

Example:
```python
# Using default from settings
provider = create_auth_provider()

# Override provider
provider = create_auth_provider("none")
```
"""
provider_type = provider_name or settings.auth_provider
provider_type = provider_type.lower()

if provider_type in ("oidc", "fief"): # Support 'fief' for backward compatibility
return OIDCAuthProvider(
base_url=settings.oidc_issuer_url,
client_id=settings.oidc_client_id,
client_secret=settings.oidc_client_secret,
)
elif provider_type == "none":
# No authentication - for development/internal use only
return NoAuthProvider()
else:
raise ValueError(
f"Unknown authentication provider: {provider_type}. "
f"Supported providers: 'oidc', 'fief' (deprecated), 'none'"
)


def get_auth_provider() -> AuthProvider:
"""
Get the configured authentication provider instance.

This is a convenience function that creates a provider using the
settings from the environment.

Returns:
An instance of AuthProvider
"""
return create_auth_provider()
Loading