I have an ASP.NET Core MVC app using the Microsoft Identity Platform for authentication, currently set up for multi-tenancy with one app registration. I want to extend this to support two separate app registrations (for two different tenants) and allow users from both tenants to authenticate, selecting the appropriate app registration dynamically based on the tenant ID.
How can I configure Program.cs
to achieve this?
Current appsettings.json
:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "******.onmicrosoft.com",
"TenantId": "**********",
"ClientId": "**********",
"CallbackPath": "/signin-oidc",
"ClientSecret": "**********"
},
"AllowedTenants": [ "**********", "**********" ],
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "User.Read"
}
Desired appsettings.json
with two app registrations:
"AzureAdOne": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "******.onmicrosoft.com",
"TenantId": "**********",
"ClientId": "**********",
"CallbackPath": "/signin-oidc",
"ClientSecret": "**********"
},
"AzureAdTwo": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "******.onmicrosoft.com",
"TenantId": "**********",
"ClientId": "**********",
"CallbackPath": "/signin-oidc",
"ClientSecret": "**********"
},
"AllowedTenants": [ "**********", "**********" ],
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "User.Read"
}
current Program.cs
string[] initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
// Sign-in users with the Microsoft identity platform
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAd", options);
options.Events.OnTokenValidated = async context =>
{
string tenantId = context.SecurityToken.Claims.FirstOrDefault(x => x.Type == "tid" || x.Type == "http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
var allowedTenants = Configuration.GetSection("AllowedTenants").Get<string[]>().ToList();
if (string.IsNullOrWhiteSpace(tenantId) || !allowedTenants.Contains(tenantId))
throw new UnauthorizedAccessException("Unable to get tenantId from token or the tenant is not authorized.");
};
})
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
In the login partial, users can select their company:
<a class="dropdown-item" href="/MicrosoftIdentity/Account/SignIn?scheme=TenantIdOne">Company One</a>
<a class="dropdown-item" href="/MicrosoftIdentity/Account/SignIn?scheme=TenantIdTwo">Company Two</a>
What’s the best way to implement this in Program.cs to dynamically select the app registration based on the tenant?
Based on this article, one can answer this question as follows:
To configure an ASP.NET Core MVC app to support authentication with two different app registrations, you can define multiple OpenID Connect schemes. Here’s how to set it up in your Program.cs
, appsettings.json
, and controller.
Step 1: Configure Program.cs
In the Program.cs
, we set up two authentication schemes, appOne
and appTwo
, which correspond to two different app registrations (one for each tenant).
builder.Services.AddAuthentication(options =>
{
// Set Cookie as the default scheme, which manages user sessions within the app.
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// DefaultChallengeScheme indicates that we want to challenge users using OpenID Connect for login.
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie() // This handles user session management with cookies.
.AddOpenIdConnect("appOne", options =>
{
// Bind configuration settings for the first app registration.
builder.Configuration.GetSection("AppOne").Bind(options);
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// Use Authorization Code flow, which is the standard flow for server-side apps.
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
// Retrieves claims (user profile details) from the user info endpoint.
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name" // Specifies how to extract the user's name from claims.
};
options.MapInboundClaims = false; // Ensures custom claims mapping.
})
.AddOpenIdConnect("appTwo", options =>
{
// Bind configuration settings for the second app registration.
builder.Configuration.GetSection("AppTwo").Bind(options);
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
};
});
Step 2: Define Configuration in appsettings.json
In the appsettings.json
file, add separate sections for AppOne
and AppTwo
. This allows each OpenID Connect scheme to retrieve its specific settings, such as Authority
, ClientId
, ClientSecret
, and callback paths.
{
"AppOne": {
"Authority": "https://login.microsoftonline.com/{tenantIdOne}",
"ClientId": "your-client-id-for-app-one",
"ClientSecret": "your-client-secret-for-app-one",
"CallbackPath": "/signin-appOne",
"SignedOutCallbackPath": "/signout-callback-appOne"
},
"AppTwo": {
"Authority": "https://login.microsoftonline.com/{tenantIdTwo}",
"ClientId": "your-client-id-for-app-two",
"ClientSecret": "your-client-secret-for-app-two",
"CallbackPath": "/signin-appTwo",
"SignedOutCallbackPath": "/signout-callback-appTwo"
}
}
Step 3: Create Login Methods in the Controller
In the controller, create two separate actions for each app. Each action initiates the authentication challenge for the specified scheme, either appOne
or appTwo
.
[AllowAnonymous]
[HttpGet]
public ActionResult LoginAppOne(string returnUrl)
{
// Redirect to appOne's authentication challenge.
return Challenge(new AuthenticationProperties
{
RedirectUri = !string.IsNullOrEmpty(returnUrl) ? returnUrl : "/",
}, "appOne");
}
[AllowAnonymous]
[HttpGet]
public ActionResult LoginAppTwo(string returnUrl)
{
// Redirect to appTwo's authentication challenge.
return Challenge(new AuthenticationProperties
{
RedirectUri = !string.IsNullOrEmpty(returnUrl) ? returnUrl : "/"
}, "appTwo");
}
Step 4: Add Links in the View
To let users select the correct login option, add links in the view that call each login method:
<li class="nav-item">
<a class="nav-link text-dark" href="~/api/Account/LoginAppOne">Login with App One</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" href="~/api/Account/LoginAppTwo">Login with App Two</a>
</li>
How It Works:
appOne
and appTwo
) enable us to connect to different app registrations for each tenant.Challenge
triggers the OpenID Connect flow for each scheme. The returnUrl
parameter ensures users are redirected back to the original page after login.AddCookie
, the authentication
session is managed with cookies, so users stay logged in until they sign out or the session expires.This setup gives users the choice to log in through either app registration, dynamically adjusting based on their selection.