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).
Shared harness contract
Section titled “Shared harness contract”Every language suite follows the same lifecycle, driven by e2e/run.py:
- 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. - Poll
/healthuntil ready; exportEMU_ORIGIN,EMU_TENANT,EMU_CERT(path tocert.pem) to the suite. - Run the suite against the seeded apps/users (fixed GUIDs, docs/03).
- Tear down; non-zero exit fails the run.
Two knobs make custom authorities work in every Microsoft SDK:
| Concern | Setting |
|---|---|
| Instance discovery | Must 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 trust | Node 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 |
Language matrix
Section titled “Language matrix”| Language | SDK(s) | Flows covered | Interaction driver |
|---|---|---|---|
| TypeScript | @azure/msal-node | client credentials, auth code + PKCE, refresh, device code | cookie-jar HTTPS sequence against the sign-in/approval pages |
| TypeScript (Graph) | @microsoft/microsoft-graph-client + @azure/msal-node | stateful 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-browser | auth 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 authenticator | passkey register → assert on the emulator origin; SSO authorize; ID token amr:["fido"] | Playwright headless Chromium (e2e/passkey) |
| Fabric companion | fabric-emulator handshake tests (pinned) | Fabric-audience client_credentials; workspace-identity provision → mint → rename → deprovision → cascade delete | e2e/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 refusal | Playwright headless Chromium (e2e/implicit) |
| Azure CLI | az (packaged, unmodified) | az cloud register + service-principal login; get-access-token for Graph and https://management.azure.com | e2e/az-cli/run.py (CI az-cli-e2e) |
| Go | microsoft-authentication-library-for-go + azidentity | client credentials (both layers), device code | HTTP approval sequence |
| Python | msal (+ optional azure-identity) | client credentials, device code | HTTP approval sequence in a thread |
| C# / .NET | Microsoft.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.WsFederation | FederationMetadata + wa=wsignin1.0; SAML 2.0 wresult verified by unmodified middleware | cookie-jar HTTPS against the account picker (e2e/wsfed) |
| Java | com.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 winner | HTTP 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 query | local 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/Dart | Dart http (automated) + flutter_appauth (manual screen) | device code end-to-end on-device; auth code + PKCE manually | integration_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 assertsclient_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 —ClientSecretCredentialwithCloud.ActiveDirectoryAuthorityHostpointed 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 viainitiate_device_flow+acquire_token_by_device_flowwith the approval driven concurrently. The suite provisions its own venv. - .NET (
e2e/dotnet/):ConfidentialClientApplicationBuilderwith.WithAuthority(authority, validateAuthority: false).WithInstanceDiscovery(false)and anHttpClientthat trustsEMU_CERT(not every certificate), supplied viaWithHttpClientFactory. Asserts the app-only claim shape (aud,roles, noscp/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 withdotnet run. - .NET WS-Fed (
e2e/wsfed/): unmodifiedAddWsFederationagainst the existing FederationMetadata URL andWtrealm=api://tasks-api. Sibling ofe2e/saml, not an extension ofe2e/dotnet. Drives the account picker, then lets the middleware verify the SAML 2.0wresult.python3 e2e/run.py wsfed. - Java (
e2e/java/):ConfidentialClientApplication.builder(...) .authority(authority).validateAuthority(false).instanceDiscovery(false); the suite loadsEMU_CERTinto a fresh trust store and sets it as the defaultSSLContextso 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 Darthttp(device authorization → pending poll → approval pages → tokens → Graph/me) — device code needs no browser, so it is fully automatable. Theflutter_appauthAuthorization Code + PKCE flow opens an external system browser thatintegration_testcannot 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.2from the Android emulator,localhostfrom the iOS simulator — pass--dart-define=EMU_ORIGIN=...), the CI emulator runsTLS_ENABLED=false(Android manifest allows cleartext; iOS has the local-networking ATS exception). CI runners: Android emulator onubuntu-latest(KVM) — modern macOS runners are arm64 without nested virtualization and cannot boot it; iOS simulator onmacos-latest. Nightly + manual dispatch, not a PR gate.
Assertions common to every suite
Section titled “Assertions common to every suite”- Access/ID tokens verify against the live JWKS;
issequals the discovery issuer. - Claim shapes per docs/04 (
tid,oid,scp/roles, pairwisesub,ver: "2.0"). client_infopresent 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.