Skip to content

Commit 7eecd95

Browse files
committed
Added MSALAuthProvider.
1 parent 3107855 commit 7eecd95

File tree

6 files changed

+168
-19
lines changed

6 files changed

+168
-19
lines changed

python/packages/autogen-studio/autogenstudio/web/auth/manager.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,14 @@ def from_env(cls) -> Self:
144144
"callback_url": os.environ.get("AUTOGENSTUDIO_GITHUB_CALLBACK_URL", ""),
145145
"scopes": os.environ.get("AUTOGENSTUDIO_GITHUB_SCOPES", "user:email").split(","),
146146
}
147-
# Add other provider config parsing here
147+
elif auth_type == "msal":
148+
config_dict["msal"] = {
149+
"tenant_id": os.environ.get("AUTOGENSTUDIO_MSAL_TENANT_ID", ""),
150+
"client_id": os.environ.get("AUTOGENSTUDIO_MSAL_CLIENT_ID", ""),
151+
"client_secret": os.environ.get("AUTOGENSTUDIO_MSAL_CLIENT_SECRET", ""),
152+
"callback_url": os.environ.get("AUTOGENSTUDIO_MSAL_CALLBACK_URL", ""),
153+
"scopes": os.environ.get("AUTOGENSTUDIO_MSAL_SCOPES", "User.Read").split(","),
154+
}
148155

149156
config = AuthConfig(**config_dict)
150157
return cls(config)

python/packages/autogen-studio/autogenstudio/web/auth/providers.py

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from urllib.parse import urlencode
55

66
import httpx
7+
import msal
78
from loguru import logger
89

910
from .exceptions import ConfigurationException, ProviderAuthException
@@ -160,23 +161,99 @@ def __init__(self, config: AuthConfig):
160161
raise ConfigurationException("MSAL auth configuration is missing")
161162

162163
self.config = config.msal
163-
# MSAL provider implementation would go here
164-
# This is a placeholder - full implementation would use msal library
164+
self.tenant_id = self.config.tenant_id
165+
self.client_id = self.config.client_id
166+
self.client_secret = self.config.client_secret
167+
self.callback_url = self.config.callback_url
168+
self.scopes = self.config.scopes
169+
170+
# Initialize MSAL Confidential Client Application
171+
authority = f"https://login.microsoftonline.com/{self.tenant_id}"
172+
self.msal_app = msal.ConfidentialClientApplication(
173+
client_id=self.client_id,
174+
client_credential=self.client_secret,
175+
authority=authority,
176+
)
165177

166178
async def get_login_url(self) -> str:
167-
"""Return the MSAL OAuth login URL."""
168-
# Placeholder - would use MSAL library to generate auth URL
169-
return "https://login.microsoftonline.com/placeholder"
179+
"""Return the Microsoft OAuth login URL."""
180+
state = secrets.token_urlsafe(32) # Generate a secure random state
181+
auth_url = self.msal_app.get_authorization_request_url(
182+
scopes=self.scopes,
183+
state=state,
184+
redirect_uri=self.callback_url,
185+
)
186+
return auth_url
170187

171188
async def process_callback(self, code: str, state: str | None = None) -> User:
172-
"""Process the MSAL callback."""
173-
# Placeholder - would use MSAL library to process code and get token/user info
174-
return User(id="msal_user_id", name="MSAL User", provider="msal")
189+
"""Exchange code for access token and get user info."""
190+
if not code:
191+
raise ProviderAuthException("msal", "Authorization code is missing")
192+
193+
try:
194+
# Exchange code for access token
195+
result = self.msal_app.acquire_token_by_authorization_code(
196+
code=code,
197+
scopes=self.scopes,
198+
redirect_uri=self.callback_url,
199+
)
200+
201+
if "error" in result:
202+
logger.error(f"MSAL token exchange failed: {result.get('error_description', result.get('error'))}")
203+
raise ProviderAuthException("msal", f"Failed to exchange code for access token: {result.get('error')}")
204+
205+
access_token = result.get("access_token")
206+
if not access_token:
207+
logger.error(f"No access token in MSAL response: {result}")
208+
raise ProviderAuthException("msal", "No access token received")
209+
210+
# Get user info with the access token
211+
async with httpx.AsyncClient() as client:
212+
user_response = await client.get(
213+
"https://graph.microsoft.com/v1.0/me",
214+
headers={"Authorization": f"Bearer {access_token}", "Accept": "application/json"},
215+
)
216+
217+
if user_response.status_code != 200:
218+
logger.error(f"Microsoft Graph user info fetch failed: {user_response.text}")
219+
raise ProviderAuthException("msal", "Failed to fetch user information")
220+
221+
user_data = user_response.json()
222+
223+
# Create User object
224+
return User(
225+
id=str(user_data.get("id")),
226+
name=user_data.get("displayName") or user_data.get("userPrincipalName"),
227+
email=user_data.get("mail") or user_data.get("userPrincipalName"),
228+
avatar_url=None, # Microsoft Graph doesn't provide avatar URL in basic profile
229+
provider="msal",
230+
metadata={
231+
"user_principal_name": user_data.get("userPrincipalName"),
232+
"tenant_id": self.tenant_id,
233+
"object_id": user_data.get("id"),
234+
"access_token": access_token,
235+
"id_token": result.get("id_token"),
236+
},
237+
)
238+
239+
except Exception as e:
240+
if isinstance(e, ProviderAuthException):
241+
raise
242+
logger.error(f"MSAL authentication error: {str(e)}")
243+
raise ProviderAuthException("msal", f"Authentication failed: {str(e)}")
175244

176245
async def validate_token(self, token: str) -> bool:
177-
"""Validate an MSAL token."""
178-
# Placeholder - would validate token with MSAL library
179-
return False
246+
"""Validate a Microsoft access token."""
247+
try:
248+
async with httpx.AsyncClient() as client:
249+
response = await client.get(
250+
"https://graph.microsoft.com/v1.0/me",
251+
headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
252+
)
253+
return response.status_code == 200
254+
except Exception as e:
255+
logger.error(f"Token validation error: {str(e)}")
256+
return False
180257

181258

182259
class FirebaseAuthProvider(AuthProvider):

python/packages/autogen-studio/frontend/src/auth/context.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isValidMessageOrigin,
99
isValidUserObject,
1010
} from "../components/utils/security-utils";
11+
import { getAuthProviderInfo } from "./utils";
1112

1213
interface AuthContextType {
1314
user: User | null;
@@ -111,13 +112,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
111112
// Update user state
112113
setUser(data.user);
113114

114-
// Show success message
115-
message.success("Successfully logged in");
115+
// Show success message with provider name
116+
const providerInfo = getAuthProviderInfo(data.user.provider || authType);
117+
message.success(`Successfully signed in with ${providerInfo.displayName}`);
116118

117119
// Redirect to home
118120
navigate(sanitizeRedirectUrl("/"));
119121
} else if (data.type === "auth-error") {
120-
message.error(`Authentication failed: ${data.error}`);
122+
const providerInfo = getAuthProviderInfo(authType);
123+
message.error(`${providerInfo.displayName} authentication failed: ${data.error}`);
121124
}
122125
};
123126

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from "react";
2+
import { GithubOutlined, WindowsOutlined } from "@ant-design/icons";
3+
4+
export interface AuthProviderInfo {
5+
name: string;
6+
displayName: string;
7+
icon: React.ReactNode;
8+
color: string;
9+
connectingText: string;
10+
buttonText: string;
11+
}
12+
13+
export const getAuthProviderInfo = (authType: string): AuthProviderInfo => {
14+
switch (authType) {
15+
case "github":
16+
return {
17+
name: "github",
18+
displayName: "GitHub",
19+
icon: <GithubOutlined />,
20+
color: "#24292f",
21+
connectingText: "Connecting to GitHub...",
22+
buttonText: "Sign in with GitHub",
23+
};
24+
25+
case "msal":
26+
return {
27+
name: "msal",
28+
displayName: "Microsoft",
29+
icon: <WindowsOutlined />,
30+
color: "#0078d4",
31+
connectingText: "Connecting to Microsoft...",
32+
buttonText: "Sign in with Microsoft",
33+
};
34+
35+
default:
36+
return {
37+
name: "unknown",
38+
displayName: "External Provider",
39+
icon: <GithubOutlined />, // fallback icon
40+
color: "#1890ff",
41+
connectingText: "Connecting...",
42+
buttonText: "Sign in",
43+
};
44+
}
45+
};
46+
47+
export const getPopupWindowName = (authType: string): string => {
48+
return `${authType}-auth`;
49+
};
50+
51+
export const isAuthEnabled = (authType: string): boolean => {
52+
return authType !== "none";
53+
};

python/packages/autogen-studio/frontend/src/pages/login.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { GithubOutlined } from "@ant-design/icons";
66
import Layout from "../components/layout";
77
import { graphql } from "gatsby";
88
import Icon from "../components/icons";
9+
import { getAuthProviderInfo, getPopupWindowName } from "../auth/utils";
910

1011
const { Title, Text } = Typography;
1112

@@ -15,6 +16,9 @@ const TOKEN_KEY = "auth_token";
1516
const LoginPage = ({ data }: any) => {
1617
const { isAuthenticated, isLoading, login, authType } = useAuth();
1718
const [isLoggingIn, setIsLoggingIn] = useState(false);
19+
20+
// Get provider-specific UI information
21+
const providerInfo = getAuthProviderInfo(authType);
1822

1923
useEffect(() => {
2024
// If user is already authenticated, redirect to home
@@ -49,7 +53,7 @@ const LoginPage = ({ data }: any) => {
4953

5054
const popup = window.open(
5155
loginUrl,
52-
"github-auth",
56+
getPopupWindowName(authType),
5357
`width=${width},height=${height},top=${top},left=${left}`
5458
);
5559

@@ -113,12 +117,16 @@ const LoginPage = ({ data }: any) => {
113117
<Button
114118
type="primary"
115119
size="large"
116-
icon={<GithubOutlined />}
120+
icon={providerInfo.icon}
117121
onClick={handleLogin}
118122
loading={isLoggingIn}
119123
block
124+
style={{
125+
backgroundColor: isLoggingIn ? undefined : providerInfo.color,
126+
borderColor: isLoggingIn ? undefined : providerInfo.color
127+
}}
120128
>
121-
{isLoggingIn ? "Connecting to GitHub..." : "Sign in with GitHub"}
129+
{isLoggingIn ? providerInfo.connectingText : providerInfo.buttonText}
122130
</Button>
123131
</Space>
124132
</div>

python/packages/autogen-studio/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ dependencies = [
3535
"autogen-agentchat>=0.4.9.2,<0.7",
3636
"autogen-ext[magentic-one, openai, azure, mcp]>=0.4.2,<0.7",
3737
"anthropic",
38-
"mcp>=1.11.0"
38+
"mcp>=1.11.0",
39+
"msal>=1.24.0"
3940
]
4041
optional-dependencies = {web = ["fastapi", "uvicorn"], database = ["psycopg"]}
4142

0 commit comments

Comments
 (0)