Architecture

Multi-tenant SaaS on Entra ID: admin consent, tenant allowlist, app lock

Register a multi-tenant SaaS app in Microsoft Entra ID, onboard customer tenants through admin consent, enforce a tenant allowlist in token validation and lock service principal credentials with app instance property lock.

12 min read
On this page

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 tid to 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:

  1. 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.
  2. 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

  1. Sign in to the Microsoft Entra admin center and open the app registration, or create one.
  2. Select Authentication, and under Supported account types choose Accounts in any organizational directory. Web app and API registrations are single-tenant by default.
  3. 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.com is https://contoso.onmicrosoft.com/myapp. If the App ID URI doesn't follow this pattern, switching the app to multi-tenant fails.
  4. Add the exact redirect URIs your sign-in and onboarding pages use. The admin consent endpoint requires the redirect_uri to match a registered value.
  5. If you expose an API, set requestedAccessTokenVersion to 2 in the manifest so your API receives v2.0 access tokens. The values null and 1 produce v1.0 tokens, and the version determines which metadata document you validate against.
  6. 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:

AuthorityAccepts
https://login.microsoftonline.com/organizationsWork and school accounts from any Entra ID tenant
https://login.microsoftonline.com/commonWork 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_credentials

Prefer a certificate or a federated credential over a client secret for the client credential; Microsoft describes both as giving a higher level of assurance.

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.

  1. Sign the admin in first. Microsoft recommends signing the user in before showing the connect view, so you know which organization they belong to.
  2. 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=8f2c1e4a

Use 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.

  1. Handle the callback. A successful response returns admin_consent=True, tenant, scope and your state. A failure adds error and error_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
  1. Record the tenant from a validated token, not the query string. Microsoft warns never to use the tenant value in the callback to authenticate or authorize anyone, because an attacker can forge it. Check state, then take the tenant ID from the tid claim 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:

  1. 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 an issuer. A templated issuer such as https://login.microsoftonline.com/{tenantid}/v2.0 may be used only when the token's iss matches after substituting the token's tid.
  2. Issuer bound to tenant. Confirm tid is a GUID and that iss is exactly https://login.microsoftonline.com/{tid}/v2.0 for that tid. This ties the tenant to the issuer and to the scope of the signing key.
  3. Audience. For v2.0 access tokens, aud must be your API's client ID.
  4. Tenant allowlist. Reject any tid that 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.
  5. Subject and actor. Check scp for delegated calls and roles for app-only calls. Don't authorize everyone in a tenant just because tid matches; Microsoft notes that checking only tid and the presence of oid could 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 upn

If 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 tid and oid together as the key for a user's data. The same sub in two tenants represents two different users.
  • Never use email, preferred_username or unique_name for authorization; they aren't unique and can be changed by tenant administrators or users. Don't use upn either, because it changes over the user's lifetime.
  • To authorize a specific client application across tenants for app-only calls, use azp (v2.0) or appid (v1.0), and validate the optional idtyp claim equals app, 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.

  1. In the Microsoft Entra admin center, as at least a Cloud Application Administrator, go to Entra ID > App registrations and open your app.
  2. Select Authentication, then Configure under App instance property lock.
  3. Turn on Enable property lock and select All properties, unless your app genuinely needs customers to configure one of these properties.
  4. 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 $params

Step 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

  1. From the test customer tenant, run the connect flow as an admin and confirm a service principal for your app appears under Enterprise applications.
  2. Sign in as a normal user from that tenant and confirm they aren't asked to consent again.
  3. Sign in from a third tenant that isn't on your allowlist. Entra ID will issue a token, and your app must reject it.
  4. Decode a token in a test environment and confirm ver is 2.0, iss contains the tid, and aud is your API's client ID.
  5. 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.
  • requestedAccessTokenVersion set to 2 for 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, iss bound to tid, aud, the tenant allowlist, and scp or roles.
  • Data keyed by tid and oid; no authorization on email, upn or preferred_username.
  • App instance property lock enabled with All properties.
  • Customer documentation covers assignment, revocation and the permissions you request.

References

Questions people ask

How do I make an Entra ID app registration multi-tenant?

In the Microsoft Entra admin center open the app registration, select Authentication, and under Supported account types choose Accounts in any organizational directory. The Application ID URI must be globally unique across all tenants, for example https://contoso.onmicrosoft.com/myapp, or the change fails.

How do customers grant admin consent to a multi-tenant app?

Send a customer administrator to the admin consent endpoint, https://login.microsoftonline.com/{tenant}/v2.0/adminconsent, with your client ID, the scopes (use /.default for application permissions) and a registered redirect URI. After approval, a service principal for your app exists in their tenant and no other users there are prompted for consent.

How do I restrict a multi-tenant app to paying customers only?

Entra ID will issue tokens to any tenant that consents, so your app must enforce its own allowlist. Validate the token's issuer and that the tid claim is a GUID matching the issuer, then reject any tid that isn't in your list of onboarded customer tenants.

What is app instance property lock in Microsoft Entra ID?

It locks sensitive properties of your app's service principals, such as credentials used for signing or verification and the token encryption key ID, so they can't be changed in the tenants where the app is installed. Microsoft enabled it by default for new applications from June 2026.

Microsoft Entra IDApp RegistrationOAuth 2.0Microsoft Graph
  1. Set Up Business Central OAuth Service-to-Service Access with Microsoft Entra

    Register a Microsoft Entra app, grant API.ReadWrite.All, add it on the Microsoft Entra Applications page and call Business Central APIs with client credentials, then fix 401 errors.

    Architecture10 min read
  2. Dataverse Application Users: Server-to-Server Auth with an Entra App

    Register a Microsoft Entra app, create a Dataverse application user with a least-privilege security role, get a client credentials token and call the Web API, then fix common errors.

    Architecture10 min read
  3. Fix Microsoft Graph 429 throttling with Retry-After, batching and delta

    Stop 429 Too Many Requests errors from Microsoft Graph: honour Retry-After, tune the SDK retry handler, batch correctly, cut request cost and replace polling with delta queries.

    Architecture13 min read