I am trying to implement multiple authentication schemes in Blazor WASM. I want my users to be able to login using either Azure AD or Azure B2C and I don't want to use Custom User Flows in Azure B2C as I have found that to be very complex and error-prone to configure. I would like to have 2 x Login buttons ie. Login AD and Login B2C.
Each button on its own is simple to implement using MSAL, but I am struggling to get both working. In Microsoft.Web.Identity, we have the option of defining multiple Authentication Schemes. However, I don't see anything like that in WASM / MSAL.
I have been working on the following concept adjusting the authentication urls for each scheme.
LoginDisplay.razor
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation
<AuthorizeView>
<Authorized>
Hello, @context.User.Identity?.Name!
<button class="nav-link btn btn-link" @onclick="BeginLogOut">Log out</button>
</Authorized>
<NotAuthorized>
<a href="authenticationAD/login">Log in AD</a>
<a href="authenticationB2C/login">Log in B2C</a>
</NotAuthorized>
</AuthorizeView>
@code{
public void BeginLogOut()
{
Navigation.NavigateToLogout("authenticationAD/logout");
}
}
AuthenticationAD.razor
@page "/authenticationAD/{action}" /*NOTE Adjusted url*/
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" >
</RemoteAuthenticatorView>
@code{
[Parameter] public string? Action { get; set; }
}
AuthenticationB2C.razor
@page "/authenticationB2C/{action}" /*NOTE Adjusted url*/
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" >
</RemoteAuthenticatorView>
@code{
[Parameter] public string? Action { get; set; }
}
Program.cs
var builder = WebAssemblyHostBuilder.CreateDefault(args);
............
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureB2C", options.ProviderOptions.Authentication);
options.ProviderOptions.Authentication.PostLogoutRedirectUri = "authenticationB2C/logout-callback";
options.ProviderOptions.Authentication.RedirectUri = "authenticationB2C/login-callback";
var webApiScopes = builder.Configuration["AzureB2C:WebApiScopes"];
var webApiScopesArr = webApiScopes.Split(" ");
foreach (var scope in webApiScopesArr)
{
options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
}
var appPaths = options.AuthenticationPaths;
appPaths.LogInCallbackPath = "authenticationB2C/login-callback";
appPaths.LogInFailedPath = "authenticationB2C/login-failed";
appPaths.LogInPath = "authenticationB2C/login";
appPaths.LogOutCallbackPath = "authenticationB2C/logout-callback";
appPaths.LogOutFailedPath = "authenticationB2C/logout-failed";
appPaths.LogOutPath = "authenticationB2C/logout";
appPaths.LogOutSucceededPath = "authenticationB2C/logged-out";
appPaths.ProfilePath = "authenticationB2C/profile";
appPaths.RegisterPath = "authenticationB2C/register";
});
builder.Services.AddOidcAuthentication(options => //THIS CODE DOES NOT RUN
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions);
options.ProviderOptions.PostLogoutRedirectUri = "authenticationAD/logout-callback";
options.ProviderOptions.RedirectUri = "authenticationAD/login-callback";
options.ProviderOptions.ResponseType = "code";
var webApiScopes = builder.Configuration["AzureAd:WebApiScopes"];
var webApiScopesArr = webApiScopes.Split(" ");
foreach (var scope in webApiScopesArr)
{
options.ProviderOptions.DefaultScopes.Add(scope);
}
var appPaths = options.AuthenticationPaths;
appPaths.LogInCallbackPath = "authenticationAD/login-callback";
appPaths.LogInFailedPath = "authenticationAD/login-failed";
appPaths.LogInPath = "authenticationAD/login";
appPaths.LogOutCallbackPath = "authenticationAD/logout-callback";
appPaths.LogOutFailedPath = "authenticationAD/logout-failed";
appPaths.LogOutPath = "authenticationAD/logout";
appPaths.LogOutSucceededPath = "authenticationAD/logged-out";
appPaths.ProfilePath = "authenticationAD/profile";
appPaths.RegisterPath = "authenticationAD/register";
});
await builder.Build().RunAsync();
This works as far as pressing the Login Button routes me to the correct authenticationXX.razor view.
The issue that I'm facing is that the second .AddXXXAuthentication does not run, so the OAuth settings for the second statement are not set. If I change the order, it is always the second statement that doesn't run. Authentication works fine based upon the first statement.
I tried using 2 off .AddMSALAuthentication statements and in that case, both statements did run. However, the ProviderOptions from appsettings.json were just over-written in the second statement. ie. it didn't create two instances of the MSAL Authentication scheme.
I know that I can hand-craft the url strings for each of the oauth steps using tags in the < RemoteAuthenticationView > component, but I was hoping to find a way to use native libraries where-ever possible to reduce the risk of introducing a security weakness in my application.
Has anyone else had experience with this scenario and can point me to some documentation / an example of how it can be done?