Search code examples
asp.netauthenticationblazorasp.net-identity

Combine auth cookies and bearer token at the same time


I am trying to implement auth so my backend can serve to WASM client (hosted) and later for MAUI Blazor. I am using identity with local database and want to use cookies for WASM and bearer token for MAUI Blazor hybrid app. I read lot of resources and watched lot of videos but cannot wrap my head around how to do it and I am not expert especially in auth setup.

My first question is why sometimes those lines are used and sometimes not? (see my example for working combine approach)

builder.Services.AddAuthentication();
...
app.UseAuthentication();
app.UseAuthorization();

Second question is how to implement system so both approaches are supported at the same time? First approach is basic Blazor web app (hosting WASM) with boilerplate:

...

builder.Services.AddAuthorization();
builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = IdentityConstants.ApplicationScheme;
        options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddIdentityCookies();
...

builder.Services.AddIdentityCore<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();

I tried multiple combination with this base (I wont share all combination I tried but you can imagine playing with those lines) but it does not work. Its always missing some scheme with either combination.

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = IdentityConstants.ApplicationScheme;
    options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddCookie(IdentityConstants.ApplicationScheme)
.AddBearerToken(IdentityConstants.BearerScheme);

Second approach I found is by using by replacing WHOLE first approach with only 2 lines of code

builder.Services.AddAuthorization();
builder.Services.AddIdentityApiEndpoints<ApplicationUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

This approach is working for both cookie and bearer token at the same time but I cannot add roles ( .AddRoles() and .AddSignInManager()) and whatsmore I dont know how secure is this approach because I cannot find more info about it.

Last question is what this line does? Because its working without it. .AddApiEndpoints()

So its all confusing to me and would like to learn and understand how this should work and what is the best approach. One more thing, I want to use identity with local database + external login so please dont suggest microsoft entra or other solutions.

Thanks.


Solution

  • If you want to use both bearer and cookie authentication you have to define a custom scheme with a handler that can handle both types of authentication.

    internal sealed class CompositeIdentityHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder)
        : SignInAuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
    {
        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            var bearerResult = await Context.AuthenticateAsync(IdentityConstants.BearerScheme);
            if (!bearerResult.None)
            {
                return bearerResult;
            }
    
            return await Context.AuthenticateAsync(IdentityConstants.ApplicationScheme);
        }
    
        protected override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties)
        {
            throw new NotImplementedException();
        }
    
        protected override Task HandleSignOutAsync(AuthenticationProperties? properties)
        {
            throw new NotImplementedException();
        }
    }
    

    and register it like this:

    private static IServiceCollection AddIdentityServices(this IServiceCollection services, Action<IdentityOptions> configure)
    {
        services
            .AddAuthentication(CUSTOM_SCHEME_NAME)
            .AddScheme<AuthenticationSchemeOptions, CompositeIdentityHandler>(CUSTOM_SCHEME_NAME, null, compositeOptions =>
            {
                compositeOptions.ForwardDefault = IdentityConstants.BearerScheme;
                compositeOptions.ForwardAuthenticate = CUSTOM_SCHEME_NAME;
            })
            .AddBearerToken(IdentityConstants.BearerScheme)
            .AddIdentityCookies();
        services.AddAuthorization();
    
        services.AddIdentityCore<ApplicationUser>(configure)
            .AddRoles<ApplicationRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddSignInManager();
    
        return services;
    }
    

    .AddIdentityApiEndpoints<ApplicationUser>() also adds a CompositeIdentityHandler similar to the one above (AddIdentityApiEndpoints github) and is required for app.MapIdentityApi<IdentityUser>(). Thats why it works when you add that line of code. More about the MapIdentityApi can be found here MapIdentityApi