Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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)
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.fief_auth_provider import FiefAuthProvider
from carbonserver.api.services.auth_providers.no_auth_provider import NoAuthProvider
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 == "fief":
return FiefAuthProvider(
base_url=settings.fief_url,
client_id=settings.fief_client_id,
client_secret=settings.fief_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: 'fief', '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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""
Fief Authentication Provider Implementation

This module provides a concrete implementation of AuthProvider using Fief.
"""

from typing import Any, Dict, List, Optional, Tuple

from fief_client import FiefAsync

from carbonserver.api.services.auth_providers.auth_provider import AuthProvider


class FiefAuthProvider(AuthProvider):
"""
Fief-based authentication provider implementation.

This class wraps the Fief client to provide authentication services
through the AuthProvider interface.
"""

def __init__(self, base_url: str, client_id: str, client_secret: str):
"""
Initialize the Fief authentication provider.

Args:
base_url: The Fief server base URL
client_id: The OAuth2 client ID
client_secret: The OAuth2 client secret
"""
self.base_url = base_url
self.client_id = client_id
self.client_secret = client_secret
self._client = FiefAsync(base_url, client_id, client_secret)

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 using Fief.

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
"""
return await self._client.auth_url(redirect_uri, scope=scope, state=state)

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 using Fief.

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
"""
tokens, user_info = await self._client.auth_callback(code, redirect_uri)
# Convert Fief objects to dicts
tokens_dict = dict(tokens)
user_info_dict = dict(user_info) if user_info else None
return (tokens_dict, user_info_dict)

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

Args:
token: The access token to validate

Returns:
True if the token is valid

Raises:
Exception if validation fails
"""
await self._client.validate_access_token(token)
return True

async def get_user_info(self, access_token: str) -> Dict[str, Any]:
"""
Get user information from Fief.

Args:
access_token: The access token for the user

Returns:
Dict containing user information (sub, email, name, etc.)
"""
user_info = await self._client.userinfo(access_token)
return dict(user_info)

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

Returns:
The token endpoint URL
"""
return f"{self.base_url}/api/token"

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

Returns:
The authorization endpoint URL
"""
return f"{self.base_url}/authorize"

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

Returns:
A tuple of (client_id, client_secret)
"""
return (self.client_id, self.client_secret)
Loading
Loading