Microsoft Authentication Library for Python enabling OAuth2/OIDC authentication with Microsoft identity platform
—
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.
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
)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)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")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")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')}")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
"""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