How the emulator’s surface maps to real Entra ID (as documented at
learn.microsoft.com/entra), and —
the point of this table — whether real work happens or just the API shape.
The design bet is that the durable, testable surface is protocol + real
cryptography + directory state, and those are done for real: real RS256 JWTs
that third-party validators accept, every OAuth2 grant a real MSAL speaks, real
WebAuthn ceremonies, a real SQLite directory. What is deliberately left out is
the policy engine — Conditional Access, MFA, Identity Protection — which is
what would turn a dev-loop emulator into an IdP.
“Real via our own wire-protocol implementation.” A row is 🟢 Real not
only when real cryptography does the work, but also when the emulator itself
implements Entra’s wire protocol and the logic behind it — so a real,
unmodified client (MSAL in five languages, the Graph SDK, a SCIM connector)
gets byte- and behaviour-identical responses.
Optional claims + group overage (_claim_names / _claim_sources)
Real Entra overage payload above the limit; protocol claims non-overridable
🟢 Real
Token signing algorithm
RS256 only — which is exactly what real Entra v2.0 advertises (id_token_signing_alg_values_supported: ["RS256"], captured in e2e/golden/). ES256/PS256 are absent from Entra too, so there is no gap to close: adding them would diverge, not converge
Device code (spec form and the bare device_code msal-node sends)
Real, with an atomic approve→mint step that closes the double-mint window
🟢 Real
private_key_jwt client assertion
Real: assertion verified against the app’s registered certificate, and now advertised in token_endpoint_auth_methods_supported so a spec-driven client actually attempts it
🟢 Real
RP-initiated logout (end_session_endpoint)
Real: clears the SSO session and honours a validatedpost_logout_redirect_uri + state; advertised via http_logout_supported
🟢 Real
Front-channel logout (OP calls each RP’s frontchannel_logout_uri)
Real: apps register a logout URI, the emulator records which apps each SSO session signed into, and logout renders one hidden iframe per signed-into RP carrying iss and sid. Apps the session never used are deliberately not notified. Now advertised, because it now happens
🟢 Real
Implicit / hybrid flow
Real: response_type=id_token and code id_token mint a genuine signed ID token at the authorize endpoint, delivered by fragment or form_post with the nonce echoed. OIDC’s rules are enforced — a nonce is required, response_mode=query is refused for an id_token, and PKCE is demanded only when a code is actually issued. id_token token is not implemented and so is not advertised
🟢 Real
mTLS / PoP / certificate-bound tokens
—
🔴 Not implemented
JAR by reference (request_uri, RFC 9101)
Real: the signed request object is fetched, verified against the app’s registered keys, and its parameters override the query — and the fetch is SSRF-guarded, reaching only origins the tenant already trusted as this app’s redirect URIs, with no redirects followed and the body size- and time-capped
🟢 Real
Inline request parameter
Refused — and not an Entra feature either: the real discovery document leaves request_parameter_supported absent, which per OIDC means false. Implementing it would diverge, not converge
🟢 Real
PAR (pushed authorization requests)
Not implemented — and not an Entra feature either: the real discovery document advertises no pushed_authorization_request_endpoint, so this is parity, not a gap
Real gate behind GRAPH_PERMISSIONS: delegated calls need the scope in scp, app-only calls the role in roles, Directory.* acts as the superset, denials are 403 Authorization_RequestDenied. Off by default — the emulator has always accepted any valid Graph-audience token, so enabling it is opt-in
🟢 Real
Separate servicePrincipal store
An app registration is its own SP; object id and appId are conflated
🟡 Emulated
Custom role definitions
Real CRUD over roleManagement/directory/roleDefinitions: tenant-authored roles list beside the built-ins, are assignable, and deleting one cascades to its assignments. Built-ins are protected from modification, and custom roles are excluded from wids — real Entra emits built-in role template GUIDs there only
🟢 Real
Administrative units
Real CRUD over directory/administrativeUnits plus membership of both users and groups (each returned with its own @odata.type), Public/HiddenMembership visibility, a dangling member refused, and FK-cascade so deleting a unit takes its memberships with it
🟢 Real
Custom security attributes
Real: attribute sets and String/Integer/Boolean definitions (id is Entra’s {set}_{name} composite), assigned onto users with the declared type enforced — an Integer attribute refuses a string and a scalar refuses a collection slot. Returned only on explicit $select, exactly as Graph does
🟢 Real
Graph beta endpoint
v1.0 only
🔴 Not implemented
Sign-in logs (Graph auditLogs/signIns)
Real: served over the flow recorder, so every row is an exchange that actually happened. The recorder now carries the user each exchange resolved, so a delegated row names userId/userPrincipalName while an app-only row is userless (correct, not missing); failures carry their concrete reason and every row has a stable id to de-duplicate on. conditionalAccessStatus is always notApplied — there is no CA engine, by design
Real ceremonies (real assertion verification, real CBOR/COSE); RP derived per-request from the Host, so passkeys work on any origin; drives amr:["fido"]
Real: the new password is scrypt-hashed into the directory, so the old credential immediately stops signing in and the new one works. Omitting newPassword returns a system-generated one in Entra’s passwordResetResponse shape, with 202 + Location as Graph answers this long-running operation
🟢 Real
Interactive SSPR (verify by email / SMS / security questions at passwordreset.microsoftonline.com)
Not implemented — it is a first-party web flow, not a documented protocol, so emulating it would mean inventing a wire format rather than reproducing one
🔴 Not implemented
SAML / WS-Federation
— stated non-goal
🔴 Not implemented
B2C user flows / External ID / CIAM
— stated non-goal
🔴 Not implemented
B2B guest invitations
Real: POST /invitations creates an actual directory user with Entra’s external shape — #EXT# UPN, userType: Guest, externalUserState: PendingAcceptance — and the returned redeem link flips that state to Accepted and redirects to the inviting app. Members keep userType: Member with a null external state
Real token exchange: an external workload presents ITS OWN OIDC token as the client_assertion and the emulator matches a registered issuer/subject/audience trust, then verifies the signature against keys fetched from that issuer’s published JWKS — no secret exists anywhere, which is the whole point. Expiry, wrong subject, wrong audience, forged signature and revoked credential are each refused. Managed through the admin API (/admin/api/apps/{id}/federated-credentials) rather than the Graph federatedIdentityCredentials route
A PDP port: real engines attach — OpenFGA, SpiceDB, Keto, Permify, Casbin, OPA, Cedar, all exercised in CI. A ~50-line InMemoryPDP ships so the sample runs with nothing attached; it is explicitly not a real engine
Real clients prove the emulator works; golden references prove its wire
contracts haven’t drifted. Three canonical references — the real Entra OIDC
discovery document, the official Microsoft Graph OpenAPI, and the SCIM 2.0 RFCs
— are committed under e2e/golden/ and diffed against the live emulator on
every push. Several 🟢 rows above name those tests as their witness
(TestGoldenParityOIDCDiscovery, TestGoldenParityGraph,
TestGoldenParitySCIM). See
golden-reference parity for what each asserts
and the documented divergences it reports.
The stated non-goals — SAML/WS-Fed, B2C user flows, MFA/Conditional Access,
production hardening — are a deliberate line. Crossing it changes the project’s
character from “the identity provider your tests run against” to “an identity
provider”, which is a different product with a different duty of care.
What that buys: everything above the line can be real, because none of it
needs a policy engine, a risk model, or a tenant’s compliance posture. A token
this emulator signs is a real token; a passkey it verifies is really verified.