diff --git a/python/packages/autogen-studio/autogenstudio/web/auth/manager.py b/python/packages/autogen-studio/autogenstudio/web/auth/manager.py index ab16e0432d0a..09cc1c775798 100644 --- a/python/packages/autogen-studio/autogenstudio/web/auth/manager.py +++ b/python/packages/autogen-studio/autogenstudio/web/auth/manager.py @@ -144,7 +144,14 @@ def from_env(cls) -> Self: "callback_url": os.environ.get("AUTOGENSTUDIO_GITHUB_CALLBACK_URL", ""), "scopes": os.environ.get("AUTOGENSTUDIO_GITHUB_SCOPES", "user:email").split(","), } - # Add other provider config parsing here + elif auth_type == "msal": + config_dict["msal"] = { + "tenant_id": os.environ.get("AUTOGENSTUDIO_MSAL_TENANT_ID", ""), + "client_id": os.environ.get("AUTOGENSTUDIO_MSAL_CLIENT_ID", ""), + "client_secret": os.environ.get("AUTOGENSTUDIO_MSAL_CLIENT_SECRET", ""), + "callback_url": os.environ.get("AUTOGENSTUDIO_MSAL_CALLBACK_URL", ""), + "scopes": os.environ.get("AUTOGENSTUDIO_MSAL_SCOPES", "User.Read").split(","), + } config = AuthConfig(**config_dict) return cls(config) diff --git a/python/packages/autogen-studio/autogenstudio/web/auth/providers.py b/python/packages/autogen-studio/autogenstudio/web/auth/providers.py index 974c9bd90d1b..432947df5122 100644 --- a/python/packages/autogen-studio/autogenstudio/web/auth/providers.py +++ b/python/packages/autogen-studio/autogenstudio/web/auth/providers.py @@ -4,6 +4,7 @@ from urllib.parse import urlencode import httpx +import msal from loguru import logger from .exceptions import ConfigurationException, ProviderAuthException @@ -160,23 +161,99 @@ def __init__(self, config: AuthConfig): 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 + self.tenant_id = self.config.tenant_id + 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 + + # Initialize MSAL Confidential Client Application + authority = f"https://login.microsoftonline.com/{self.tenant_id}" + self.msal_app = msal.ConfidentialClientApplication( + client_id=self.client_id, + client_credential=self.client_secret, + authority=authority, + ) 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" + """Return the Microsoft OAuth login URL.""" + state = secrets.token_urlsafe(32) # Generate a secure random state + auth_url = self.msal_app.get_authorization_request_url( + scopes=self.scopes, + state=state, + redirect_uri=self.callback_url, + ) + return auth_url 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") + """Exchange code for access token and get user info.""" + if not code: + raise ProviderAuthException("msal", "Authorization code is missing") + + try: + # Exchange code for access token + result = self.msal_app.acquire_token_by_authorization_code( + code=code, + scopes=self.scopes, + redirect_uri=self.callback_url, + ) + + if "error" in result: + logger.error(f"MSAL token exchange failed: {result.get('error_description', result.get('error'))}") + raise ProviderAuthException("msal", f"Failed to exchange code for access token: {result.get('error')}") + + access_token = result.get("access_token") + if not access_token: + logger.error(f"No access token in MSAL response: {result}") + raise ProviderAuthException("msal", "No access token received") + + # Get user info with the access token + async with httpx.AsyncClient() as client: + user_response = await client.get( + "https://graph.microsoft.com/v1.0/me", + headers={"Authorization": f"Bearer {access_token}", "Accept": "application/json"}, + ) + + if user_response.status_code != 200: + logger.error(f"Microsoft Graph user info fetch failed: {user_response.text}") + raise ProviderAuthException("msal", "Failed to fetch user information") + + user_data = user_response.json() + + # Create User object + return User( + id=str(user_data.get("id")), + name=user_data.get("displayName") or user_data.get("userPrincipalName"), + email=user_data.get("mail") or user_data.get("userPrincipalName"), + avatar_url=None, # Microsoft Graph doesn't provide avatar URL in basic profile + provider="msal", + metadata={ + "user_principal_name": user_data.get("userPrincipalName"), + "tenant_id": self.tenant_id, + "object_id": user_data.get("id"), + "access_token": access_token, + "id_token": result.get("id_token"), + }, + ) + + except Exception as e: + if isinstance(e, ProviderAuthException): + raise + logger.error(f"MSAL authentication error: {str(e)}") + raise ProviderAuthException("msal", f"Authentication failed: {str(e)}") async def validate_token(self, token: str) -> bool: - """Validate an MSAL token.""" - # Placeholder - would validate token with MSAL library - return False + """Validate a Microsoft access token.""" + try: + async with httpx.AsyncClient() as client: + response = await client.get( + "https://graph.microsoft.com/v1.0/me", + headers={"Authorization": f"Bearer {token}", "Accept": "application/json"}, + ) + return response.status_code == 200 + except Exception as e: + logger.error(f"Token validation error: {str(e)}") + return False class FirebaseAuthProvider(AuthProvider): diff --git a/python/packages/autogen-studio/frontend/src/auth/context.tsx b/python/packages/autogen-studio/frontend/src/auth/context.tsx index 5f7ecd947f97..eb4188729252 100644 --- a/python/packages/autogen-studio/frontend/src/auth/context.tsx +++ b/python/packages/autogen-studio/frontend/src/auth/context.tsx @@ -8,6 +8,7 @@ import { isValidMessageOrigin, isValidUserObject, } from "../components/utils/security-utils"; +import { getAuthProviderInfo } from "./utils"; interface AuthContextType { user: User | null; @@ -111,13 +112,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ // Update user state setUser(data.user); - // Show success message - message.success("Successfully logged in"); + // Show success message with provider name + const providerInfo = getAuthProviderInfo(data.user.provider || authType); + message.success(`Successfully signed in with ${providerInfo.displayName}`); // Redirect to home navigate(sanitizeRedirectUrl("/")); } else if (data.type === "auth-error") { - message.error(`Authentication failed: ${data.error}`); + const providerInfo = getAuthProviderInfo(authType); + message.error(`${providerInfo.displayName} authentication failed: ${data.error}`); } }; diff --git a/python/packages/autogen-studio/frontend/src/auth/utils.tsx b/python/packages/autogen-studio/frontend/src/auth/utils.tsx new file mode 100644 index 000000000000..e4997f760d12 --- /dev/null +++ b/python/packages/autogen-studio/frontend/src/auth/utils.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { GithubOutlined, WindowsOutlined, LoginOutlined } from "@ant-design/icons"; + +export interface AuthProviderInfo { + name: string; + displayName: string; + icon: React.ReactNode; + color: string; + connectingText: string; + buttonText: string; +} + +export const getAuthProviderInfo = (authType: string): AuthProviderInfo => { + switch (authType) { + case "github": + return { + name: "github", + displayName: "GitHub", + icon: , + color: "#24292f", + connectingText: "Connecting to GitHub...", + buttonText: "Sign in with GitHub", + }; + + case "msal": + return { + name: "msal", + displayName: "Microsoft", + icon: , + color: "#0078d4", + connectingText: "Connecting to Microsoft...", + buttonText: "Sign in with Microsoft", + }; + + default: + return { + name: "unknown", + displayName: "External Provider", + icon: , // generic auth icon + color: "#1890ff", + connectingText: "Connecting...", + buttonText: "Sign in", + }; + } +}; + +export const getPopupWindowName = (authType: string): string => { + return `${authType}-auth`; +}; + +export const isAuthEnabled = (authType: string): boolean => { + return authType !== "none"; +}; \ No newline at end of file diff --git a/python/packages/autogen-studio/frontend/src/pages/login.tsx b/python/packages/autogen-studio/frontend/src/pages/login.tsx index 4401126a7c5b..572600dcac91 100644 --- a/python/packages/autogen-studio/frontend/src/pages/login.tsx +++ b/python/packages/autogen-studio/frontend/src/pages/login.tsx @@ -6,6 +6,7 @@ import { GithubOutlined } from "@ant-design/icons"; import Layout from "../components/layout"; import { graphql } from "gatsby"; import Icon from "../components/icons"; +import { getAuthProviderInfo, getPopupWindowName } from "../auth/utils"; const { Title, Text } = Typography; @@ -15,6 +16,9 @@ const TOKEN_KEY = "auth_token"; const LoginPage = ({ data }: any) => { const { isAuthenticated, isLoading, login, authType } = useAuth(); const [isLoggingIn, setIsLoggingIn] = useState(false); + + // Get provider-specific UI information + const providerInfo = getAuthProviderInfo(authType); useEffect(() => { // If user is already authenticated, redirect to home @@ -49,7 +53,7 @@ const LoginPage = ({ data }: any) => { const popup = window.open( loginUrl, - "github-auth", + getPopupWindowName(authType), `width=${width},height=${height},top=${top},left=${left}` ); @@ -113,12 +117,16 @@ const LoginPage = ({ data }: any) => { diff --git a/python/packages/autogen-studio/pyproject.toml b/python/packages/autogen-studio/pyproject.toml index 72817ac21d19..9ffefd8bcddb 100644 --- a/python/packages/autogen-studio/pyproject.toml +++ b/python/packages/autogen-studio/pyproject.toml @@ -35,7 +35,8 @@ dependencies = [ "autogen-agentchat>=0.4.9.2,<0.7", "autogen-ext[magentic-one, openai, azure, mcp]>=0.4.2,<0.7", "anthropic", - "mcp>=1.11.0" + "mcp>=1.11.0", + "msal>=1.24.0" ] optional-dependencies = {web = ["fastapi", "uvicorn"], database = ["psycopg"]}