What?
This commit is contained in:
+90
-30
@@ -21,12 +21,57 @@ class AzureAuthenticator:
|
||||
self.credential: Optional[InteractiveBrowserCredential] = None
|
||||
self.subscriptions: List[Dict[str, str]] = []
|
||||
|
||||
def authenticate(self, credential: InteractiveBrowserCredential = None) -> bool:
|
||||
def discover_tenant_id(self) -> tuple[str, InteractiveBrowserCredential, List[Dict[str, str]]]:
|
||||
"""
|
||||
Authenticate to Azure. Can reuse credential from Graph authenticator.
|
||||
Discover user's tenant ID by authenticating with organizations endpoint.
|
||||
|
||||
Returns:
|
||||
tuple: (tenant_id, credential, subscriptions) - The discovered tenant ID, credential to reuse, and subscription list
|
||||
|
||||
Raises:
|
||||
Exception: If no subscriptions found or authentication fails
|
||||
"""
|
||||
try:
|
||||
# Create temporary credential with "organizations" for discovery
|
||||
temp_credential = InteractiveBrowserCredential(
|
||||
tenant_id="organizations",
|
||||
additionally_allowed_tenants=["*"]
|
||||
)
|
||||
|
||||
# List subscriptions to extract tenant
|
||||
sub_client = SubscriptionClient(temp_credential)
|
||||
subscriptions_list = list(sub_client.subscriptions.list())
|
||||
|
||||
if not subscriptions_list:
|
||||
raise Exception("No Azure subscriptions found. Please ensure you have access to at least one subscription.")
|
||||
|
||||
# Extract tenant ID from first subscription
|
||||
tenant_id = subscriptions_list[0].tenant_id
|
||||
print(f"Discovered Tenant ID: {tenant_id}")
|
||||
|
||||
# Convert subscriptions to dict format
|
||||
subscriptions = [
|
||||
{
|
||||
'id': sub.subscription_id,
|
||||
'name': sub.display_name
|
||||
}
|
||||
for sub in subscriptions_list
|
||||
]
|
||||
|
||||
# Return tenant_id, credential, and subscriptions for reuse
|
||||
return tenant_id, temp_credential, subscriptions
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to discover tenant: {str(e)}")
|
||||
|
||||
def authenticate(self, credential: InteractiveBrowserCredential = None, tenant_id: str = None, subscriptions: List[Dict[str, str]] = None) -> bool:
|
||||
"""
|
||||
Authenticate to Azure. Can reuse credential, use specific tenant, or discover tenant.
|
||||
|
||||
Args:
|
||||
credential: Optional credential to reuse (from GraphAuthenticator)
|
||||
tenant_id: Optional specific tenant ID to use
|
||||
subscriptions: Optional pre-fetched subscriptions list (avoids re-listing)
|
||||
|
||||
Returns:
|
||||
bool: True if authentication succeeded, False otherwise
|
||||
@@ -36,45 +81,60 @@ class AzureAuthenticator:
|
||||
"""
|
||||
try:
|
||||
if credential:
|
||||
# Reuse credential from Graph authentication
|
||||
# Reuse credential (recommended path to avoid multiple auth prompts)
|
||||
self.credential = credential
|
||||
else:
|
||||
# Create new interactive browser credential with "organizations" tenant
|
||||
# This allows the user to login with any organizational account
|
||||
# additionally_allowed_tenants="*" allows acquiring tokens for any tenant
|
||||
# Create credential with specific tenant ID
|
||||
if not tenant_id:
|
||||
raise Exception("Either credential or tenant_id must be provided")
|
||||
|
||||
self.tenant_id = tenant_id
|
||||
|
||||
# Create NEW credential with specific tenant_id (avoids redirect)
|
||||
self.credential = InteractiveBrowserCredential(
|
||||
tenant_id="organizations",
|
||||
tenant_id=tenant_id,
|
||||
additionally_allowed_tenants=["*"]
|
||||
)
|
||||
|
||||
# List all subscriptions (this will use the cached token from Graph auth)
|
||||
sub_client = SubscriptionClient(self.credential)
|
||||
subscriptions_list = list(sub_client.subscriptions.list())
|
||||
# Use provided subscriptions or list them if not provided
|
||||
if subscriptions:
|
||||
# Reuse pre-fetched subscriptions (avoids duplicate API call)
|
||||
self.subscriptions = subscriptions
|
||||
|
||||
# Extract tenant ID from the first subscription
|
||||
if subscriptions_list:
|
||||
# Get tenant ID from subscription (format: /subscriptions/{sub-id})
|
||||
# The tenant info is in the subscription object
|
||||
first_sub = subscriptions_list[0]
|
||||
self.tenant_id = first_sub.tenant_id if hasattr(first_sub, 'tenant_id') else None
|
||||
if self.tenant_id:
|
||||
print(f"Detected Tenant ID: {self.tenant_id}")
|
||||
# Set tenant ID if provided
|
||||
if tenant_id:
|
||||
self.tenant_id = tenant_id
|
||||
|
||||
if subscriptions_list:
|
||||
print(f"Successfully authenticated to Azure. Found {len(subscriptions_list)} subscription(s).")
|
||||
|
||||
# Store subscriptions for later selection
|
||||
self.subscriptions = [
|
||||
{
|
||||
'id': sub.subscription_id,
|
||||
'name': sub.display_name
|
||||
}
|
||||
for sub in subscriptions_list
|
||||
]
|
||||
print(f"Using cached authentication. Found {len(subscriptions)} subscription(s).")
|
||||
|
||||
return True
|
||||
else:
|
||||
# List all subscriptions (fallback for legacy code path)
|
||||
sub_client = SubscriptionClient(self.credential)
|
||||
subscriptions_list = list(sub_client.subscriptions.list())
|
||||
|
||||
return False
|
||||
# Extract tenant ID if not already set
|
||||
if subscriptions_list and not self.tenant_id:
|
||||
first_sub = subscriptions_list[0]
|
||||
self.tenant_id = first_sub.tenant_id if hasattr(first_sub, 'tenant_id') else None
|
||||
if self.tenant_id:
|
||||
print(f"Detected Tenant ID: {self.tenant_id}")
|
||||
|
||||
if subscriptions_list:
|
||||
print(f"Successfully authenticated to Azure. Found {len(subscriptions_list)} subscription(s).")
|
||||
|
||||
# Store subscriptions for later selection
|
||||
self.subscriptions = [
|
||||
{
|
||||
'id': sub.subscription_id,
|
||||
'name': sub.display_name
|
||||
}
|
||||
for sub in subscriptions_list
|
||||
]
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Azure authentication failed: {str(e)}")
|
||||
|
||||
+29
-17
@@ -24,10 +24,15 @@ class GraphAuthenticator:
|
||||
self.credential: Optional[InteractiveBrowserCredential] = None
|
||||
self.client: Optional[GraphServiceClient] = None
|
||||
|
||||
async def authenticate(self) -> bool:
|
||||
async def authenticate(self, tenant_id: str = None, credential: InteractiveBrowserCredential = None, skip_validation: bool = False) -> bool:
|
||||
"""
|
||||
Authenticate to Microsoft Graph using interactive browser login.
|
||||
|
||||
Args:
|
||||
tenant_id: Optional specific tenant ID (recommended to avoid double auth)
|
||||
credential: Optional credential to reuse (avoids creating new credential)
|
||||
skip_validation: Skip the me.get() validation call (use when reusing credential to avoid extra auth)
|
||||
|
||||
Returns:
|
||||
bool: True if authentication succeeded, False otherwise
|
||||
|
||||
@@ -35,14 +40,19 @@ class GraphAuthenticator:
|
||||
Exception: If authentication fails
|
||||
"""
|
||||
try:
|
||||
# Create interactive browser credential
|
||||
# Using "organizations" allows login with any organizational account
|
||||
# additionally_allowed_tenants="*" allows acquiring tokens for any tenant (needed for Key Vault access)
|
||||
self.credential = InteractiveBrowserCredential(
|
||||
tenant_id="organizations",
|
||||
client_id=self.client_id,
|
||||
additionally_allowed_tenants=["*"]
|
||||
)
|
||||
if credential:
|
||||
# Reuse provided credential (recommended to avoid multiple auth prompts)
|
||||
self.credential = credential
|
||||
else:
|
||||
# Use specific tenant if provided, otherwise fall back to "organizations"
|
||||
auth_tenant = tenant_id if tenant_id else "organizations"
|
||||
|
||||
# Create interactive browser credential
|
||||
self.credential = InteractiveBrowserCredential(
|
||||
tenant_id=auth_tenant,
|
||||
client_id=self.client_id,
|
||||
additionally_allowed_tenants=["*"]
|
||||
)
|
||||
|
||||
# Define scopes for Microsoft Graph
|
||||
scopes = ['https://graph.microsoft.com/.default']
|
||||
@@ -53,17 +63,19 @@ class GraphAuthenticator:
|
||||
scopes=scopes
|
||||
)
|
||||
|
||||
# Authenticate to Graph API FIRST
|
||||
# This triggers the initial browser auth for Graph scope
|
||||
# Then Management API will use SSO (single sign-on) from this auth
|
||||
me = await self.client.me.get()
|
||||
# Validate authentication (unless skip_validation is True)
|
||||
if not skip_validation:
|
||||
me = await self.client.me.get()
|
||||
|
||||
if me:
|
||||
print(f"Successfully authenticated as: {me.display_name} ({me.user_principal_name})")
|
||||
if me:
|
||||
print(f"Successfully authenticated as: {me.display_name} ({me.user_principal_name})")
|
||||
return True
|
||||
|
||||
return False
|
||||
else:
|
||||
print("Skipping Graph validation (using cached authentication)")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Graph authentication failed: {str(e)}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user