The article shows how an ASP.NET Core application could implement a sign in and a sign out with two different Azure App registrations which could also be implemented using separate identity providers (tenants). The user of the application can decide to authenticate against either one of the Azure AD clients. The clients can also be deployed on separate Azure Active directories. Separate authentication schemes are used for both of the clients. Each client requires a scheme for the Open ID Connect sign in and the cookie session. The Azure AD client authentication is implemented using Microsoft.Identity.Web.
Code: https://github.com/damienbod/AspNetCore6Experiments
The clients are setup to use a non default Open ID Connect scheme and also a non default cookie scheme. After a successful authentication, the OnTokenValidated event is used to sign into the default cookie scheme using the claims principal returned from the Azure AD client. “t1” is used for the Open ID Connect scheme and “cookiet1” is used for the second scheme. No default schemes are defined. The second Azure App Registration client configuration is setup in the same way.
services.AddAuthentication()
.AddMicrosoftIdentityWebApp(
Configuration.GetSection("AzureAdT1"), "t1", "cookiet1");
services.Configure<OpenIdConnectOptions>("t1", options =>
{
var existingOnTokenValidatedHandler
= options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
await existingOnTokenValidatedHandler(context);
await context.HttpContext.SignInAsync(
CookieAuthenticationDefaults
.AuthenticationScheme, context.Principal);
};
});
services.AddAuthentication()
.AddMicrosoftIdentityWebApp(
Configuration.GetSection("AzureAdT2"), "t2", "cookiet2");
services.Configure<OpenIdConnectOptions>("t2", options =>
{
var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
await existingOnTokenValidatedHandler(context);
await context.HttpContext.SignInAsync(
CookieAuthenticationDefaults
.AuthenticationScheme, context.Principal);
};
});
The AddAuthorization is used in a standard way and no default policy is defined. We would like the user to have the possibility to choose against what tenant and client to authenticate.
services.AddAuthorization();
services.AddRazorPages()
.AddMvcOptions(options => { })
.AddMicrosoftIdentityUI();
A third default scheme is added to keep the session after a successful authentication using the client schemes which authenticated. The identity is signed into this scheme after a successfully Azure AD authentication. The SignInAsync method is used for this in the OnTokenValidated event.
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
The Configure method is setup in a standard way.
public void Configure(IApplicationBuilder app)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
});
}
The sign in and the sign out needs custom implementations. The SignInT1 method is used to authenticate using the first client and the SignInT2 is used for the second. This can be called from the Razor page view. The CustomSignOut is used to sign out the correct schemes and redirect to the Azure AD endsession endpoint. The CustomSignOut method uses the clientId of the Azure AD configuration to sign out the correct session. This value can be read using the aud claim.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
namespace AspNetCoreRazorMultiClients
{
[AllowAnonymous]
[Route("[controller]")]
public class CustomAccountController : Controller
{
private readonly IConfiguration _configuration;
public CustomAccountController(IConfiguration configuration)
{
_configuration = configuration;
}
[HttpGet("SignInT1")]
public IActionResult SignInT1([FromQuery] string redirectUri)
{
var scheme = "t1";
string redirect;
if (!string.IsNullOrEmpty(redirectUri) && Url.IsLocalUrl(redirectUri))
{
redirect = redirectUri;
}
else
{
redirect = Url.Content("~/")!;
}
return Challenge(new AuthenticationProperties { RedirectUri = redirect }, scheme);
}
[HttpGet("SignInT2")]
public IActionResult SignInT2([FromQuery] string redirectUri)
{
var scheme = "t2";
string redirect;
if (!string.IsNullOrEmpty(redirectUri) && Url.IsLocalUrl(redirectUri))
{
redirect = redirectUri;
}
else
{
redirect = Url.Content("~/")!;
}
return Challenge(new AuthenticationProperties { RedirectUri = redirect }, scheme);
}
[HttpGet("CustomSignOut")]
public async Task<IActionResult> CustomSignOut()
{
var aud = HttpContext.User.FindFirst("aud");
if (aud.Value == _configuration["AzureAdT1:ClientId"])
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync("cookiet1");
var authSignOut = new AuthenticationProperties
{
RedirectUri = "https://localhost:44348/SignoutCallbackOidc"
};
return SignOut(authSignOut, "t1");
}
else
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync("cookiet2");
var authSignOut = new AuthenticationProperties
{
RedirectUri = "https://localhost:44348/SignoutCallbackOidc"
};
return SignOut(authSignOut, "t2");
}
}
}
}
The _LoginPartial.cshtml Razor view can use the CustomAccount controller method to sign in or sign out. The available clients can be selected in a drop down control.
<ul class="navbar-nav">
@if (User.Identity.IsAuthenticated)
{
<li class="nav-item">
<span class="navbar-text text-dark">Hello @User.Identity.Name!</span>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-controller="CustomAccount" asp-action="CustomSignOut">Sign out</a>
</li>
}
else
{
<li>
<div class="main-menu">
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownLangButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
sign-in
</button>
<div class="dropdown-menu" aria-labelledby="dropdownLangButton">
<a class="dropdown-item" asp-controller="CustomAccount" asp-action="SignInT1" >t1</a>
<a class="dropdown-item" asp-controller="CustomAccount" asp-action="SignInT2">t2</a>
</div>
</div>
</div>
</li>
}
</ul>
The app.settings have the Azure AD settings for each client as required.
{
"AzureAdT1": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "damienbodhotmail.onmicrosoft.com",
"TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
"ClientId": "46d2f651-813a-4b5c-8a43-63abcb4f692c",
"CallbackPath": "/signin-oidc/t1",
"SignedOutCallbackPath ": "/SignoutCallbackOidc"
// "ClientSecret": "add secret to the user secrets"
},
"AzureAdT2": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "damienbodhotmail.onmicrosoft.com",
"TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
"ClientId": "8e2b45c2-cad0-43c3-8af2-b32b73de30e4",
"CallbackPath": "/signin-oidc/t2",
"SignedOutCallbackPath ": "/SignoutCallbackOidc"
// "ClientSecret": "add secret to the user secrets"
},
When the application is started, the user can login using any client as required.

This works really good, if you don’t know which tenant is your default scheme. If you always use a default scheme with one tenant default, then you can use the multiple-authentication-schemes example like defined in the Microsoft.Identity.Web docs.
Links:
https://github.com/AzureAD/microsoft-identity-web/wiki/multiple-authentication-schemes
https://github.com/AzureAD/microsoft-identity-web/wiki/customization#openidconnectoptions
https://github.com/AzureAD/microsoft-identity-web
https://docs.microsoft.com/en-us/aspnet/core/security/authentication