asp.net-coreblazormicrosoft-entra-idmicrosoft-identity-webazure-entra-id

Multiple authentication methods in Blazor 8 won't work


I try to add an authentication method in Blazor 8 but only the last method will work.

With the follwing code I am able to login with cookie:

...
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(opts =>
    {
        builder.Configuration.GetSection("AzureAd").Bind(opts);
    });
builder.Services.AddAuthentication(options =>
            {
                options.DefaultScheme = IdentityConstants.ApplicationScheme;
                options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
                options.DefaultSignOutScheme = IdentityConstants.ExternalScheme;
            }).AddIdentityCookies();
...

I can also see to button "OpenIdConnect". If I click the button, I can see it will try to login, but will redirect back to login. At the console I can see the log

IDX21310: OpenIdConnectProtocolValidationContext.ProtocolMessage.AccessToken is null, there is no 'token' in the OpenIdConnect Response to validate.

enter image description here

If I change my code like this:

...
 builder.Services.AddAuthentication(options =>
             {
                 options.DefaultScheme = IdentityConstants.ApplicationScheme;
                 options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
                 options.DefaultSignOutScheme = IdentityConstants.ExternalScheme;
             }).AddIdentityCookies();
  builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
     .AddMicrosoftIdentityWebApp(opts =>
     {
         builder.Configuration.GetSection("AzureAd").Bind(opts);
     });
...

I am able to login with the Microsoft Account, but now I can't login with credentials by Username and Password. Now I only will be redirected to "home" but I am not logged in.

What am I doing wrong?

I like to login both with Microsoft Identity and over credentials by JWT or Cookies.


Solution

  • You are using the built-in "Asp.Net core Identity" login page. I suppose you create from the Blazor Web App template with "individual" authentication. Within this template, there is built-in way to add external login provider.

    1. Install package Microsoft.AspNetCore.Authentication.MicrosoftAccount
    2. Just add following code to program.cs
    builder.Services.AddAuthentication()
       .AddMicrosoftAccount(microsoftOptions =>
       {
           microsoftOptions.ClientId = "xxxxxxxx";
           microsoftOptions.ClientSecret = "xxxxxx";
       });
    

    Then when you run the project, there will be a button name "microsoft". After you finished login, your "asp.net core identity" let you confirm to register an account to your local database. (note that you should have use "update-database" to initialize)

    enter image description here

    The built-in way doesn't really use the entra ID flow. It just register (or copy) that account to local database and coninue using the "asp.net core identity" cookie scheme to authenticate. So the claims you set in Azure won't actually be copied. You may need to create claims/roles etc at your local database again.


    If the above built-in way doesn't meet your requirement. You will need to modify some codes.

    First of all, the key issue is the options.DefaultAuthenticateScheme. That makes only 1 scheme can take effect.

    Even you didn't specify it. But
    builder.Services.AddAuthentication(options =>{options.DefaultScheme =
    and
    builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    will both re-configure the "DefaultScheme" (which include "options.DefaultAuthenticateScheme").

    So you could create a "policy scheme" to determine, if you log in with "local asp.net core identity" this will generate a cookie named ".AspNetCore.Identity.Application" , then return the local scheme for use. If not, return the "EntraID" scheme.

    builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = "both";
    })
        .AddPolicyScheme("both", "Identity or EntraID", options =>
        {
                options.ForwardDefaultSelector = context =>
                {
                    if (context.Request.Cookies.ContainsKey(".AspNetCore.Identity.Application"))
                    {
                        return IdentityConstants.ApplicationScheme;
                       
                    }
                    else
                    {
                        return OpenIdConnectDefaults.AuthenticationScheme;
                    }
                };
        });
    

    After adding this, if go to the login page, you will find 2 buttons, because the signinManager detects 2 external schemes.

    enter image description here

    To solve this, we can do some modification to the ExternalLoginPicker.razor.

    enter image description here

    ...
                    @foreach (var provider in externalLogins)
                    {
                        if (provider.Name != "both")
                        {
                            <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                        }                  
                    }
    ...
    

    You may need add another logout button in Navmenu.razor

    ...
                        <div class="nav-item px-3">
                            <form action="/EntraLogout" method="get">
                                <AntiforgeryToken />
                                <button type="submit" class="nav-link">
                                    <span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true"></span> Logout for EntraID
                                </button>
                            </form>
                        </div>
    ...
    

    Then in program.cs

    app.MapGet("/EntraLogout", async (HttpContext httpContext) => { await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);  await httpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); });