Skip to content

E2E testing with real Entra SDKs

The e2e suites prove that unmodified Microsoft client libraries complete real flows against the emulator. Reference: entra-docs/docs/identity-platform/ (msal-client-application-configuration.md, msal-authentication-flows.md).

Every language suite follows the same lifecycle, driven by e2e/run.py:

  1. Build the emulator; start it on a fixed port with an ephemeral DB_PATH, ORIGIN_MODE=compat (CI has no hosts entries), and TLS enabled.
  2. Poll /health until ready; export EMU_ORIGIN, EMU_TENANT, EMU_CERT (path to cert.pem) to the suite.
  3. Run the suite against the seeded apps/users (fixed GUIDs, docs/03).
  4. Tear down; non-zero exit fails the run.

Two knobs make custom authorities work in every Microsoft SDK:

ConcernSetting
Instance discoveryMust be disabled (the emulator is not in Microsoft’s cloud metadata): msal-js knownAuthorities; MSAL Go WithInstanceDiscovery(false); MSAL Python instance_discovery=False; MSAL.NET .WithInstanceDiscovery(false); MSAL4J .instanceDiscovery(false); azure-identity DisableInstanceDiscovery
TLS trustNode NODE_EXTRA_CA_CERTS; Go custom http.Client with the cert in RootCAs; Python verify=<cert> (msal) / connection_verify (azure-identity); .NET cert-trusting HttpClient via WithHttpClientFactory; Java: load the cert into a trust store and set the default SSLContext; browsers via Playwright ignoreHTTPSErrors
LanguageSDK(s)Flows coveredInteraction driver
TypeScript@azure/msal-nodeclient credentials, auth code + PKCE, refresh, device codecookie-jar HTTPS sequence against the sign-in/approval pages
TypeScript (Graph)@microsoft/microsoft-graph-client + @azure/msal-nodestateful directory: user/group/app writes incl. $ref membership, OData $select/$top/$filter/$skiptoken, role assignment + custom role definitions, administrative units, custom security attributes, consent grants, federatedIdentityCredentials, token lifetime policies, B2B invitation + redemption, password reset proved by ROPC sign-in, auditLogs/signIns + directoryAudits, soft-delete → recycle bin → restore → purge (docs/19)Graph SDK request pipeline against /graph
TypeScript (browser)@azure/msal-browserauth code + PKCE, cached account, RP-initiated logout, front-channel logout (the OP’s hidden iframe is really fetched), post-logout redirect validation, ID token amr:["pwd"]Playwright headless Chromium (opt-in, heavier)
Chromium (WebAuthn)navigator.credentials + CDP virtual authenticatorpasskey register → assert on the emulator origin; SSO authorize; ID token amr:["fido"]Playwright headless Chromium (e2e/passkey)
Fabric companionfabric-emulator handshake tests (pinned)Fabric-audience client_credentials; workspace-identity provision → mint → rename → deprovision → cascade deletee2e/fabric/run.py (CI fabric-e2e)
Chromium (implicit / hybrid)raw /authorize (not msal-browser)response_type=id_token and code id_token front-channel redirects; hybrid code exchange; query-mode refusalPlaywright headless Chromium (e2e/implicit)
Azure CLIaz (packaged, unmodified)az cloud register + service-principal login; get-access-token for Graph and https://management.azure.come2e/az-cli/run.py (CI az-cli-e2e)
Gomicrosoft-authentication-library-for-go + azidentityclient credentials (both layers), device codeHTTP approval sequence
Pythonmsal (+ optional azure-identity)client credentials, device codeHTTP approval sequence in a thread
C# / .NETMicrosoft.Identity.Client (MSAL.NET) + Wilson (Microsoft.IdentityModel.Protocols.OpenIdConnect)client credentials (+ token-cache hit); JwtBearer-stack validation of the resulting JWT (discovery, JWKS, tamper, key rotation)— (no interactive flow)
C# / .NET (WS-Fed)Microsoft.AspNetCore.Authentication.WsFederationFederationMetadata + wa=wsignin1.0; SAML 2.0 wresult verified by unmodified middlewarecookie-jar HTTPS against the account picker (e2e/wsfed)
Javacom.microsoft.azure:msal4j (MSAL4J)client credentials— (no interactive flow)
Python (concurrency)msal + raw replays (e2e/concurrency)second use of every credential: spent device code, reused refresh token and its revoked successor, plus 8-way races on both — exactly one winnerHTTP approval sequence, then a threading.Barrier so the racers arrive together
Python (JAR)PyJWT + cryptography (e2e/jar)RFC 9101 request object by reference: SSRF guard (untrusted origin refused, 302 not followed) and the positive case where the object’s parameters override the querylocal HTTP servers on trusted and untrusted origins
Python (cloud metadata)msal (e2e/cloud-metadata)the discovery document’s cloud coordinates are followed, not just read: the advertised graph host answers with this emulator’s seeded directory, and nothing in the document names an Azure host
Flutter/DartDart http (automated) + flutter_appauth (manual screen)device code end-to-end on-device; auth code + PKCE manuallyintegration_test on Android emulator / iOS simulator — nightly, not PR gate

Notes per language:

  • TypeScript is the reference suite. The msal-node auth-code test uses getAuthCodeUrl → drive the account picker over HTTPS with a cookie jar → acquireTokenByCode, then asserts client_info-derived account identity and JWKS verification.
  • Go tests two layers deliberately: raw MSAL Go (what the emulator’s protocol surface promises) and azidentity (what real Go services use — ClientSecretCredential with Cloud.ActiveDirectoryAuthorityHost pointed at the emulator). The roadmap’s embeddable library will wrap this harness for downstream consumers.
  • Python: ConfidentialClientApplication(..., instance_discovery=False, verify=EMU_CERT); device flow via initiate_device_flow + acquire_token_by_device_flow with the approval driven concurrently. The suite provisions its own venv.
  • .NET (e2e/dotnet/): ConfidentialClientApplicationBuilder with .WithAuthority(authority, validateAuthority: false).WithInstanceDiscovery(false) and an HttpClient that trusts EMU_CERT (not every certificate), supplied via WithHttpClientFactory. Asserts the app-only claim shape (aud, roles, no scp/oid) and that a second call is served from MSAL’s token cache. Wilson then fetches OIDC discovery + JWKS and validates the JWT the same way JwtBearer does — including a tampered-signature refusal and a rotate-with-grace check. Built and run with dotnet run.
  • .NET WS-Fed (e2e/wsfed/): unmodified AddWsFederation against the existing FederationMetadata URL and Wtrealm=api://tasks-api. Sibling of e2e/saml, not an extension of e2e/dotnet. Drives the account picker, then lets the middleware verify the SAML 2.0 wresult. python3 e2e/run.py wsfed.
  • Java (e2e/java/): ConfidentialClientApplication.builder(...) .authority(authority).validateAuthority(false).instanceDiscovery(false); the suite loads EMU_CERT into a fresh trust store and sets it as the default SSLContext so msal4j’s HTTP client accepts the TLS connection. Built and run with Maven (mvn compile exec:java).
  • Flutter (e2e/flutter/, run by .github/workflows/flutter-e2e.yml): no official MSAL exists for Dart. The automated on-device test drives the device-code flow end-to-end with Dart http (device authorization → pending poll → approval pages → tokens → Graph /me) — device code needs no browser, so it is fully automatable. The flutter_appauth Authorization Code + PKCE flow opens an external system browser that integration_test cannot drive; it ships as a manual screen in the same app. Device-emulator specifics: the authority is the address the device sees (10.0.2.2 from the Android emulator, localhost from the iOS simulator — pass --dart-define=EMU_ORIGIN=...), the CI emulator runs TLS_ENABLED=false (Android manifest allows cleartext; iOS has the local-networking ATS exception). CI runners: Android emulator on ubuntu-latest (KVM) — modern macOS runners are arm64 without nested virtualization and cannot boot it; iOS simulator on macos-latest. Nightly + manual dispatch, not a PR gate.
  • Access/ID tokens verify against the live JWKS; iss equals the discovery issuer.
  • Claim shapes per docs/04 (tid, oid, scp/roles, pairwise sub, ver: "2.0").
  • client_info present on delegated responses; absent on client credentials.
  • Negative paths: wrong secret → invalid_client; replayed code / reused refresh token → invalid_grant; device poll before approval → authorization_pending.