Skip to content

Quickstart: your first token in 5 minutes

Point a real MSAL client at the emulator and acquire a working token — no cloud tenant, no app registration, no waiting. Every value below is a seeded dev constant, so the snippets are copy-paste runnable.

By the end you’ll have: the emulator running, a client-credentials token from curl and from MSAL (Node / Python / Go / azidentity / .NET / Java), and a Microsoft Graph call authorized by that token.

The simplest MSAL-friendly setup is compat mode (every surface on one origin) with the default TLS on — so everything lives at https://localhost:8443.

Terminal window
# from a clone:
go build ./cmd/entra-emulator
ORIGIN_MODE=compat ./entra-emulator
# …or with Docker (defaults to compat):
docker run -p 8443:8443 ghcr.io/calvinchengx/entra-emulator:latest

Prefer Homebrew, a pre-built binary (incl. Windows), or go install? See Installation for every method.

First run creates ./data/ with a SQLite store, a persisted self-signed TLS certificate (stable fingerprint), a persisted RSA signing key (stable kid), and the deterministic seed directory below.

Sanity check:

Terminal window
curl --cacert ./data/tls/cert.pem \
https://localhost:8443/11111111-1111-1111-1111-111111111111/v2.0/.well-known/openid-configuration

The seeded daemon app is a confidential client. Ask for a Graph-audience token with client credentials:

Terminal window
curl --cacert ./data/tls/cert.pem \
https://localhost:8443/11111111-1111-1111-1111-111111111111/oauth2/v2.0/token \
-d grant_type=client_credentials \
-d client_id=cccccccc-0000-0000-0000-000000000002 \
-d client_secret=daemon-app-secret \
-d 'scope=https://graph.microsoft.com/.default'

You get a normal access_token (an RS256 JWT). Paste it into jwt.ms or decode the middle segment: aud is https://graph.microsoft.com, tid is the seeded tenant, appid is the daemon.

Real MSAL SDKs work unchanged — you only override the authority (and, for non-Microsoft hosts, mark it known and skip instance discovery).

process.env.NODE_EXTRA_CA_CERTS = './data/tls/cert.pem'; // trust the emulator cert
import * as msal from '@azure/msal-node';
const cca = new msal.ConfidentialClientApplication({
auth: {
clientId: 'cccccccc-0000-0000-0000-000000000002',
clientSecret: 'daemon-app-secret',
authority: 'https://localhost:8443/11111111-1111-1111-1111-111111111111',
knownAuthorities: ['localhost:8443'],
},
});
const res = await cca.acquireTokenByClientCredential({
scopes: ['https://graph.microsoft.com/.default'],
});
console.log(res.accessToken);
import msal
app = msal.ConfidentialClientApplication(
"cccccccc-0000-0000-0000-000000000002", # client id
client_credential="daemon-app-secret",
authority="https://localhost:8443/11111111-1111-1111-1111-111111111111",
instance_discovery=False, # don't probe the real AAD metadata
verify="./data/tls/cert.pem", # trust the emulator cert
)
result = app.acquire_token_for_client(
scopes=["https://graph.microsoft.com/.default"])
print(result["access_token"]) # or result["error_description"] on failure

MSAL Go (microsoft-authentication-library-for-go)

Section titled “MSAL Go (microsoft-authentication-library-for-go)”
cred, _ := confidential.NewCredFromSecret("daemon-app-secret")
client, _ := confidential.New(
"https://localhost:8443/11111111-1111-1111-1111-111111111111",
"cccccccc-0000-0000-0000-000000000002", cred,
confidential.WithInstanceDiscovery(false), // don't call the real AAD metadata endpoint
// confidential.WithHTTPClient(certTrustingClient), // or trust the cert system-wide
)
res, _ := client.AcquireTokenByCredential(ctx,
[]string{"https://graph.microsoft.com/.default"})
fmt.Println(res.AccessToken)

azidentity (DefaultAzureCredential family)

Section titled “azidentity (DefaultAzureCredential family)”
cred, _ := azidentity.NewClientSecretCredential(
"11111111-1111-1111-1111-111111111111", // tenant
"cccccccc-0000-0000-0000-000000000002", // client id
"daemon-app-secret",
&azidentity.ClientSecretCredentialOptions{
ClientOptions: azcore.ClientOptions{
Cloud: cloud.Configuration{ActiveDirectoryAuthorityHost: "https://localhost:8443"},
},
})
tok, _ := cred.GetToken(ctx, policy.TokenRequestOptions{
Scopes: []string{"https://graph.microsoft.com/.default"}})
var app = ConfidentialClientApplicationBuilder
.Create("cccccccc-0000-0000-0000-000000000002") // client id
.WithClientSecret("daemon-app-secret")
.WithAuthority(new Uri("https://localhost:8443/11111111-1111-1111-1111-111111111111"),
validateAuthority: false)
.WithInstanceDiscovery(false) // don't probe AAD metadata
// Dev-only: trust the emulator's self-signed cert.
.WithHttpClientFactory(certTrustingFactory)
.Build();
var result = await app.AcquireTokenForClient(
new[] { "https://graph.microsoft.com/.default" }).ExecuteAsync();
var app = ConfidentialClientApplication.builder(
"cccccccc-0000-0000-0000-000000000002", // client id
ClientCredentialFactory.createFromSecret("daemon-app-secret"))
.authority("https://localhost:8443/11111111-1111-1111-1111-111111111111/")
.validateAuthority(false)
.instanceDiscovery(false) // don't probe AAD metadata
.build(); // trust the cert via the JVM trust store
var result = app.acquireToken(ClientCredentialParameters
.builder(Collections.singleton("https://graph.microsoft.com/.default"))
.build()).get();

The emulator serves a minimal read-only Microsoft Graph. Use the token from any step above:

Terminal window
TOKEN=... # the access_token from step 2 or 3
curl --cacert ./data/tls/cert.pem -H "Authorization: Bearer $TOKEN" \
https://localhost:8443/graph/v1.0/users

You’ll get the seeded users (Alice, Bob). See Graph API for the full surface (/me, /me/memberOf, $select/$filter, writes).

For Go integration tests, embed the emulator in-process — no ports, no docker, no cleanup. This is roadmap #1:

func TestSignIn(t *testing.T) {
emu := emulator.StartT(t, emulator.WithTLS()) // in-process; auto-closed
cred, _ := confidential.NewCredFromSecret(emulator.DaemonSecret)
client, _ := confidential.New(emu.Authority(), emulator.DaemonClientID, cred,
confidential.WithHTTPClient(emu.HTTPClient()), // trusts the instance cert
confidential.WithInstanceDiscovery(false))
res, _ := client.AcquireTokenByCredential(context.Background(),
[]string{"api://" + emulator.DaemonClientID + "/.default"})
// assert on res.AccessToken …
}

emu.Authority(), emu.HTTPClient(), and the seeded IDs/secrets are all exported, so tests need no hard-coded fixtures.

ThingValue
Tenant11111111-1111-1111-1111-111111111111
SPA (public, PKCE)cccccccc-0000-0000-0000-000000000001, redirect https://localhost:3000
Daemon (confidential)cccccccc-0000-0000-0000-000000000002, secret daemon-app-secret
User: Alicealice@entraemulator.dev / Password1!
User: Bobbob@entraemulator.dev / Password1!
GroupEngineering (Alice + Bob)

Full details, plus how to add your own, are in Data model & seed.

To exercise a user flow (authorization code + PKCE, device code, ROPC, passkeys), use the seeded SPA (cccccccc-0000-0000-0000-000000000001) as a public client and sign in as Alice at the emulator’s sign-in page. The exact request/response shapes are in OIDC endpoints.

There’s no first-party MSAL package for Flutter, so mobile apps use a generic AppAuth client. Point flutter_appauth at the emulator’s endpoints — it’s just OIDC authorization code + PKCE:

final result = await const FlutterAppAuth().authorizeAndExchangeCode(
AuthorizationTokenRequest(
'cccccccc-0000-0000-0000-000000000001', // seeded SPA client id
'com.example.app://auth', // your registered redirect
serviceConfiguration: AuthorizationServiceConfiguration(
authorizationEndpoint: '$authority/oauth2/v2.0/authorize',
tokenEndpoint: '$authority/oauth2/v2.0/token',
endSessionEndpoint: '$authority/oauth2/v2.0/logout',
),
scopes: ['openid', 'profile', 'email', 'offline_access'],
),
);
  • invalid_authority / instance-discovery errors — set knownAuthorities (MSAL.js/Node), instance_discovery=False (MSAL Python), or WithInstanceDiscovery(false) (MSAL Go); the SDK is trying to reach login.microsoftonline.com.
  • TLS / self-signed errors — trust the cert (entra-emulator trust), set NODE_EXTRA_CA_CERTS (Node), pass verify="<cert>" (MSAL Python), or use a cert-aware HTTP client (Go). MSAL Go requires https and won’t accept an http:// authority.
  • AADSTS500011 (resource not found) — client credentials needs exactly one <resource>/.default scope; check the resource matches a registered app or the Graph resource id.
  • Subdomain URLs won’t resolveORIGIN_MODE=compat keeps everything on localhost; otherwise run entra-emulator hosts --apply. See TLS & origins.