CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-msal

Microsoft Authentication Library for Python enabling OAuth2/OIDC authentication with Microsoft identity platform

Pending
Overview
Eval results
Files

common-auth-flows.mddocs/

Common Authentication Flows

MSAL Python provides shared authentication functionality across both public and confidential client applications. These common flows include silent token acquisition from cache, authorization code flow for web scenarios, username/password authentication, refresh token handling, and comprehensive account management.

Capabilities

Silent Token Acquisition

The primary method for acquiring tokens without user interaction by leveraging cached tokens and automatic refresh capabilities.

def acquire_token_silent(
    self,
    scopes: list,
    account,
    authority=None,
    force_refresh=False,
    claims_challenge=None,
    **kwargs
):
    """
    Acquire access token silently using cached refresh token.

    Parameters:
    - scopes: List of scopes to request
    - account: Account object from get_accounts() or cache
    - authority: Authority URL override  
    - force_refresh: Force token refresh even if cached token is valid
    - claims_challenge: Additional claims from resource provider

    Returns:
    Dictionary with 'access_token' on success, 'error' on failure
    """

def acquire_token_silent_with_error(
    self,
    scopes: list,
    account,
    authority=None,
    force_refresh=False,
    claims_challenge=None,
    **kwargs
):
    """
    Same as acquire_token_silent but returns detailed error information.

    Returns:
    Dictionary with detailed error information on failure
    """

Usage example:

import msal

app = msal.PublicClientApplication(
    client_id="your-client-id",
    authority="https://login.microsoftonline.com/common"
)

# Get cached accounts
accounts = app.get_accounts()

if accounts:
    # Try silent acquisition first
    result = app.acquire_token_silent(
        scopes=["User.Read", "Mail.Read"],
        account=accounts[0]
    )
    
    if "access_token" in result:
        print("Silent authentication successful!")
        access_token = result["access_token"]
    else:
        print(f"Silent authentication failed: {result.get('error')}")
        if result.get('error') == 'interaction_required':
            # Fall back to interactive authentication
            result = app.acquire_token_interactive(
                scopes=["User.Read", "Mail.Read"]
            )
else:
    print("No cached accounts found")
    # Perform initial interactive authentication
    result = app.acquire_token_interactive(scopes=["User.Read", "Mail.Read"])

# Force refresh example
result = app.acquire_token_silent(
    scopes=["User.Read"],
    account=accounts[0],
    force_refresh=True  # Always get fresh token
)

Authorization Code Flow

OAuth2 authorization code flow implementation with PKCE (Proof Key for Code Exchange) support for enhanced security.

def initiate_auth_code_flow(
    self,
    scopes: list,
    redirect_uri=None,
    state=None,
    prompt=None,
    login_hint=None,
    domain_hint=None,
    claims_challenge=None,
    max_age=None,
    **kwargs
):
    """
    Initiate authorization code flow.

    Parameters:
    - scopes: List of scopes to request
    - redirect_uri: Redirect URI registered in Azure portal
    - state: State parameter for CSRF protection
    - prompt: Prompt behavior (none, login, consent, select_account)
    - login_hint: Email to pre-populate sign-in form
    - domain_hint: Domain hint to skip domain selection
    - claims_challenge: Additional claims from resource provider
    - max_age: Maximum authentication age in seconds

    Returns:
    Dictionary containing auth_uri, state, code_verifier, and other flow data
    """

def acquire_token_by_auth_code_flow(
    self,
    auth_code_flow: dict,
    auth_response: dict,
    scopes=None,
    **kwargs
):
    """
    Complete authorization code flow.

    Parameters:
    - auth_code_flow: Flow state from initiate_auth_code_flow()
    - auth_response: Authorization response parameters from redirect
    - scopes: Optional scopes override

    Returns:
    Dictionary with 'access_token' on success, 'error' on failure
    """

def get_authorization_request_url(
    self,
    scopes: list,
    redirect_uri=None,
    state=None,
    prompt=None,
    login_hint=None,
    domain_hint=None,
    claims_challenge=None,
    max_age=None,
    **kwargs
):
    """
    Generate authorization URL (simpler alternative to initiate_auth_code_flow).

    Returns:
    Authorization URL string
    """

Usage example for web application:

import msal
from flask import Flask, request, redirect, session, url_for
import secrets

app_flask = Flask(__name__)
app_flask.secret_key = secrets.token_hex(16)

msal_app = msal.ConfidentialClientApplication(
    client_id="your-client-id",
    client_credential="your-client-secret",
    authority="https://login.microsoftonline.com/common"
)

@app_flask.route('/login')
def login():
    # Generate CSRF state
    state = secrets.token_urlsafe(32)
    session['state'] = state
    
    # Initiate auth code flow
    auth_flow = msal_app.initiate_auth_code_flow(
        scopes=["User.Read", "profile", "openid"],
        redirect_uri=url_for('auth_response', _external=True),
        state=state
    )
    
    # Store flow data in session
    session['auth_flow'] = auth_flow
    
    # Redirect to authorization URL
    return redirect(auth_flow['auth_uri'])

@app_flask.route('/auth-response')
def auth_response():
    # Verify state parameter
    if request.args.get('state') != session.get('state'):
        return "Invalid state parameter", 400
    
    # Get stored flow data
    auth_flow = session.get('auth_flow', {})
    
    # Handle authorization errors
    if 'error' in request.args:
        error = request.args.get('error')
        error_description = request.args.get('error_description', '')
        return f"Authorization error: {error} - {error_description}", 400
    
    # Complete the flow
    result = msal_app.acquire_token_by_auth_code_flow(
        auth_code_flow=auth_flow,
        auth_response=request.args
    )
    
    if "access_token" in result:
        # Store tokens in session (consider more secure storage in production)
        session['tokens'] = {
            'access_token': result['access_token'],
            'expires_in': result.get('expires_in'),
            'refresh_token': result.get('refresh_token'),
            'id_token_claims': result.get('id_token_claims', {})
        }
        
        username = result.get('id_token_claims', {}).get('preferred_username', 'Unknown')
        return f"Login successful! Welcome, {username}"
    else:
        return f"Login failed: {result.get('error_description')}", 400

@app_flask.route('/logout')
def logout():
    # Clear session
    session.clear()
    
    # Optional: redirect to logout URL
    logout_url = f"https://login.microsoftonline.com/common/oauth2/v2.0/logout"
    return redirect(logout_url)

Username/Password Authentication

Resource Owner Password Credentials (ROPC) flow for scenarios where interactive authentication is not possible. Note: This flow is not recommended for most scenarios due to security limitations.

def acquire_token_by_username_password(
    self,
    username: str,
    password: str,
    scopes: list,
    claims_challenge=None,
    auth_scheme=None,
    **kwargs
):
    """
    Acquire token using username and password.

    Parameters:
    - username: User's email address or UPN
    - password: User's password
    - scopes: List of scopes to request
    - claims_challenge: Additional claims from resource provider
    - auth_scheme: Authentication scheme (e.g., PopAuthScheme for PoP tokens)

    Returns:
    Dictionary with 'access_token' on success, 'error' on failure
    """

Usage example:

import msal
import getpass

app = msal.PublicClientApplication(
    client_id="your-client-id",
    authority="https://login.microsoftonline.com/your-tenant-id"
)

# Get credentials (use secure input for password)
username = input("Username: ")
password = getpass.getpass("Password: ")

result = app.acquire_token_by_username_password(
    username=username,
    password=password,
    scopes=["User.Read"]
)

if "access_token" in result:
    print("Username/password authentication successful!")
    access_token = result["access_token"]
else:
    print(f"Authentication failed: {result.get('error_description')}")
    if result.get('error') == 'invalid_grant':
        print("Invalid username or password")
    elif result.get('error') == 'interaction_required':
        print("Multi-factor authentication required - use interactive flow")

Refresh Token Handling

Direct refresh token usage for scenarios where you have a refresh token from external sources.

def acquire_token_by_refresh_token(
    self,
    refresh_token: str,
    scopes: list,
    **kwargs
):
    """
    Acquire token using refresh token.

    Parameters:
    - refresh_token: Refresh token string
    - scopes: List of scopes to request

    Returns:
    Dictionary with 'access_token' on success, 'error' on failure
    """

Usage example:

import msal

app = msal.PublicClientApplication(
    client_id="your-client-id",
    authority="https://login.microsoftonline.com/common"
)

# Use refresh token obtained from elsewhere
refresh_token = "0.AAAA..."  # Your refresh token

result = app.acquire_token_by_refresh_token(
    refresh_token=refresh_token,
    scopes=["User.Read", "Mail.Read"]
)

if "access_token" in result:
    print("Refresh token authentication successful!")
    access_token = result["access_token"]
    new_refresh_token = result.get("refresh_token")  # May get new refresh token
else:
    print(f"Refresh failed: {result.get('error_description')}")
    if result.get('error') == 'invalid_grant':
        print("Refresh token expired or invalid")

Account Management

Comprehensive account management including listing cached accounts and removing accounts from cache.

def get_accounts(self, username=None) -> list:
    """
    Get list of accounts in cache.

    Parameters:
    - username: Filter accounts by username

    Returns:
    List of account dictionaries
    """

def remove_account(self, account):
    """
    Remove account and associated tokens from cache.

    Parameters:
    - account: Account object from get_accounts()
    """

def is_pop_supported(self) -> bool:
    """
    Check if Proof-of-Possession tokens are supported.

    Returns:
    True if PoP tokens are supported (requires broker)
    """

Usage example:

import msal

app = msal.PublicClientApplication(
    client_id="your-client-id",
    authority="https://login.microsoftonline.com/common"
)

# List all cached accounts
accounts = app.get_accounts()
print(f"Found {len(accounts)} cached accounts:")

for i, account in enumerate(accounts):
    print(f"{i+1}. {account.get('username')}")
    print(f"   Environment: {account.get('environment')}")
    print(f"   Home Account ID: {account.get('home_account_id')}")
    print(f"   Authority Type: {account.get('authority_type')}")

# Filter accounts by username
specific_accounts = app.get_accounts(username="user@example.com")
print(f"Accounts for user@example.com: {len(specific_accounts)}")

# Remove specific account
if accounts:
    print(f"Removing account: {accounts[0].get('username')}")
    app.remove_account(accounts[0])
    
    # Verify removal
    remaining_accounts = app.get_accounts()
    print(f"Remaining accounts: {len(remaining_accounts)}")

# Account-specific silent authentication
for account in app.get_accounts():
    result = app.acquire_token_silent(
        scopes=["User.Read"],
        account=account
    )
    
    if "access_token" in result:
        print(f"Successfully acquired token for {account.get('username')}")
    else:
        print(f"Failed to acquire token for {account.get('username')}: {result.get('error')}")

Legacy Authorization Code Method

Simplified authorization code method (legacy - use auth code flow methods for new applications):

def acquire_token_by_authorization_code(
    self,
    code: str,
    redirect_uri=None,
    scopes=None,
    **kwargs
):
    """
    Acquire token using authorization code (legacy method).

    Parameters:
    - code: Authorization code from redirect
    - redirect_uri: Redirect URI used in authorization request
    - scopes: List of scopes

    Returns:
    Dictionary with 'access_token' on success, 'error' on failure
    """

Error Handling

Common error scenarios and handling patterns for shared authentication flows:

# Silent authentication error handling
result = app.acquire_token_silent(scopes=["User.Read"], account=account)

if "access_token" not in result:
    error = result.get("error")
    
    if error == "interaction_required":
        # User interaction needed (MFA, consent, etc.)
        print("Interactive authentication required")
        result = app.acquire_token_interactive(scopes=["User.Read"])
    elif error == "invalid_grant":
        # Refresh token expired
        print("Refresh token expired, removing account")
        app.remove_account(account)
    elif error == "token_expired":
        # Access token expired (should auto-refresh)
        print("Token expired, trying force refresh")
        result = app.acquire_token_silent(
            scopes=["User.Read"], 
            account=account, 
            force_refresh=True
        )
    else:
        print(f"Silent authentication failed: {result.get('error_description')}")

# Authorization code flow error handling
auth_response = request.args  # From web framework

if 'error' in auth_response:
    error = auth_response.get('error')
    if error == 'access_denied':
        # User cancelled or denied consent
        print("User denied consent")
    elif error == 'invalid_scope':
        # Invalid scope requested
        print("Invalid scope in request")
    else:
        print(f"Authorization error: {auth_response.get('error_description')}")
else:
    # Complete the flow
    result = app.acquire_token_by_auth_code_flow(auth_flow, auth_response)

# Username/password error handling
result = app.acquire_token_by_username_password(username, password, scopes)

if "access_token" not in result:
    error = result.get("error")
    
    if error == "invalid_grant":
        # Wrong credentials
        print("Invalid username or password")
    elif error == "interaction_required":
        # MFA or other interaction needed
        print("Multi-factor authentication required")
    elif error == "invalid_client":
        # ROPC not enabled for application
        print("Username/password flow not enabled for this application")

Install with Tessl CLI

npx tessl i tessl/pypi-msal

docs

common-auth-flows.md

confidential-client.md

index.md

managed-identity.md

public-client.md

security-advanced.md

token-cache.md

tile.json