Test Microsoft.Identity.Web and MSAL apps without an Entra ID tenant

Point your ASP.NET Core web app, web API or MSAL.NET client at Entra ID v2.0-compatible endpoints that sign in configured test users. There's no tenant, no app registrations and no test accounts with MFA.

The problem

Apps built on Microsoft.Identity.Web or MSAL expect a real Entra ID tenant. Local development and CI then depend on a shared tenant, app registrations, client secrets that expire, and test accounts that hit Conditional Access or MFA prompts. Testing "what does an Orders.Admin user see?" means asking a directory admin to assign an app role.

Why HeyIdentity fits

A HeyIdentity configuration with the Microsoft profile serves the URLs Microsoft's libraries build from Instance and TenantId, with the configuration ID as the tenant ID:

https://app.heyidentity.dev/{configurationId}/v2.0/.well-known/openid-configuration
https://app.heyidentity.dev/{configurationId}/oauth2/v2.0/authorize
https://app.heyidentity.dev/{configurationId}/oauth2/v2.0/token
https://app.heyidentity.dev/{configurationId}/discovery/v2.0/keys
https://app.heyidentity.dev/{configurationId}/oauth2/v2.0/pin

The last one is the pinned user page: open it in the browser you test with to pin one test user, and sign-ins skip the user picker until you clear the pin. Pinning is a Pro feature; a configuration with a single user never shows a picker, on every plan. Use the lower-case configuration ID in your authority, because the pin is not sent to an upper-case URL.

Tokens look like Entra ID v2.0 tokens: issuer https://app.heyidentity.dev/{configurationId}/v2.0, and tid, oid, ver: "2.0", preferred_username, scp for delegated scopes, roles from the user's configured roles, and client_info for MSAL's token cache. A scope like api://orders-api/access_as_user produces an access token whose aud is the API's client ID, just as Entra does.

Set it up

  1. Sign up and open your management link.
  2. In Settings, choose the Microsoft profile.
  3. Add a client for your web app (for example orders-web, with a secret) with the redirect URIs https://localhost:5001/signin-oidc and https://localhost:5001/signout-callback-oidc.
  4. If you have an API, add a client for it (for example orders-api) and expose the scope api://orders-api/access_as_user. Allow that scope on the web app's client.
  5. Add test users and give each one roles, for example ["Orders.Admin"] for one user and ["Orders.Reader"] for another.

Web app sign-in (AddMicrosoftIdentityWebApp)

Only Instance and TenantId change. Note the trailing slash on Instance.

"AzureAd": {
  "Instance": "https://app.heyidentity.dev/",
  "TenantId": "{configurationId}",
  "ClientId": "orders-web",
  "ClientSecret": "{the client's secret}",
  "CallbackPath": "/signin-oidc"
}

Web API (AddMicrosoftIdentityWebApi)

"AzureAd": {
  "Instance": "https://app.heyidentity.dev/",
  "TenantId": "{configurationId}",
  "ClientId": "orders-api"
}

Default audience validation works, because tokens for api://orders-api/... carry aud: "orders-api". [RequiredScope("access_as_user")] and role checks work against scp and roles.

Web app that calls an API (token acquisition)

When the web app acquires tokens through MSAL (EnableTokenAcquisitionToCallDownstreamApi), MSAL first asks login.microsoftonline.com to validate the authority, and that check fails for any non-Microsoft host. Use an explicit authority instead of Instance/TenantId:

"AzureAd": {
  "Authority": "https://app.heyidentity.dev/{configurationId}/v2.0",
  "PreserveAuthority": true,
  "ClientId": "orders-web",
  "ClientSecret": "{the client's secret}",
  "CallbackPath": "/signin-oidc"
}

Leave Instance and TenantId empty or unset in that environment. This is the snippet the management page's Connect your app card shows; it also works for a web app that only signs users in, so one setting covers both.

MSAL.NET directly

Turn off instance discovery for the same reason:

var app = PublicClientApplicationBuilder.Create("orders-desktop")
    .WithAuthority("https://app.heyidentity.dev/{configurationId}/")
    .WithInstanceDiscovery(false)
    .WithRedirectUri("http://localhost")
    .Build();

var result = await app.AcquireTokenInteractive(new[] { "api://orders-api/access_as_user" }).ExecuteAsync();

ConfidentialClientApplicationBuilder works the same way, for the authorization code and client credentials flows.

Verify it

  1. Run your app and sign in. HeyIdentity shows your test users; pick one, or, with Pro, use Pin and sign in to skip the picker on later sign-ins in this browser.
  2. Check the signed-in user's claims: tid equals your configuration ID, and roles contains the user's roles.
  3. Sign in as the other user and confirm your role-protected pages behave differently.
  4. Or see it without any code: demo case D signs in against a Microsoft-profile configuration and shows these claims.

Every setting on this page is covered by HeyIdentity's own client conformance tests, which run the real Microsoft.Identity.Web and MSAL.NET libraries against it.

Limitations

Try it with your app

Sign up to get your own tenant and a configuration you can switch to the Microsoft profile.

Sign up See demo case D