mirror of
https://github.com/microsoft/autogen.git
synced 2025-07-08 09:31:51 +00:00

<!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> ## Why are these changes needed? https://github.com/user-attachments/assets/b649053b-c377-40c7-aa51-ee64af766fc2 <img width="100%" alt="image" src="https://github.com/user-attachments/assets/03ba1df5-c9a2-4734-b6a2-0eb97ec0b0e0" /> ## Authentication This PR implements an experimental authentication feature to enable personalized experiences (multiple users). Currently, only GitHub authentication is supported. You can extend the base authentication class to add support for other authentication methods. By default authenticatio is disabled and only enabled when you pass in the `--auth-config` argument when running the application. ### Enable GitHub Authentication To enable GitHub authentication, create a `auth.yaml` file in your app directory: ```yaml type: github jwt_secret: "your-secret-key" token_expiry_minutes: 60 github: client_id: "your-github-client-id" client_secret: "your-github-client-secret" callback_url: "http://localhost:8081/api/auth/callback" scopes: ["user:email"] ``` Please see the documentation on [GitHub OAuth](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authenticating-to-the-rest-api-with-an-oauth-app) for more details on obtaining the `client_id` and `client_secret`. To pass in this configuration you can use the `--auth-config` argument when running the application: ```bash autogenstudio ui --auth-config /path/to/auth.yaml ``` Or set the environment variable: ```bash export AUTOGENSTUDIO_AUTH_CONFIG="/path/to/auth.yaml" ``` ```{note} - Authentication is currently experimental and may change in future releases - User data is stored in your configured database - When enabled, all API endpoints require authentication except for the authentication endpoints - WebSocket connections require the token to be passed as a query parameter (`?token=your-jwt-token`) ``` ## Related issue number <!-- For example: "Closes #1234" --> Closes #4350 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed. --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
209 lines
8.0 KiB
Python
209 lines
8.0 KiB
Python
import json
|
|
import secrets
|
|
from abc import ABC, abstractmethod
|
|
from urllib.parse import urlencode
|
|
|
|
import httpx
|
|
from loguru import logger
|
|
|
|
from .exceptions import ConfigurationException, ProviderAuthException
|
|
from .models import AuthConfig, GithubAuthConfig, User
|
|
|
|
|
|
class AuthProvider(ABC):
|
|
"""Base authentication provider interface."""
|
|
|
|
@abstractmethod
|
|
async def get_login_url(self) -> str:
|
|
"""Return the URL for initiating login."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def process_callback(self, code: str, state: str | None = None) -> User:
|
|
"""Process the OAuth callback code and return user data."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def validate_token(self, token: str) -> bool:
|
|
"""Validate a provider token and return boolean indicating validity."""
|
|
pass
|
|
|
|
|
|
class NoAuthProvider(AuthProvider):
|
|
"""Default provider that always authenticates (for development)."""
|
|
|
|
def __init__(self):
|
|
self.default_user = User(
|
|
id="guestuser@gmail.com", name="Default User", email="guestuser@gmail.com", provider="none"
|
|
)
|
|
|
|
async def get_login_url(self) -> str:
|
|
"""Return the URL for initiating login."""
|
|
return "/api/auth/callback?automatic=true"
|
|
|
|
async def process_callback(self, code: str | None = None, state: str | None = None) -> User:
|
|
"""Process the OAuth callback code and return user data."""
|
|
return self.default_user
|
|
|
|
async def validate_token(self, token: str) -> bool:
|
|
"""Validate a provider token and return boolean indicating validity."""
|
|
return True
|
|
|
|
|
|
class GithubAuthProvider(AuthProvider):
|
|
"""GitHub OAuth authentication provider."""
|
|
|
|
def __init__(self, config: AuthConfig):
|
|
if not config.github:
|
|
raise ConfigurationException("GitHub auth configuration is missing")
|
|
|
|
self.config = config.github
|
|
self.client_id = self.config.client_id
|
|
self.client_secret = self.config.client_secret
|
|
self.callback_url = self.config.callback_url
|
|
self.scopes = self.config.scopes
|
|
|
|
async def get_login_url(self) -> str:
|
|
"""Return the GitHub OAuth login URL."""
|
|
state = secrets.token_urlsafe(32) # Generate a secure random state
|
|
params = {
|
|
"client_id": self.client_id,
|
|
"redirect_uri": self.callback_url,
|
|
"scope": " ".join(self.scopes),
|
|
"state": state,
|
|
"allow_signup": "true",
|
|
}
|
|
return f"https://github.com/login/oauth/authorize?{urlencode(params)}"
|
|
|
|
async def process_callback(self, code: str, state: str | None = None) -> User:
|
|
"""Exchange code for access token and get user info."""
|
|
if not code:
|
|
raise ProviderAuthException("github", "Authorization code is missing")
|
|
|
|
# Exchange code for access token
|
|
token_url = "https://github.com/login/oauth/access_token"
|
|
token_data = {
|
|
"client_id": self.client_id,
|
|
"client_secret": self.client_secret,
|
|
"code": code,
|
|
"redirect_uri": self.callback_url,
|
|
}
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
token_response = await client.post(token_url, data=token_data, headers={"Accept": "application/json"})
|
|
|
|
if token_response.status_code != 200:
|
|
logger.error(f"GitHub token exchange failed: {token_response.text}")
|
|
raise ProviderAuthException("github", "Failed to exchange code for access token")
|
|
|
|
token_json = token_response.json()
|
|
access_token = token_json.get("access_token")
|
|
|
|
if not access_token:
|
|
logger.error(f"No access token in GitHub response: {token_json}")
|
|
raise ProviderAuthException("github", "No access token received")
|
|
|
|
# Get user info with the access token
|
|
user_response = await client.get(
|
|
"https://api.github.com/user",
|
|
headers={"Authorization": f"token {access_token}", "Accept": "application/json"},
|
|
)
|
|
|
|
if user_response.status_code != 200:
|
|
logger.error(f"GitHub user info fetch failed: {user_response.text}")
|
|
raise ProviderAuthException("github", "Failed to fetch user information")
|
|
|
|
user_data = user_response.json()
|
|
|
|
# Get user emails if scope includes email
|
|
email = None
|
|
if "user:email" in self.scopes:
|
|
email_response = await client.get(
|
|
"https://api.github.com/user/emails",
|
|
headers={"Authorization": f"token {access_token}", "Accept": "application/json"},
|
|
)
|
|
|
|
if email_response.status_code == 200:
|
|
emails = email_response.json()
|
|
primary_emails = [e for e in emails if e.get("primary") is True]
|
|
if primary_emails:
|
|
email = primary_emails[0].get("email")
|
|
|
|
# Create User object
|
|
return User(
|
|
id=str(user_data.get("id")),
|
|
name=user_data.get("name") or user_data.get("login"),
|
|
email=email,
|
|
avatar_url=user_data.get("avatar_url"),
|
|
provider="github",
|
|
metadata={
|
|
"login": user_data.get("login"),
|
|
"github_id": user_data.get("id"),
|
|
"access_token": access_token,
|
|
},
|
|
)
|
|
|
|
async def validate_token(self, token: str) -> bool:
|
|
"""Validate a GitHub access token."""
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.get(
|
|
"https://api.github.com/user", headers={"Authorization": f"token {token}", "Accept": "application/json"}
|
|
)
|
|
return response.status_code == 200
|
|
|
|
|
|
class MSALAuthProvider(AuthProvider):
|
|
"""Microsoft Authentication Library (MSAL) provider."""
|
|
|
|
def __init__(self, config: AuthConfig):
|
|
if not config.msal:
|
|
raise ConfigurationException("MSAL auth configuration is missing")
|
|
|
|
self.config = config.msal
|
|
# MSAL provider implementation would go here
|
|
# This is a placeholder - full implementation would use msal library
|
|
|
|
async def get_login_url(self) -> str:
|
|
"""Return the MSAL OAuth login URL."""
|
|
# Placeholder - would use MSAL library to generate auth URL
|
|
return "https://login.microsoftonline.com/placeholder"
|
|
|
|
async def process_callback(self, code: str, state: str | None = None) -> User:
|
|
"""Process the MSAL callback."""
|
|
# Placeholder - would use MSAL library to process code and get token/user info
|
|
return User(id="msal_user_id", name="MSAL User", provider="msal")
|
|
|
|
async def validate_token(self, token: str) -> bool:
|
|
"""Validate an MSAL token."""
|
|
# Placeholder - would validate token with MSAL library
|
|
return False
|
|
|
|
|
|
class FirebaseAuthProvider(AuthProvider):
|
|
"""Firebase authentication provider."""
|
|
|
|
def __init__(self, config: AuthConfig):
|
|
if not config.firebase:
|
|
raise ConfigurationException("Firebase auth configuration is missing")
|
|
|
|
self.config = config.firebase
|
|
# Firebase provider implementation would go here
|
|
# This is a placeholder - full implementation would use Firebase Admin SDK
|
|
|
|
async def get_login_url(self) -> str:
|
|
"""Return information for Firebase auth (used differently than OAuth)."""
|
|
# Firebase auth is typically handled on the client side
|
|
return json.dumps(
|
|
{"apiKey": self.config.api_key, "authDomain": self.config.auth_domain, "projectId": self.config.project_id}
|
|
)
|
|
|
|
async def process_callback(self, code: str, state: str | None = None) -> User:
|
|
"""Process a Firebase ID token."""
|
|
# Placeholder - would verify Firebase ID token and get user info
|
|
return User(id="firebase_user_id", name="Firebase User", provider="firebase")
|
|
|
|
async def validate_token(self, token: str) -> bool:
|
|
"""Validate a Firebase ID token."""
|
|
# Placeholder - would validate token with Firebase Admin SDK
|
|
return False
|