协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
ms-office-enterprise-sso-python【开发与工程】Use when authenticating a Python client to Microsoft Entra ID / Azure AD / on-prem AD FS — covering the four enterprise auth topologies (Windows brokered interactive via msal[broker]/pymsalruntime, Linux brokered interactive via azure-identity-broker on Intune-managed Linux, headless / daemon with client credentials + certs + Managed Identity + Workl…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Companion skill to ms-office-python (parent). For other areas see: ms-office-excel-python, ms-office-word-python, ms-office-powerpoint-python, ms-office-graph-python, ms-office-security-python.
This is the family's KEYSTONE skill — the largest body of content, the most cited from siblings, and the one most likely to be wrong about a Microsoft detail. It covers the Python client side of enterprise SSO. For PowerShell-based infrastructure administration (AD FS server install, Entra ID tenant configuration, WAP deployment), see windows-sso. For server-side Python web apps consuming generic OAuth/OIDC/SAML against non-Microsoft IdPs, see python-auth-security.
Authenticating a Python client to Microsoft Entra ID involves picking the right flow for the right context, configuring it correctly, hardening the token cache, and validating every token claim on receipt. The wrong choice silently degrades security (device code on a managed device, ROPC anywhere, hardcoded secrets); the right choice composes cleanly with Conditional Access, MFA, device compliance, and tenant-wide audit logging.
This skill covers the four enterprise auth topologies — each is a peer, not a footnote — and the supporting concerns (token cache, JWT validation, SAML, Kerberos, national clouds, hardware keys, troubleshooting).
The opening section — answer this matrix BEFORE choosing a library.
| Device state | Recommended flow | Library |
|---|---|---|
| Win 11 AAD-joined | Broker interactive + silent | msal[broker] |
| Win 11 Hybrid-joined | Broker interactive + silent (with hedging — see §6.1) | msal[broker] |
| Win 11 Workplace-joined | Broker if it engages, else system browser fallback | msal[broker] |
| Win 11 unjoined | System browser (no broker) | msal |
| Linux + Intune-managed | Linux broker via DBus | azure-identity-broker (InteractiveBrowserBrokerCredential) |
| Linux + unmanaged | Device code OR cert-based client credentials | msal / azure-identity |
| Headless server | Client credentials with certificate | azure-identity (CertificateCredential) or msal (ConfidentialClientApplication) |
| Container in Azure | Managed Identity OR Workload Identity Federation | azure-identity (ManagedIdentityCredential / WorkloadIdentityCredential) |
| Container elsewhere (AWS / GCP / on-prem) | Workload Identity Federation OR cert from vault | azure-identity (WorkloadIdentityCredential / CertificateCredential) |
| CI (GitHub Actions / GitLab) | OIDC Federated Credential | azure-identity (WorkloadIdentityCredential using the runner's OIDC token) |
| On-prem service (Kerberos realm) | Kerberos / SPNEGO | requests-gssapi (Linux/macOS) / requests-negotiate-sspi (Windows) / pyspnego (unified) |
| Disconnected / air-gapped service | Cert-based client credentials with cert pre-staged | msal / azure-identity |
This matrix decides everything else. Mismatched flow ↔ device state = Conditional Access failures (the most common production incident this family sees).
dsregcmd /status matrix (Windows)When the recommended flow on Windows isn't behaving as expected, the first diagnostic is dsregcmd /status on the client. Five rows you'll see in real environments:
| Row | What to look for | Implication |
|---|---|---|
| AAD-joined | AzureAdJoined: YES, EnterpriseJoined: NO, DomainJoined: NO | Cloud-only managed device; broker preferred |
| Hybrid-joined | AzureAdJoined: YES, EnterpriseJoined: YES, DomainJoined: YES | Both on-prem AD and Entra ID; broker works; PRT issued |
| Workplace-joined | WorkplaceJoined: YES, AzureAdJoined: NO, DomainJoined: NO | Personal device with work profile; broker engages when available, falls back to browser |
| AD-only (on-prem AD-joined) | DomainJoined: YES, AzureAdJoined: NO | No Entra ID identity on device; cannot use broker; use Kerberos for on-prem services, device-code or browser for Entra ID resources |
| Unjoined | All NO | Personal / unmanaged device; system browser only |
Other diagnostic fields worth checking:
WamDefaultSet: YES and WamDefaultGUID: { ... } — the Web Account Manager is wired up.AzureAdPrt: YES — Primary Refresh Token issued; required for SSO across Microsoft 365.KeySignTest: PASSED — the device's key for binding tokens is healthy.DeviceAuthStatus: SUCCESS — Entra ID accepts the device's certificate.If any of those report NO / FAILED, the broker path will misbehave; document the diagnostic in the troubleshooting flow.
| Library | Purpose | Status (2026-05) | OS support | When to use | When NOT to use |
|---|---|---|---|---|---|
msal | Microsoft Authentication Library — direct M365 / Graph auth | Active (preferred for M365 auth) | All | Default for any Python client → Microsoft IdP path | Generic non-Microsoft IdP (use authlib); Azure SDK auth abstraction (use azure-identity) |
msal[broker] / pymsalruntime | WAM broker bindings for Windows | Active | Windows | Conditional-Access compliance on managed Windows | Linux (separate broker package); Mac (no Win broker) |
msal-extensions | Encrypted token cache | Active | All | Persisting the MSAL token cache across runs without leaking tokens | When the cache is genuinely transient (single-shot CLI) |
azure-identity | High-level credential abstractions | Active | All | Azure SDK auth, Managed Identity, Workload Identity Federation, DefaultAzureCredential chains | Direct Graph auth where MSAL's contract is closer to your needs |
azure-identity-broker | InteractiveBrowserBrokerCredential (separate package) | Active | Windows + Linux (Intune-managed) | Brokered auth from azure-identity contexts; Linux broker via DBus | Mac (use msal[broker] equivalent path); unmanaged Linux (no broker) |
authlib | Generic OIDC/OAuth2 client | Active | All | Non-Microsoft IdPs (Okta, Auth0, Keycloak) | Microsoft IdPs — MSAL is better-fitting |
requests-oauthlib | Simpler OAuth2 for requests | Active | All | Lightweight non-Microsoft OAuth2 | Microsoft IdPs (MSAL is more correct) |
pyspnego | Modern unified SPNEGO/NTLM/Kerberos | Active (preferred) | All | Kerberos / SPNEGO against on-prem services | Cloud-only (no on-prem identity) |
requests-gssapi | Kerberos for requests on Linux/Mac | Active | Linux / macOS | Adding Kerberos to existing requests code on UNIX | Windows (use requests-negotiate-sspi) |
requests-negotiate-sspi | Windows Negotiate via SSPI | Active | Windows | Adding Kerberos to existing requests code on Windows | Linux / macOS |
requests-kerberos | (legacy unified Kerberos) | LEGACY (superseded) | All | NOT for new code | Migrate to pyspnego + requests-gssapi / requests-negotiate-sspi |
pysaml2 | SAML 2.0 SP / IdP | Active | All | Talking SAML to a legacy IdP | Modern OIDC paths exist (prefer those when offered) |
python3-saml | OneLogin's SAML toolkit | Active | All | SAML when pysaml2 doesn't fit | Same as pysaml2 |
pyjwt[crypto] | JWT signing/validation | Active | All | Validating Entra-issued access / id tokens | Generic JWS / JWE (use python-jose) |
python-jose[cryptography] | JWT / JWE / JWK | Active | All | When JWE is required (rare in Microsoft surface) | Standard JWT (pyjwt is enough) |
cryptography | Foundation crypto | Active | All | Everywhere via dependency chain | (always pinned, CVE-watch) |
fido2 | WebAuthn / FIDO2 client | Active | All | Hardware-key auth flows | Server-side FIDO2 validation (see python-auth-security) |
pyscard | PC/SC smart cards (CAC / PIV) | Active | All | Smartcard / CAC / PIV scenarios | Software-only auth |
python-pkcs11 | PKCS#11 token access | Active | All | HSM / HW token integration | Software keys |
keyring | Cross-platform OS keyring | Active | All | Single-secret retrieval from OS credential store | Full credential management (use msal-extensions for MSAL caches) |
adal | (predecessor to msal) | EOL | All | NOT for any new code; migrate immediately | All cases — replaced by msal |
Boundary clarifier (Codex H-29): MSAL is for direct M365 / Graph auth; azure-identity is for Azure SDK auth and high-level credential abstraction (which includes some Graph use via GraphServiceClient(credentials=...)). They overlap — pick MSAL when you want explicit token control, azure-identity when you want credential abstraction and Managed Identity / Workload Identity primitives.
sudo dnf install -y python3.12 python3-pip python3-devel gcc-c++ \
krb5-devel libxml2-devel libxslt-devel libsecret-devel libxmlsec1-devel cyrus-sasl-gssapi
python3 -m pip install --upgrade pip
python3 -m pip install msal msal-extensions azure-identity pyjwt cryptography defusedxml pyspnego
# Optional, per branch:
python3 -m pip install azure-identity-broker # Linux broker (Intune-managed)
python3 -m pip install requests-gssapi # Kerberos on requests
python3 -m pip install pysaml2 python3-saml # SAML
python3 -m pip install fido2 python-pkcs11 keyring # hardware keys
For Linux broker support (Intune-managed devices only):
# Add Microsoft's Linux repo (Ubuntu/Debian have similar steps)
sudo dnf install -y microsoft-identity-broker # only available on supported distros
sudo apt update
sudo apt install -y python3.12 python3-pip python3-dev build-essential \
libkrb5-dev libxml2-dev libxslt1-dev libsecret-1-dev libxmlsec1-dev libsasl2-modules-gssapi-mit
python3 -m pip install --upgrade pip
python3 -m pip install msal msal-extensions azure-identity pyjwt cryptography defusedxml pyspnego
python3 -m pip install azure-identity-broker requests-gssapi
python3 -m pip install pysaml2 python3-saml fido2 python-pkcs11 keyring
# Optional, Linux broker:
# curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --dearmor -o /usr/share/keyrings/microsoft.gpg
# sudo apt install -y microsoft-identity-broker
winget install --id Python.Python.3.12 -e --silent
python -m pip install --upgrade pip
python -m pip install msal "msal[broker]" msal-extensions azure-identity azure-identity-broker `
pyjwt cryptography defusedxml requests-negotiate-sspi pyspnego pysaml2 python3-saml fido2 keyring
# Verify broker DLLs load:
python -c "import pymsalruntime; print(pymsalruntime.__file__)"
# CONFIDENCE: minimal viable pattern — production hardening notes in §7 Token cache hardening and the Security Hardening section below; full references/ guide planned (v1.1).
import msal
app = msal.PublicClientApplication(
client_id=CLIENT_ID,
authority=f"https://login.microsoftonline.com/{TENANT_ID}",
enable_broker_on_windows=True,
)
accounts = app.get_accounts()
if accounts:
result = app.acquire_token_silent(scopes=SCOPES, account=accounts[0])
else:
result = None
if not result:
result = app.acquire_token_interactive(scopes=SCOPES, parent_window_handle=msal.PublicClientApplication.CONSOLE_WINDOW_HANDLE)
# result["access_token"] is the bearer; result["id_token_claims"] has user info
Three things this pattern enforces: broker explicitly enabled (enable_broker_on_windows=True), silent attempted first (cache hit avoids broker UI), parent_window_handle passed (required for the broker pop-up to actually appear and not silently fail).
# CONFIDENCE: minimal viable pattern — production hardening notes in §7 Token cache hardening and the Security Hardening section below; full references/ guide planned (v1.1).
from azure.identity.broker import InteractiveBrowserBrokerCredential
cred = InteractiveBrowserBrokerCredential(
tenant_id=TENANT_ID, client_id=CLIENT_ID,
parent_window_handle=0, # 0 = unbound on Linux DBus; broker prompt appears in foreground
)
token = cred.get_token(*SCOPES)
# token.token is the bearer
Verify broker availability first: microsoft-identity-broker package installed and microsoft-identity-broker.service running. Without it the credential silently falls back to system browser.
# CONFIDENCE: minimal viable pattern — production hardening notes in §7 Token cache hardening and the Security Hardening section below; full references/ guide planned (v1.1).
import msal
app = msal.PublicClientApplication(client_id=CLIENT_ID,
authority=f"https://login.microsoftonline.com/{TENANT_ID}")
flow = app.initiate_device_flow(scopes=SCOPES)
print(flow["message"]) # prints "go to microsoft.com/devicelogin and enter CODE"
result = app.acquire_token_by_device_flow(flow)
DO NOT use this on a managed Windows endpoint. See HARD-RULE 1.
# CONFIDENCE: minimal viable pattern — production hardening notes in the Security Hardening section below (cert/key handling: vault-only); full references/ guide planned (v1.1).
import msal
with open(CERT_PATH, "rb") as fh:
cert_pem = fh.read()
with open(KEY_PATH, "rb") as fh:
key_pem = fh.read()
app = msal.ConfidentialClientApplication(
client_id=CLIENT_ID,
authority=f"https://login.microsoftonline.com/{TENANT_ID}",
client_credential={"private_key": key_pem, "thumbprint": CERT_THUMBPRINT, "public_certificate": cert_pem},
)
result = app.acquire_token_for_client(scopes=[f"{RESOURCE}/.default"])
Cert and key MUST come from a vault — not from disk paths checked into git. The path above is illustrative.
# CONFIDENCE: minimal viable pattern — production hardening notes in the Security Hardening section below; full references/ guide planned (v1.1).
from azure.identity import ManagedIdentityCredential
cred = ManagedIdentityCredential() # system-assigned MI
# cred = ManagedIdentityCredential(client_id=USER_ASSIGNED_MI_CLIENT_ID) # user-assigned MI
token = cred.get_token("https://graph.microsoft.com/.default")
No secret. No cert. The Azure platform issues the credential. Works on App Service, VM, Container Apps, AKS, Functions, Container Instances, Logic Apps.
# CONFIDENCE: minimal viable pattern — production hardening notes in the Security Hardening section below; full references/ guide planned (v1.1).
from azure.identity import WorkloadIdentityCredential
cred = WorkloadIdentityCredential(
tenant_id=TENANT_ID, client_id=CLIENT_ID,
token_file_path="/var/run/secrets/azure/tokens/azure-identity-token", # AKS injects this
)
token = cred.get_token("https://graph.microsoft.com/.default")
For GitHub Actions, the OIDC token comes from ACTIONS_ID_TOKEN_REQUEST_TOKEN / ACTIONS_ID_TOKEN_REQUEST_URL. For GitLab, the equivalent OIDC token. The federated credential is configured in the Entra ID app registration.
# CONFIDENCE: minimal viable pattern — production notes in §13 Troubleshooting and the Security Hardening section below; full references/ guide planned (v1.1).
import requests
from requests_gssapi import HTTPSPNEGOAuth # or requests_negotiate_sspi.HttpNegotiateAuth on Windows
session = requests.Session()
session.auth = HTTPSPNEGOAuth()
resp = session.get("https://onprem-service.example.com/api/data") # uses ambient Kerberos ticket
Requires a valid Kerberos ticket on the client (klist to verify). For service accounts, kinit from a keytab. pyspnego is the modern unified path if you need NTLM fallback or direct SPNEGO control.
msal.SerializableTokenCache() produces a JSON blob that holds access tokens, refresh tokens, and ID tokens. Persisting it to disk without encryption = a token leak vector. msal-extensions wraps it in OS-native encrypted storage:
| Platform | Backend |
|---|---|
| Windows | DPAPI (per-user encryption) |
| macOS | Keychain |
| Linux | libsecret (GNOME Keyring / KWallet) — falls back to file-based with explicit warning if unavailable |
Use msal_extensions.PersistedTokenCache with the appropriate PersistenceBuilder for the OS. Reading the cache without msal-extensions (e.g., from a different binary) is unsupported and breaks the security model.
What the broker gets you that the browser doesn't:
deviceid, is_compliant) — proves the device is enrolled and meets policy.Conditional Access policies evaluate these claims. A token without them is treated as a token from an unmanaged device — for sensitive resources, that's a block.
Common CA error codes :
| Code | Meaning |
|---|---|
AADSTS53003 | Access blocked by Conditional Access policies |
AADSTS50158 | External security challenge required (step-up MFA) |
AADSTS50076 | Multi-factor authentication required |
AADSTS50132 | Sign-in session became invalid (often device-state change) |
AADSTS65001 | Consent required (admin or user) |
AADSTS50105 | Entitled application not assigned to user (RBAC / SP role mismatch) |
AADSTS50034 | User account doesn't exist in directory (often typo'd UPN) |
AADSTS900971 | No reply address provided (app-registration redirect URI mismatch) |
AADSTS70011 | Invalid scope (the scope literal you requested isn't valid for the resource) |
AADSTS70008 | Refresh token expired |
When these surface in logs, the fix is configuration / policy — not retrying the same call.
For deep generic JWT validation patterns, see python-auth-security JWT Best Practices. Microsoft-specific notes:
tid (tenant ID) — multi-tenant apps MUST validate. Without it, a token from tenant B can replay against your tenant-A app.azp (authorized party) — for access tokens minted for a downstream API, identifies the client app that received the token. Validate when you delegate the call.acr / amr (authentication context references / methods) — tells you what MFA / hardware-key / passwordless was used. Step-up flows depend on this.oid (object ID) — the user's immutable ID in Entra ID. Prefer over email / upn for joins and audit (those can change).tid validation example:# CONFIDENCE: minimal viable pattern — claim checklist above + Security Hardening section below; deep JWT patterns in the python-auth-security skill; full references/ guide planned (v1.1).
import jwt
from jwt import PyJWKClient
jwks_client = PyJWKClient(f"https://login.microsoftonline.com/{TENANT_ID}/discovery/keys")
signing_key = jwks_client.get_signing_key_from_jwt(token).key
claims = jwt.decode(
token, signing_key,
algorithms=["RS256"],
audience=AUDIENCE,
issuer=f"https://login.microsoftonline.com/{TENANT_ID}/v2.0",
options={"require": ["exp", "iat", "nbf", "iss", "aud", "tid"]},
)
assert claims["tid"] == EXPECTED_TID, f"Token from wrong tenant: {claims['tid']}"
Algorithm-pin (algorithms=["RS256"]) is mandatory. JWKS caching via PyJWKClient is mandatory (don't fetch JWKS per token validation).
See python-auth-security for the generic SAML patterns. Microsoft-specific notes:
https://login.microsoftonline.com/{tenant_id}/federationmetadata/2007-06/federationmetadata.xml.https://adfs.example.com/FederationMetadata/2007-06/FederationMetadata.xml.pysaml2 and python3-saml are both viable. pysaml2 is more powerful but easier to misconfigure XML signature verification (a CVE class historically). python3-saml (OneLogin) has simpler defaults and is the safer first pick.InResponseTo matching your original AuthnRequest ID.Overview only. Deep coverage will live in python-auth-security if expanded:
fido2 library; CTAP2 protocol; YubiKey + Feitian + SoloKey support.pyscard for PC/SC; python-pkcs11 for PKCS#11 backend.ConfidentialClientApplication when the cert lives on a hardware token; load the private key through the PKCS#11 interface, sign assertions there.Entra ID supports passwordless / phishing-resistant FIDO2 since 2022; the client side is straightforward (browser handles WebAuthn). What Python clients do is rarely the FIDO2 protocol itself; usually they consume tokens that were minted via a FIDO2-attested sign-in.
Different authorities, different Graph endpoints:
| Cloud | Authority URL | Graph endpoint |
|---|---|---|
| Commercial | https://login.microsoftonline.com/{tenant} | https://graph.microsoft.com |
| GCC (Government Community Cloud) | Same as Commercial | Same as Commercial (GCC L4 uses Commercial endpoints) |
| GCC-H (High) | https://login.microsoftonline.us/{tenant} | https://graph.microsoft.us |
| DoD | https://login.microsoftonline.us/{tenant} | https://dod-graph.microsoft.us |
| Germany (closed 2021; historical mention) | n/a | n/a |
| China (21Vianet) | https://login.partner.microsoftonline.cn/{tenant} | https://microsoftgraph.chinacloudapi.cn |
The Python libraries support sovereign clouds — pass the appropriate authority URL to MSAL or use azure-identity with the sovereign endpoint constants. Some Graph endpoints / features ship later (or not at all) in sovereign clouds; verify per resource on Microsoft Learn before relying.
| Symptom | Likely cause | Fix |
|---|---|---|
AADSTS53003 block | Conditional Access policy requires compliant device / MFA | Switch to broker flow on managed Windows; comply with policy |
pymsalruntime DLL load failure on import | Missing VC++ runtime; ARM64 Python on x64 broker DLLs | Install VC++ 2015-2022 redist; verify python -c "import platform; print(platform.architecture())" matches broker |
3399614473 API contract violation | Mismatch between broker DLL version and msal version | Upgrade both msal[broker] and pymsalruntime together |
| Missing HWND on Windows broker prompt | parent_window_handle not passed | Pass msal.PublicClientApplication.CONSOLE_WINDOW_HANDLE for CLI, or actual HWND for GUI |
acquire_token_silent returns None unexpectedly | Cache expired, refresh token revoked, tenant policy revoked session | Fall through to interactive — silent + interactive together is the correct pattern |
DefaultAzureCredential succeeds in dev, fails in prod | Different credential source available in each env (cached CLI vs Managed Identity) | Use explicit ManagedIdentityCredential in prod, not DefaultAzureCredential |
AADSTS500011 (no service principal in tenant) | App registered in one tenant, consumed from another, no MT consent | Run admin consent in target tenant: https://login.microsoftonline.com/{tenant}/adminconsent?client_id=... |
See ms-office-security-python for the consolidated checklist. Area-specific items:
SerializableTokenCache to disk.Authorization: Bearer ... headers. Wrap the logger with a token-redacting filter before enabling debug.tid for multi-tenant apps (HARD-RULE 4). The default behaviour of pyjwt is to NOT require tid — you must request it explicitly via options["require"].InResponseTo; validate Issuer; never skip signature verification.oid), tenant (tid), scopes requested, and outcome. The audit trail is what differentiates a forensic story from a vague "we got hacked."msal, azure-identity, cryptography, pyjwt, lxml versions explicitly. CVE-watch via dep-currency-check.pyspnego over requests-kerberos; modern, unified, no gssapi-vs-pykerberos confusion.msal[broker] + PublicClientApplication(enable_broker_on_windows=True)azure-identity-broker + InteractiveBrowserBrokerCredentialazure-identity + ManagedIdentityCredentialmsal.ConfidentialClientApplication OR azure-identity + CertificateCredentialazure-identity + WorkloadIdentityCredential (Federated Credential)pyspnego or requests-gssapipyjwt + PyJWKClient with explicit algorithms, audience, issuer, and options["require"]python3-saml first; pysaml2 if you need more controlmsal[broker] extras require the pymsalruntime package which ships compiled DLLs. ARM64 Python on x64 broker DLLs (or vice-versa) fails at import. Match architectures.azure-identity-broker is a SEPARATE package from azure-identity (not an extra). It must be pip install azure-identity-broker explicitly.microsoft-identity-broker from Microsoft's repo AND an Intune-enrolled device. Without enrolment, the credential silently falls back to system browser.DefaultAzureCredential chains many sources and is convenient but produces wildly different behaviour across environments. Prefer explicit credentials in production.from adal import ... is a finding — see ms-office-security-python rule MSOSEC-A001.dsregcmd /status requires admin context for some fields. The non-admin fields are usually enough to diagnose; document the admin path in troubleshooting.msal, azure-identity, azure-identity-broker, pymsalruntime, pyjwt, cryptography, pyspnego, pysaml2, python3-saml.cryptography, pyjwt, lxml (XXE class), pysaml2 (signature-bypass class).| Need | Skill |
|---|---|
| Generic Python web-app OAuth/OIDC/SAML/JWT (non-Microsoft IdPs) | python-auth-security |
| PowerShell-based AD FS / Entra ID infrastructure administration | windows-sso |
| Linux ⇄ AD via Centrify | linux-centrify |
| Calling Graph after acquiring the token | ms-office-graph-python |
| Hardening / validator / checklist | ms-office-security-python |
| Dependency CVE scanning of the auth libs | dep-currency-check |
| Active Directory infrastructure side | windows-ad-admin |