Skip to main content
Multi-Cloud Manager uses Microsoft Authentication Library (MSAL) for Python to authenticate users and access Azure resources through the Azure Management API.

Authentication Flow

The Azure authentication module implements the OAuth 2.0 authorization code flow:

Environment Variables

Configure these environment variables in your .env file:
# Azure OAuth Configuration
AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret

# Application Configuration
APP_BASE_URL=http://localhost:5000

Variable Descriptions

VariableDescriptionExample
AZURE_TENANT_IDYour Azure AD tenant IDaaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
AZURE_CLIENT_IDApplication (client) ID from app registration11111111-2222-3333-4444-555555555555
AZURE_CLIENT_SECRETClient secret value (not the secret ID)abc123~xyz...
APP_BASE_URLBase URL where your backend is hostedhttps://api.yourdomain.com
Keep your AZURE_CLIENT_SECRET secure and never commit it to version control. Rotate secrets regularly.

Azure App Registration

1

Create App Registration

Navigate to Azure Portal → Azure Active Directory → App registrations → New registration
  • Name: Multi-Cloud Manager
  • Supported account types: Single tenant or multi-tenant based on your needs
  • Redirect URI: http://localhost:5000/getAToken (for development)
2

Create Client Secret

In your app registration:
  • Go to Certificates & secrets → New client secret
  • Add a description and set expiration
  • Copy the secret value (not the ID) immediately
3

Configure API Permissions

Add the following API permission:
  • API: Azure Service Management
  • Permission: user_impersonation (delegated)
  • Grant admin consent if required
4

Note Configuration Values

Copy these values from the Overview page:
  • Application (client) ID → AZURE_CLIENT_ID
  • Directory (tenant) ID → AZURE_TENANT_ID

Code Implementation

The Azure authentication module is located in backend/auth/azure_auth.py:

MSAL Configuration

import msal
import os
from azure.identity import ClientSecretCredential
from azure.mgmt.subscription import SubscriptionClient

AUTHORITY = f"https://login.microsoftonline.com/{os.getenv('AZURE_TENANT_ID')}"
SCOPE = ["https://management.azure.com/.default"]
CLIENT_ID = os.getenv("AZURE_CLIENT_ID")
CLIENT_SECRET = os.getenv("AZURE_CLIENT_SECRET")
APP_BASE_URL = os.getenv("APP_BASE_URL", "http://localhost:5000")
REDIRECT_PATH = "/getAToken"

def build_msal_app():
    return msal.ConfidentialClientApplication(
        CLIENT_ID, 
        authority=AUTHORITY, 
        client_credential=CLIENT_SECRET
    )

OAuth Scopes

The application requests the following scope:
SCOPE = ["https://management.azure.com/.default"]
This scope grants access to Azure Resource Manager APIs, allowing the application to:
  • List subscriptions
  • Query resource groups
  • Manage virtual machines and other resources

API Endpoints

Login Endpoint

Route: /api/login/azure Method: GET Description: Initiates the Azure OAuth flow by redirecting to Microsoft’s authorization endpoint.
@azure_auth.route("/api/login/azure")
def login():
    url = build_msal_app().get_authorization_request_url(
        scopes=SCOPE, 
        redirect_uri=f"{APP_BASE_URL}{REDIRECT_PATH}",
    )
    return redirect(url)
Usage:
curl -L http://localhost:5000/api/login/azure

Callback Endpoint

Route: /getAToken Method: GET Description: Handles the OAuth callback, exchanges the authorization code for tokens, and stores account information in the session.
@azure_auth.route(REDIRECT_PATH)
def authorized():
    code = request.args.get("code")
    if not code:
        return jsonify({"error": "Brak kodu"}), 401

    result = build_msal_app().acquire_token_by_authorization_code(
        code, scopes=SCOPE, redirect_uri=f"{APP_BASE_URL}{REDIRECT_PATH}"
    )
    
    if "id_token_claims" in result:
        # Store user and token in session
        session["user"] = result["id_token_claims"]
        session["access_token"] = result.get("access_token")
        
        # Fetch Azure subscriptions
        cred = ClientSecretCredential(
            tenant_id=os.getenv("AZURE_TENANT_ID"),
            client_id=os.getenv("AZURE_CLIENT_ID"),
            client_secret=os.getenv("AZURE_CLIENT_SECRET")
        )
        
        sub_client = SubscriptionClient(cred)
        subs = [s.subscription_id for s in sub_client.subscriptions.list()]
        
        # Store account in session
        azure_account = {
            "provider": "azure",
            "tenantId": os.getenv("AZURE_TENANT_ID"),
            "displayName": session["user"].get("name") or "Azure account",
            "subscriptions": subs
        }
        
        # Update or add account
        accounts = session.get("accounts", [])
        # Remove existing Azure account with same tenantId
        accounts = [acc for acc in accounts 
                    if not (acc.get("provider") == "azure" and 
                           acc.get("tenantId") == azure_account["tenantId"])]
        accounts.append(azure_account)
        session["accounts"] = accounts
        
    return redirect("http://localhost:3000/dashboard")

Session Data Structure

After successful authentication, the session contains:
session = {
    "user": {
        "name": "John Doe",
        "preferred_username": "[email protected]",
        "oid": "user-object-id",
        # ... other ID token claims
    },
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
    "accounts": [
        {
            "provider": "azure",
            "tenantId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
            "displayName": "John Doe",
            "subscriptions": [
                "sub-id-1",
                "sub-id-2"
            ]
        }
    ]
}

Subscription Discovery

After authentication, the module automatically discovers Azure subscriptions:
from azure.identity import ClientSecretCredential
from azure.mgmt.subscription import SubscriptionClient

cred = ClientSecretCredential(
    tenant_id=tenant_id,
    client_id=client_id,
    client_secret=client_secret
)

sub_client = SubscriptionClient(cred)
try:
    subs = [s.subscription_id for s in sub_client.subscriptions.list()]
except Exception as e:
    print(f"Error listing subscriptions: {e}")
    subs = []
If subscription listing fails, the account is still added with an empty subscription list.

Account Deduplication

The system prevents duplicate Azure accounts in the session by comparing tenantId:
# Filter out existing Azure accounts with the same tenantId
filtered_accounts = [
    acc for acc in current_accounts 
    if not (acc.get("provider") == "azure" and acc.get("tenantId") == tenant_id)
]
filtered_accounts.append(azure_account)
session["accounts"] = filtered_accounts

Troubleshooting

Error: “Brak kodu”

Cause: No authorization code in callback URL Solution: Check that the redirect URI in Azure App Registration matches {APP_BASE_URL}/getAToken

Error: Failed to List Subscriptions

Cause: Insufficient permissions or invalid credentials Solution:
  • Verify the client secret is correct and not expired
  • Ensure the user has permissions to view subscriptions
  • Check that API permissions include Azure Service Management

Error: Invalid Client

Cause: Client ID or client secret mismatch Solution: Double-check AZURE_CLIENT_ID and AZURE_CLIENT_SECRET environment variables

Security Best Practices

  1. Always use HTTPS in production for APP_BASE_URL
  2. Configure Flask session with a strong SECRET_KEY
  3. Rotate client secrets before expiration
  4. Use Azure Key Vault for secret management in production
  5. Enable Conditional Access policies for additional security

Next Steps

Build docs developers (and LLMs) love