To run a SaaS application for many organizations on Microsoft Entra ID, register one multi-tenant application in your own tenant, onboard each customer through the admin consent endpoint so a service principal is created in their tenant, and enforce your own tenant allowlist in token validation, because Entra ID issues tokens to any organization that consents. Validate the issuer against the tid claim, key all data by tid and oid, and turn on app instance property lock so nobody can add credentials to your app's service principals in customer tenants.
Who this is for and what you will have
This guide is for developers and architects building a B2B SaaS product, an ISV integration or an internal platform used by several Entra ID tenants. It assumes you are comfortable with OAuth 2.0 and OpenID Connect. At the end you will have:
- A multi-tenant app registration with the right audience, App ID URI and token version.
- An onboarding flow that sends customer admins through admin consent and records the tenant safely.
- Token validation that enforces a tenant allowlist and ties
tidto the issuer. - App instance property lock configured on the registration.
- A troubleshooting list for the AADSTS errors customers report during onboarding.
How multi-tenant apps work in Entra ID
A multi-tenant app has one application object in your home tenant. When a user or admin from another tenant consents, Entra ID creates a service principal, a local representation of your app, in that tenant, along with a record of the consent. That service principal is what the customer manages: it lets them apply their own policies to sign-ins, require user assignment, and remove the app.
Two consequences shape the design:
- Entra ID doesn't know who your customers are. Any organization whose admin consents can sign users in to your app and get tokens for your API. Restricting access to customers who have actually signed up is your app's job.
- Your app has a service principal in every customer tenant. App instance property lock exists so the sensitive properties of those service principals, such as credentials, can't be modified there.
Prerequisites
- An Entra ID tenant where you register the app.
- An account with at least Cloud Application Administrator or Application Administrator rights in that tenant.
- A verified custom domain in the tenant for the publisher domain (publisher verification doesn't accept
*.onmicrosoft.com). - For publisher verification: a Microsoft AI Cloud Partner Program account that has completed verification, associated with the tenant.
- A test customer tenant where you can act as an administrator.
Step 1: Register the multi-tenant app
- Sign in to the Microsoft Entra admin center and open the app registration, or create one.
- Select Authentication, and under Supported account types choose Accounts in any organizational directory. Web app and API registrations are single-tenant by default.
- Set the Application ID URI. For a multi-tenant app it must be globally unique across all tenants. Microsoft's example for a tenant named
contoso.onmicrosoft.comishttps://contoso.onmicrosoft.com/myapp. If the App ID URI doesn't follow this pattern, switching the app to multi-tenant fails. - Add the exact redirect URIs your sign-in and onboarding pages use. The admin consent endpoint requires the
redirect_urito match a registered value. - If you expose an API, set
requestedAccessTokenVersionto2in the manifest so your API receives v2.0 access tokens. The valuesnulland1produce v1.0 tokens, and the version determines which metadata document you validate against. - Set a publisher domain on your verified domain and complete publisher verification. Customers see a blue verified badge on the consent prompt, and admins can build user consent policies around it. If a customer tenant has risk-based step-up consent enabled, users can't consent to most multi-tenant apps registered after November 8, 2020 that aren't publisher verified and request more than basic sign-in and profile permissions.
Separate API and client registrations
If your product has a web front end and a separate API, each with its own registration, the API must exist in the customer tenant before the client can be consented. Add the client's application ID to the API registration's knownClientApplications property in the manifest, so the customer consents to both in one step:
"knownClientApplications": ["11112222-bbbb-3333-cccc-4444dddd5555"]Step 2: Choose the authority
A multi-tenant app can't know the user's tenant before sign-in, so it sends sign-in requests to a shared authority:
| Authority | Accepts |
|---|---|
https://login.microsoftonline.com/organizations | Work and school accounts from any Entra ID tenant |
https://login.microsoftonline.com/common | Work and school accounts plus personal Microsoft accounts |
For a B2B SaaS product, use organizations. After the first sign-in you know the user's tenant, so request subsequent tokens from the tenant-specific endpoint. Microsoft documents a common MSAL mistake: requesting a second token from /common misses the cache, because the cached token is stored under the user's tenant, and the user is prompted to sign in again.
Background jobs that call Microsoft Graph with application permissions use the client credentials flow, and that request always targets one customer tenant:
POST https://login.microsoftonline.com/{customer-tenant-id}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
client_id=00001111-aaaa-2222-bbbb-3333cccc4444
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=<JWT signed with your certificate>
&grant_type=client_credentialsPrefer a certificate or a federated credential over a client secret for the client credential; Microsoft describes both as giving a higher level of assurance.
Step 3: Onboard customers with admin consent
Application permissions always require an administrator's consent, and many delegated permissions do too. Build an explicit "Connect your organization" page instead of relying on whichever user signs in first.
- Sign the admin in first. Microsoft recommends signing the user in before showing the connect view, so you know which organization they belong to.
- Redirect to the admin consent endpoint.
https://login.microsoftonline.com/{customer-tenant-id}/v2.0/adminconsent
?client_id=00001111-aaaa-2222-bbbb-3333cccc4444
&scope=https://graph.microsoft.com/.default
&redirect_uri=https://app.contoso.com/onboarding/consent-callback
&state=8f2c1e4aUse the tenant ID from step 1, or organizations. Don't use common. The static /.default scope requests every permission configured on the registration and is required for application permissions; dynamic scopes let you ask for delegated permissions at run time instead.
- Handle the callback. A successful response returns
admin_consent=True,tenant,scopeand yourstate. A failure addserroranderror_description.
https://app.contoso.com/onboarding/consent-callback
?admin_consent=True
&tenant=aaaabbbb-0000-cccc-1111-dddd2222eeee
&scope=https://graph.microsoft.com/.default
&state=8f2c1e4a- Record the tenant from a validated token, not the query string. Microsoft warns never to use the
tenantvalue in the callback to authenticate or authorize anyone, because an attacker can forge it. Checkstate, then take the tenant ID from thetidclaim of the admin's validated ID token and add it to your allowlist.
An alternative for delegated-only apps is the normal authorization request with prompt=consent sent by an admin. After the admin consents and the service principal is created, other users in that tenant aren't prompted. If an admin signs in without prompt=consent, the consent applies only to their own account, which is useful for a trial.
Step 4: Enforce the tenant allowlist in token validation
Every web app validating ID tokens and every API validating access tokens should apply these checks, in order:
- Signature and signing key issuer. Fetch keys from the tenant-independent metadata (
https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration). Each key in the keys document has anissuer. A templated issuer such ashttps://login.microsoftonline.com/{tenantid}/v2.0may be used only when the token'sissmatches after substituting the token'stid. - Issuer bound to tenant. Confirm
tidis a GUID and thatissis exactlyhttps://login.microsoftonline.com/{tid}/v2.0for thattid. This ties the tenant to the issuer and to the scope of the signing key. - Audience. For v2.0 access tokens,
audmust be your API's client ID. - Tenant allowlist. Reject any
tidthat isn't an active customer. Microsoft's claims guidance is explicit: data stored in the context of a tenant must only be accessed from the same tenant. - Subject and actor. Check
scpfor delegated calls androlesfor app-only calls. Don't authorize everyone in a tenant just becausetidmatches; Microsoft notes that checking onlytidand the presence ofoidcould also admit every service principal in that tenant.
validate signature using keys from jwks_uri (tenant-independent metadata)
key_issuer = keys[kid].issuer.replace("{tenantid}", token.tid)
require key_issuer == token.iss
require is_guid(token.tid)
require token.iss == "https://login.microsoftonline.com/" + token.tid + "/v2.0"
require token.aud == API_CLIENT_ID
require tenants.is_active(token.tid) # your allowlist
require "Orders.Read" in token.scp or "Orders.Read.All" in token.roles
data_key = (token.tid, token.oid) # never email or upnIf you use Microsoft.Identity.Web for ASP.NET Core, it performs the signature and issuer checks for you; the allowlist and authorization checks remain yours.
Identify users and tenants correctly
- Use
tidandoidtogether as the key for a user's data. The samesubin two tenants represents two different users. - Never use
email,preferred_usernameorunique_namefor authorization; they aren't unique and can be changed by tenant administrators or users. Don't useupneither, because it changes over the user's lifetime. - To authorize a specific client application across tenants for app-only calls, use
azp(v2.0) orappid(v1.0), and validate the optionalidtypclaim equalsapp, because delegated tokens can be obtained by other parties.
The same principle applies to any service boundary: verify identity and tenant on every request. For a broader view, see zero trust enterprise remote access architecture.
Step 5: Lock the service principal with app instance property lock
App instance property lock protects sensitive properties of your app's service principals from modification. The lockable properties are:
- Credentials with usage type
Verify(used when the app authenticates with client credentials). - Credentials with usage type
Sign(used for SAML token signing). tokenEncryptionKeyId, which tells Entra ID to encrypt tokens with a specific public key.
Microsoft states that since June 2026 Enable property lock is on by default for new applications. Older registrations need to be checked.
- In the Microsoft Entra admin center, as at least a Cloud Application Administrator, go to Entra ID > App registrations and open your app.
- Select Authentication, then Configure under App instance property lock.
- Turn on Enable property lock and select All properties, unless your app genuinely needs customers to configure one of these properties.
- Select Save.
The same setting is available through the servicePrincipalLockConfiguration property of the application object in Microsoft Graph. Microsoft's tutorial uses the beta endpoint:
Import-Module Microsoft.Graph.Beta.Applications
$params = @{
servicePrincipalLockConfiguration = @{
isEnabled = $true
allProperties = $true
}
}
Update-MgBetaApplication -ApplicationId $applicationObjectId -BodyParameter $paramsStep 6: Give customer admins control
Document the controls customers have, so security teams can approve your app quickly:
- Require assignment. In Entra ID > Enterprise apps > your app > Properties, set Assignment required? to Yes, then assign users and groups under Users and groups. User consent is then disallowed, so tenant-wide admin consent is required.
- Pre-create or manage the service principal with PowerShell:
Connect-MgGraph -Scopes "Application.ReadWrite.All"
$appId = "00001111-aaaa-2222-bbbb-3333cccc4444"
$sp = Get-MgServicePrincipal -Filter "AppId eq '$appId'"
if (-not $sp) { $sp = New-MgServicePrincipal -AppId $appId }
Update-MgServicePrincipal -ServicePrincipalId $sp.Id -AppRoleAssignmentRequired:$true- Revoke access. Admins remove the app or its permissions under Enterprise applications. If an admin consented for all users, individual users can't revoke it.
Your offboarding process should remove the tenant from the allowlist and stop background jobs for it, even if the customer leaves the service principal in place.
Verification
- From the test customer tenant, run the connect flow as an admin and confirm a service principal for your app appears under Enterprise applications.
- Sign in as a normal user from that tenant and confirm they aren't asked to consent again.
- Sign in from a third tenant that isn't on your allowlist. Entra ID will issue a token, and your app must reject it.
- Decode a token in a test environment and confirm
veris2.0,isscontains thetid, andaudis your API's client ID. - In your app registration, confirm App instance property lock shows the lock enabled.
Troubleshooting
AADSTS50194: application isn't configured as a multitenant application. You're using /common with a single-tenant registration. Change Supported account types, or use a tenant-specific endpoint.
AADSTS700016: application wasn't found in the directory/tenant. The app hasn't been consented in that tenant, or the request went to the wrong tenant. Run the admin consent flow first.
AADSTS65001: the user or administrator hasn't consented. Consent is missing for the requested permissions. Send an interactive request or rerun admin consent.
AADSTS90094: administrator consent is required. The permissions need admin consent, or the customer disabled user consent. Have an admin use the connect flow.
AADSTS500011: resource principal not found in the tenant. Your API's service principal doesn't exist in the customer tenant. Use knownClientApplications or consent the API first.
AADSTS50105: signed-in user isn't assigned to a role for the app. The customer set Assignment required? to Yes. Ask their admin to assign the user or a group.
AADSTS650052: the app needs access to a service the organization hasn't subscribed to. Your app requests permissions for a service the customer doesn't subscribe to or hasn't enabled. Remove that permission from the request, or ask the customer's admin to review their service subscriptions.
Users get a sign-in prompt on every token request. You're requesting follow-up tokens from /common or /organizations. Use the tenant-specific authority after the first sign-in.
Closing checklist
- Registration set to Accounts in any organizational directory with a globally unique App ID URI.
requestedAccessTokenVersionset to2for your API; validation uses matching metadata.- Publisher domain set and publisher verification completed.
- Onboarding uses the admin consent endpoint; the tenant is recorded from a validated token's
tid, never from the callback query string. - Token validation checks signing key issuer,
issbound totid,aud, the tenant allowlist, andscporroles. - Data keyed by
tidandoid; no authorization onemail,upnorpreferred_username. - App instance property lock enabled with All properties.
- Customer documentation covers assignment, revocation and the permissions you request.
References
- Convert single-tenant app to multitenant on Microsoft Entra ID
- Microsoft identity platform admin consent protocols
- OAuth 2.0 client credentials flow
- Access tokens in the Microsoft identity platform
- Secure applications and APIs by validating claims
- How to configure app instance property lock
- Working with applications using Microsoft Graph: lock sensitive properties
- Publisher verification overview
- Restrict a Microsoft Entra app to a set of users
- Microsoft Entra authentication and authorization error codes