Multi-tenant Azure AD applications sound simple on paper. You register an app once, mark it multi-tenant, and any organisation using Azure AD can start consuming it. In practice, the real work begins once you decide who should actually be allowed to call your API. This post walks through a working setup where a multi-tenant Azure AD API accepts delegated access tokens issued from several different tenants, and how you validate those tokens so that only tenants you explicitly trust get through.
The complete sample used in this walkthrough is available on Damien Bod’s GitHub, linked in the reference section at the end of this article.
Setting up the API app registration
Start by creating the Azure App registration that exposes your API scope. This registration must be marked multi-tenant so that service principals from other tenants can consume it. Configure the app to issue only v2 tokens, and keep in mind this design produces delegated access tokens only. It does not cover app-only, client-credentials style calls.

The screenshot shows the Expose an API blade with a custom scope named access_as_user, created under the Application ID URI. Client applications request this scope when acquiring a token for your API. Exposing a scope here only tells Azure AD what permission is available. It does not tell your API which tenants should be trusted to use it, and that decision has to be enforced explicitly inside your API code.
Implementing the API
With the API registration in place, the ASP.NET Core project needs to validate the incoming access token correctly and layer additional authorization checks on top. Three things matter here: which issuers are trusted, which client application is allowed to call the API, and whether the token is a delegated token rather than an app-only token.
The configuration below restricts the API to a fixed metadata address, sets the expected audience, and pins the accepted issuer list to only the tenants you want to support. That last part, ValidIssuers, is the single most important line in a multi-tenant setup.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.MetadataAddress = aadMetadataAddress;
//options.Authority = issuert1;
options.Audience = aud;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidAudiences = new List<string> { aud },
ValidIssuers = new List<string> { issuert1 }
};
});
services.AddSingleton<IAuthorizationHandler, ValidTenantsAndClientsHandler>();
services.AddAuthorization(policies =>
{
policies.AddPolicy("ValidTenantsAndClients", p =>
{
// only delegated, trusted, known clients allowed to use the API
p.Requirements.Add(new ValidTenantsAndClientsRequirement());
// validate id of application for which the token was created
p.RequireClaim("azp", azpClientId);
// client secret = 1, 2 if certificate is used
p.RequireClaim("azpacr", "1");
});
});
services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
Note that the issuer list is hardcoded to known tenant IDs rather than left open. Azure AD’s common multi-tenant endpoint will happily issue tokens for any tenant that has registered the app, so if you skip pinning ValidIssuers, your API implicitly trusts every Azure AD tenant in the world, as long as the audience matches. This is the most common mistake when standing up a multi-tenant API: developers get authentication working, move on, and forget that authentication is not the same thing as authorization.
The authorization policy adds a custom requirement, ValidTenantsAndClientsRequirement, plus two claim checks. The azp claim confirms which client application requested the token. The azpacr claim confirms the client authenticated using a secret (value 1) rather than a certificate. Swap that check if your client uses certificate-based authentication instead of a secret.
The custom authorization handler below inspects the scope claim on the token. Delegated tokens carry a scope claim, a space-separated list of permission strings. Application-only tokens carry a roles claim instead and have no scope claim at all. This handler explicitly looks for access_as_user inside scope, and only then marks the requirement as satisfied.
public class ValidTenantsAndClientsHandler
: AuthorizationHandler<ValidTenantsAndClientsRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ValidTenantsAndClientsRequirement requirement)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (requirement == null)
throw new ArgumentNullException(nameof(requirement));
var scopeClaim = context.User.Claims.FirstOrDefault(t => t.Type == "scope");
if (scopeClaim != null)
{
var scopes = scopeClaim.Value.Split(" ",
StringSplitOptions.RemoveEmptyEntries);
if (scopes.Any(t => t == "access_as_user"))
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
This is a deliberate defense-in-depth measure. Even if a caller somehow gets past the issuer and audience checks, this handler blocks any token that isn’t a genuine delegated user token carrying the expected scope. A common pitfall is trusting the audience and issuer checks alone and skipping this scope validation, which then lets through any correctly issued token, including ones meant for an entirely different purpose, as long as the audience happens to match.
Creating the service principal in other tenants
Once the API app registration exists, every tenant that wants to consume the API needs a service principal for it created locally, inside that tenant. This step is easy to overlook: your app registration lives in your home tenant, but Azure AD needs a service principal object present in every consuming tenant before that tenant’s admins or users can consent to it.
Run the following PowerShell against the target tenant using the AzureAD module. Connect-AzureAD must authenticate into the target tenant, not your own tenant, otherwise the service principal ends up in the wrong place.
Connect-AzureAD -TenantId '<target-tenant-id>'
New-AzureADServicePrincipal -AppId 'AppId-from-multi-tenant-api'
Once this command completes, the multi-tenant API application shows up in that tenant’s Enterprise applications list. Administrators can now grant it consent, exactly as they would for any other third-party application registered in their organisation.
Granting consent in the target tenant
The newly created service principal appears under Enterprise applications in the target tenant. From there, a tenant administrator can review the requested permissions and grant consent on behalf of the whole organisation, which is what most production B2B scenarios need, since asking every individual user to consent separately does not scale well.
Using the API and the consent experience
When a user signs into the client application and it requests a token for the multi-tenant API, Azure AD shows a consent screen. What that screen looks like, and whether it appears at all, depends on the tenant’s consent policies and on whether the signed-in user is an administrator.

The screenshot shows a typical admin consent prompt. The app is flagged as ‘not published by Microsoft’ since it is not a verified publisher, and the admin ticks ‘Consent on behalf of your organization’ to grant access for every user in the tenant in a single step. If an organisation has restricted user consent, a sensible default for production tenants, ordinary users won’t see an option to consent at all. They will instead see a message asking them to request admin approval, so plan for that step in your rollout process rather than assuming every user can self-serve.
Once consent has been granted, you can confirm it by opening the API permissions blade on the Enterprise application in the target tenant.

The access_as_user delegated permission now shows up under ‘Other permissions granted’, confirming this tenant is authorized to call the API on behalf of its users. Checking this blade is a useful first troubleshooting step: if users start hitting 403 errors calling the API, look here before digging into token claims or redeploying anything.
Trade-offs and production considerations
This explicit tenant-and-client model gives you fine-grained control, at the cost of extra operational work. Every new consuming tenant needs its own service principal creation step and its own admin consent, and the API’s ValidIssuers list needs updating whenever a new tenant is onboarded. That is a manual, configuration-driven scaling limit worth planning for if you expect dozens of tenant integrations rather than a handful of trusted partners.
It is worth comparing this approach with Microsoft Entra External ID, formerly Azure AD B2C, which is built around user-facing multi-tenant scenarios and handles consent plumbing quite differently. If your scenario is closer to B2C, external end users signing up directly, rather than B2B, a fixed set of partner organisations, Entra External ID is usually a better fit than bolting explicit tenant validation onto a classic multi-tenant Azure AD app.
One more thing worth watching: pinning ValidIssuers to a fixed list means the API needs a config reload or redeploy whenever a trusted tenant is added. Some teams instead validate the tid (tenant ID) claim against a database or configuration store at runtime, which avoids a redeploy but adds a lookup on every request. Pick whichever trade-off suits how often your tenant list actually changes and how much request latency you can afford to spend on that check.
Leave a Reply