# Feature parity: entra-emulator vs. real Microsoft Entra ID How the emulator's surface maps to real Entra ID (as documented at [learn.microsoft.com/entra](https://learn.microsoft.com/en-us/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. :::note[Absent means 404, not 501] This emulator has **no `501` stubs**: a feature that isn't implemented simply has no route, so a client sees a **404**. A πŸ”΄ below therefore means "absent", not "honestly refused". Likewise 🟠 means *"real when you attach a real engine"* β€” where a toy fallback ships instead, the row says so. ::: ## Legend | | Meaning | |---|---| | 🟒 **Real** | Genuine work: real signed JWTs, real crypto, real state, real logic enforced β€” no pretending. | | 🟑 **Emulated** | Faithful API contract + persisted state, but no engine β€” clock-derived or management-only. | | 🟠 **Bring-your-own-engine** | Real when you attach a real external engine; a toy or companion stands in otherwise. | | πŸ”΄ **Not implemented** | Absent (404). | ## Token service (`07-token-service`) | Entra feature | Emulator | Type | |---|---|---| | RS256-signed access / ID tokens | Real compact JWS over `crypto/rsa` β€” real 2048-bit keys, real signatures any validator accepts | 🟒 Real | | `kid` / JWKS | RFC 7638 JWK thumbprint; JWKS publishes active **and** retired-but-unexpired keys | 🟒 Real | | Signing-key rotation | Real: new active key, old retired but still served until it expires, mutex-guarded signer swap | 🟒 Real | | Entra v2.0 claim shapes (`tid`/`oid`/`azp`/`appid`/`scp`/`roles`/`ver`/`idtyp`, pairwise `sub`) | Full | 🟒 Real | | `amr` (`pwd` / `fido`) | Threaded from the actual grant used | 🟒 Real | | `wids` (directory-role template GUIDs) | Emitted, gated on `groupMembershipClaims` | 🟒 Real | | Optional claims + **group overage** (`_claim_names` / `_claim_sources`) | Real Entra overage payload above the limit; protocol claims non-overridable | 🟒 Real | | Signing algorithms **other than RS256** (ES256 / PS256) | RS256 only β€” a deliberate, documented choice | πŸ”΄ Not implemented | ## OIDC / OAuth2 endpoints (`08-oidc-endpoints`) | Entra feature | Emulator | Type | |---|---|---| | OIDC discovery + JWKS | Full, conformance-tested against the real Entra discovery document | 🟒 Real | | **Instance discovery** (`/common/discovery/instance`) | Served β€” MSAL calls it before every token request, and a 404 fails the whole login | 🟒 Real | | `authorization_code` + **PKCE** (S256/plain) | Real, with atomic single-use code consumption | 🟒 Real | | `refresh_token` | Real rotation, plus **family revocation on reuse** β€” replaying a rotated token kills the whole chain | 🟒 Real | | `client_credentials` | Real; `.default` only, tolerating the stray scopes MSAL-Go/azidentity send | 🟒 Real | | `password` (ROPC) | Real scrypt verification β†’ `amr:["pwd"]` | 🟒 Real | | `urn:ietf:params:oauth:grant-type:jwt-bearer` (on-behalf-of) | Real; enforces assertion audience, rejects app-only assertions | 🟒 Real | | 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 | Implemented β€” **but discovery advertises only `client_secret_post`/`client_secret_basic`, so a spec-driven client never tries it** | 🟑 Emulated | | Front-channel logout | `/logout` exists; `frontchannel_logout_supported` is not advertised | 🟑 Emulated | | **Implicit / hybrid flow** | `response_types_supported: ["code"]` only | πŸ”΄ Not implemented | | **mTLS / PoP / certificate-bound tokens** | β€” | πŸ”΄ Not implemented | | **PAR / JAR** (`request_uri`) | β€” | πŸ”΄ Not implemented | | **CAE** (continuous access evaluation) | β€” | πŸ”΄ Not implemented | | **Token-lifetime / claims-mapping policies** | β€” | πŸ”΄ Not implemented | ## Microsoft Graph (`09-graph-api`) | Entra feature | Emulator | Type | |---|---|---| | Directory reads (`/me`, users, groups, members, `memberOf`, `userinfo`) | Full, over the real store | 🟒 Real | | Directory **writes** (users, groups, applications; group membership `$ref`) | Full CRUD, persisted | 🟒 Real | | Recycle bin (`directory/deletedItems`, restore, permanent delete) | Real state machine; the 30-day window is **clock-derived**, so it's testable | 🟑 Emulated | | OAuth2 permission grants (consent) | Stored **and load-bearing**: consented scopes are intersected into the token's `scp`, honouring `AllPrincipals` vs per-principal | 🟒 Real | | Directory roles (`roleManagement/directory`) | Full CRUD, and assignments really drive `wids` in tokens | 🟒 Real | | Authentication methods inventory (password / FIDO2) | Read + delete | 🟑 Emulated | | OData `$select` / `$filter` / `$top` / `$skiptoken` / `$count` | Supported (single `$filter` clause) | 🟒 Real | | **Graph permission enforcement** (scopes/roles gating operations) | **Any valid Graph-audience token authorizes any operation** β€” documented, deliberate | πŸ”΄ Not implemented | | Separate servicePrincipal store | An app registration **is** its own SP; object `id` and `appId` are conflated | 🟑 Emulated | | **Custom role definitions** | Only the seeded built-ins exist | πŸ”΄ Not implemented | | **Administrative units**, **custom security attributes** | β€” | πŸ”΄ Not implemented | | **Graph `beta` endpoint** | v1.0 only | πŸ”΄ Not implemented | | **Sign-in / audit logs** (Graph `auditLogs`, `signIns`) | β€” (the emulator's own `/admin/api/audit` is a different, test-only thing) | πŸ”΄ Not implemented | ## SCIM 2.0 (`10-scim-provisioning`) | Entra feature | Emulator | Type | |---|---|---| | SCIM **service provider** (inbound): `ServiceProviderConfig`, `ResourceTypes`, `Schemas`; Users + Groups CRUD, PatchOp | Real RFC 7643/7644 shapes over real HTTP, bearer static-secret auth as Entra does | 🟒 Real | | SCIM **provisioning client** (outbound): filter-probe β†’ create / update / `active:false` deprovision, member-correlated groups, incremental watermark | Real β€” the emulator pushes the directory out using Entra's actual sequence | 🟒 Real | | Provisioning **scheduler** (the ~40-minute cycle) | Admin-triggered instead of timed β€” deliberate, so tests are deterministic | 🟑 Emulated | | `PUT /Groups/{id}` | Users have PUT; Groups do not (GET/POST/PATCH/DELETE only) | πŸ”΄ Not implemented | ## Sign-in experience | Entra feature | Emulator | Type | |---|---|---| | Passkey / WebAuthn sign-in | 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 | | Attestation policy / AAGUID allowlists / cross-device CTAP | Stated non-goals | πŸ”΄ Not implemented | | **MFA / step-up authentication** | β€” | πŸ”΄ Not implemented | | **Conditional Access** (policies, named locations, auth strengths) | β€” the line the project deliberately doesn't cross | πŸ”΄ Not implemented | | **Identity Protection / risky users** | β€” | πŸ”΄ Not implemented | | **SSPR / password reset** | β€” | πŸ”΄ Not implemented | | **SAML / WS-Federation** | β€” stated non-goal | πŸ”΄ Not implemented | | **B2C user flows / External ID / CIAM** | β€” stated non-goal | πŸ”΄ Not implemented | | **B2B guest invitations / cross-tenant access** | β€” | πŸ”΄ Not implemented | ## Workload & platform identity | Entra feature | Emulator | Type | |---|---|---| | **Managed identity** (`/msi/token`, App Service protocol) | Real β€” `azidentity`'s ManagedIdentityCredential gets a real token | 🟒 Real | | Fabric-audience tokens + workspace identity (app reg + SP + managed credential, state machine, cascade delete) | Real at the token layer | 🟒 Real | | Fabric **control plane** | Out of scope by design β€” the companion `fabric-emulator` serves it | 🟠 BYO-companion | | **Workload identity federation** (`federatedIdentityCredential`) | β€” despite `private_key_jwt` being present | πŸ”΄ Not implemented | | **Device registration / Intune compliance / device-bound tokens** | β€” | πŸ”΄ Not implemented | | **Application Proxy** | β€” | πŸ”΄ Not implemented | | **PIM / privileged access**, **entitlement management**, **access reviews** | β€” | πŸ”΄ Not implemented | | **Group writeback / hybrid sync (AD Connect)** | β€” | πŸ”΄ Not implemented | ## Authorization beyond the token | Entra feature | Emulator | Type | |---|---|---| | Externalized authorization (fine-grained, relationship-based) | 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 | 🟠 BYO-engine | | Custom authentication extensions (token-issuance webhook callout) | Real callout β€” you bring the endpoint | 🟠 BYO-engine | ## Directory, storage & transport | Entra feature | Emulator | Type | |---|---|---| | Persisted directory | Real SQLite (WAL, foreign keys, forward-only migrations) | 🟒 Real | | Credential hashing | Real scrypt for passwords/secrets; SHA-256 for refresh/device codes | 🟒 Real | | Concurrency contracts (single-use codes, refresh-reuse detection, device-code approveβ†’mint) | Real atomic SQL β€” not best-effort | 🟒 Real | | Multi-tenant | Multiple tenants exist and are isolated | 🟑 Emulated | | TLS with a wildcard cert over the emulator's origins | Real self-signed X.509, regenerated on SAN drift, stable fingerprint otherwise | 🟒 Real | | **Cloud-instance metadata** (`tenant_region_scope`, `cloud_instance_name`, sovereign clouds) | β€” | πŸ”΄ Not implemented | ## Emulator-only (no Entra equivalent β€” these exist for testing) | Feature | Purpose | |---|---| | Token forge (`/admin/api/tokens`) | Mint arbitrary claims, negative expiry, or a deliberately invalid signature β€” test your validator's failure paths | | Clock control (`/admin/api/clock`) | Freeze/advance β€” makes token expiry and the 30-day recycle bin deterministic | | Fault injection (`/admin/api/faults`) | Forced token errors, latency, probabilistic flakiness | | Audit trail (`/admin/api/audit`) | Every authorize/token exchange, for assertions | | Export / import | Snapshot a directory; deliberately excludes signing keys and live grants | | Admin portal | Inspect and drive the directory in a browser | ## Ecosystem conformance: real clients as witnesses The bar: **real clients, unmodified**. Two knobs make them work β€” instance discovery disabled per-SDK, and TLS trust injected per-SDK. | Real client (pinned) | Surface exercised | Status | |---|---|---| | `@azure/msal-node` | client_credentials, auth code + PKCE, refresh, device code; `client_info` account identity, `nonce`, `ver:"2.0"` | 🟒 CI `sdk-e2e` | | `@microsoft/microsoft-graph-client` | Through the SDK's own pipeline: user CRUD, role assignment, consent grant, app-role assignment, auth methods, recycle-bin round-trip | 🟒 CI `sdk-e2e` | | MSAL **Go** + **`azidentity`** | client_credentials, device code, `ClientSecretCredential`, **ManagedIdentityCredential**, embedded-library mode | 🟒 CI `sdk-e2e` | | MSAL **Python** | client_credentials, device code | 🟒 CI `sdk-e2e` | | **MSAL.NET** (`Microsoft.Identity.Client`) | client_credentials + token-cache hit, app-only claim shape | 🟒 CI `sdk-e2e` | | **msal4j** (Java) | client_credentials, with the emulator cert in a real trust store | 🟒 CI `sdk-e2e` | | OpenFGA Β· SpiceDB Β· Keto Β· Permify Β· Casbin Β· OPA Β· Cedar | The PDP port against real engines | 🟒 CI `pdp-compat` (7-way matrix) | | Flutter (`http`, `flutter_appauth`) | Device code on real Android/iOS | 🟑 Nightly, not a PR gate; the auth-code leg is a manual screen | | **`@azure/msal-browser`** | β€” | πŸ”΄ Not wired | ## Scope boundary: a dev-loop emulator, not an IdP 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.