Skip to content

calvinchengx/entra-emulator

Entra Emulator

CI Flutter e2e License: Apache 2.0

A local, MSAL-compatible emulator of Microsoft Entra ID (Azure AD), in a single Go binary. The OIDC/OAuth 2.0 v2.0 endpoints MSAL talks to, a minimal read-only Microsoft Graph, and an unauthenticated admin REST API, so you can develop sign-in, token acquisition, and protected-API calls offline with no cloud tenant.

Entra Emulator demo: OIDC discovery, a client-credentials token, and its decoded Entra v2.0 claims — all against a local binary

📖 Documentation site — the full reference, also browsable as Markdown in docs/.

⚠️ Local development tool only — intentionally insecure. Open admin API, publicly known seeded users/secrets, self-signed TLS, signing key stored unencrypted. Run it on localhost only. Never point real users or secrets at it.

Quick start

go build ./cmd/entra-emulator
./entra-emulator
# Health:    https://portal.entra.localhost:8443/health   (compat: https://localhost:8443/health)
# Discovery: https://login.entra.localhost:8443/11111111-1111-1111-1111-111111111111/v2.0/.well-known/openid-configuration

First run creates ./data/ with the SQLite store, a persisted self-signed wildcard TLS certificate (stable fingerprint), a persisted RSA signing key (stable kid), and a deterministic seed directory. Subdomain names need hosts entries (./entra-emulator hosts --apply), or set ORIGIN_MODE=compat to keep everything on https://localhost:8443.

Docker

A ~13 MB distroless image (pure-Go, no cgo) with a built-in HEALTHCHECK:

docker run -p 8443:8443 -v entra-emulator-data:/app/data \
  ghcr.io/calvinchengx/entra-emulator:latest

The image defaults to ORIGIN_MODE=compat and binds 0.0.0.0; mount a volume at /app/data to persist the store and cert. Tagged releases also publish cross-platform binaries (linux/darwin/windows × amd64/arm64) via GoReleaser.

Homebrew

macOS and Linux (Intel/Apple Silicon), from the tap:

brew install calvinchengx/tap/entra-emulator
entra-emulator version

Each tagged release refreshes the cask automatically. brew upgrade picks up new versions.

winget (Windows)

winget install calvinchengx.entra-emulator
entra-emulator version

Available once a release's manifest PR merges into microsoft/winget-pkgs (validation can take a day or two); until then, use the release archive or go install. Every install method — Homebrew, winget, Docker, pre-built binaries, go install, source — is in docs/02-installation.md.

What works

  • Flows: Authorization Code + PKCE (S256/plain), Client Credentials (<resource>/.default, app-role auto-grant), rotating Refresh Tokens with family-revocation-on-reuse, Device Code (RFC 8628, with the human approval page), front-channel logout, OIDC UserInfo.
  • Sign-in methods: account-picker / password (amr: ["pwd"]) and passkeys (FIDO2/WebAuthn) — register + assert ceremonies yielding amr: ["fido"], with the relying party built per-request from the Host header (so passkeys work on any origin). See How-to: passkey sign-in.
  • Tokens: real RS256-signed JWTs with Entra v2.0 claim shapes (tid, oid, scp/roles, pairwise sub, ver: "2.0", client_info), verifiable against the live JWKS (kid = RFC 7638 thumbprint). Optional claims + group claims (with the Entra-style overage payload) per app registration.
  • Graph: /v1.0/me, /users, /users/{id-or-upn}, /groups, /groups/{id}, /groups/{id}/members with @odata envelopes and $top/$skiptoken paging.
  • Admin API: full CRUD for users, groups (+membership), app registrations (+redirect URIs, show-once secrets, scopes, app roles), seed/reset, health, certificate download.
  • Surfaces: one HTTPS listener, Host-routed — login. / portal. / graph.entra.localhost, with localhost as the serve-everything compat origin.

Point MSAL at it

{
  "auth": {
    "clientId": "cccccccc-0000-0000-0000-000000000001",
    "authority": "https://login.entra.localhost:8443/11111111-1111-1111-1111-111111111111",
    "knownAuthorities": ["login.entra.localhost:8443"],
    "redirectUri": "https://localhost:3000"
  }
}

Trust the self-signed cert (./entra-emulator trust prints the platform command; NODE_EXTRA_CA_CERTS=$( ./entra-emulator cert-path ) for Node clients).

Seed data (fixed GUIDs, reproducible CI)

What Value
Tenant 11111111-1111-1111-1111-111111111111
Users alice@entraemulator.dev, bob@entraemulator.dev (password Password1!), group Engineering
Public SPA app cccccccc-…-0001, redirect https://localhost:3000, scope access_as_user
Confidential daemon cccccccc-…-0002, secret daemon-app-secret, app role Tasks.Read.All

Configuration

Environment > entra-emulator.config.json > defaults; invalid config aborts naming the offending key. Key settings: PORT (8443), TENANT_ID, ORIGIN_MODE (subdomains|compat), PUBLIC_ORIGIN, REQUIRE_PASSWORD, DB_PATH, TOKEN_LIFETIME_*. Full reference: docs/04-configuration.md.

Design & development

The full design lives in docs/ — architecture, configuration, data model + seed, token service, endpoint contracts, Graph, admin API, TLS/origins, testing, and the post-parity roadmap (embeddable Go test library, token forge, fault injection…).

go build ./...   # everything, including the CLI
go test ./...    # integration tests drive the full handler stack
go vet ./...
python3 e2e/run.py     # real-SDK e2e: @azure/msal-node, MSAL Go + azidentity, MSAL Python

The e2e suites prove unmodified Microsoft SDKs complete real flows against the emulator (see docs/16-e2e-sdk-matrix.md): client credentials, Authorization Code + PKCE with client_info account identity, silent refresh, and device code with headless approval — in TypeScript, Go, and Python.

Embed it in Go tests

The emulator package runs the whole thing in-process — no external server, no fixed ports — so a Go test can point MSAL Go / azidentity straight at it:

import "github.com/calvinchengx/entra-emulator/emulator"

func TestMyAPI(t *testing.T) {
    emu := emulator.StartT(t, emulator.WithTLS()) // auto-closed at test end
    cred, _ := confidential.NewCredFromSecret(emulator.DaemonSecret)
    client, _ := confidential.New(emu.Authority(), emulator.DaemonClientID, cred,
        confidential.WithHTTPClient(emu.HTTPClient()),      // trusts the instance cert
        confidential.WithInstanceDiscovery(false))
    tok, _ := client.AcquireTokenByCredential(ctx, []string{"api://…/.default"})
    // …drive your resource API with tok.AccessToken
}

emu exposes Authority(), Issuer, JWKSURL(), the seeded client IDs/secrets, and Store() for direct fixture setup. WithTLS() is required for MSAL clients (they reject non-HTTPS authorities); plain HTTP is fine for direct API calls.

Dependencies: modernc.org/sqlite (pure-Go SQLite, no cgo) and golang.org/x/crypto (scrypt). Cross-compiles to a single static binary on all platforms.

Implementation notes

Protocol surface, claim shapes, and error bodies follow Microsoft's published Entra ID v2.0 behavior. Internals: Go stdlib net/http, hand-rolled RS256 JWS, SQLite via a pure-Go driver, and a Svelte portal embedded with go:embed.

Disclaimer

An independent developer tool, not affiliated with or endorsed by Microsoft. "Microsoft Entra ID", "Azure AD", "Microsoft Graph", and "MSAL" are Microsoft trademarks. This project emulates publicly documented protocol behavior for local development and testing only.

License

Apache License 2.0. See NOTICE for acknowledgments.

About

A local, MSAL-compatible emulator of Microsoft Entra ID in a single Go binary — OIDC/OAuth2 endpoints, minimal Graph, admin portal, workspace/managed identities

Resources

License

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors