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) => {
}
+ icon={providerInfo.icon}
onClick={handleLogin}
loading={isLoggingIn}
block
+ style={{
+ backgroundColor: isLoggingIn ? undefined : providerInfo.color,
+ borderColor: isLoggingIn ? undefined : providerInfo.color
+ }}
>
- {isLoggingIn ? "Connecting to GitHub..." : "Sign in with GitHub"}
+ {isLoggingIn ? providerInfo.connectingText : providerInfo.buttonText}
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"]}