Victor Dibia fe1feb3906
Enable Auth in AGS (#5928)
<!-- 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>
2025-03-14 15:02:05 -07:00

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