dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.19k stars 9.93k forks source link

Net 8 Blazor Web App (Interactive server w/ prerendering ) - multiple schemes doesn't work #57023

Open olejsc opened 1 month ago

olejsc commented 1 month ago

Is there an existing issue for this?

Describe the bug

The application I work with utilizes two seperate authentication schemes:

Regarding routing, this is the general structure:

I've followed the instructions here for configuring multiple policy schemes: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/policyschemes?view=aspnetcore-8.0

My configuration looks something like this:

Configuration

Authentication:

services.AddAuthentication(opts =>
            {
                opts.DefaultScheme = "multischeme";
            })
            .AddPolicyScheme("multischeme", "Internal or External", opts =>
            {
                opts.ForwardDefaultSelector = ctx =>
                {
                    if (ctx.Request.Path.StartsWithSegments("/auth/internal") || ctx.Request.Path.StartsWithSegments("/internal/somepage") )
                    {
                       return "InternalScheme";
                    }
                    // Does not allowed undefined, so we have to return one of the schemes
                    return "ExternalScheme";
                };
            })
           .AddOpenIdConnect("InternalScheme", "Internal auth scheme", options =>
            {
                options.SignInScheme = "InternalCookieScheme";   // <---- CUSTOM COOKIE SCHEME
                options.Authority = "xxxxxxxxx";
                options.ClientId = "xxxxxxxxxxxxx";
                options.ClientSecret = "xxxxxxxxxx";
                options.CallbackPath = new PathString("/xxxx/return");
                options.ResponseType = "code";
                options.RequireHttpsMetadata = true;
                options.Events = new OpenIdConnectEvents
                {
                    OnTokenValidated = ctx =>
                    {
                        // abbreviated, reading some claims, adding some custom claims to a new custom identity
                        // custom claim is used to authorize internal users
                        return Task.CompletedTask;
                    }
                };
            })           
         .AddCookie("InternalCookieScheme", "Internal auth cookie scheme", options =>
            {
                options.Cookie.Name = "InternalAuthCookie";
                // abbreviated...
            })            
            .AddOpenIdConnect("ExternalScheme", "External auth scheme", options =>
            {
                options.SignInScheme = "ExternalCookieScheme"; // <---- CUSTOM COOKIE SCHEME 
                options.Authority = "XXXXXXXXXXXXX";
                options.ClientId = "xxxxxx";
                options.ClientSecret = "xxxxx";
                options.CallbackPath = new PathString("/xxxxxx/callback");
                options.ResponseType = "code";
                options.Scope.Clear();
                options.Scope.Add("openid");
                options.RequireHttpsMetadata = true;
                options.Events = new OpenIdConnectEvents
                {
                    OnTokenValidated = async ctx =>
                    {
                        // abbreviated, reading some claims, adding some custom claims to a new custom identity
                        // custom claim is used to authorize external users
                        return Task.CompletedTask;
                    }
                };
            })
            .AddCookie("ExternalCookieScheme", "External auth cookie scheme", options =>
            {
                options.Cookie.Name = "ExternalAuthCookie";
                // abbreviated...
            });

Authorization:

services.AddAuthorization(config =>
{
    // INTERNAL user policies
    config.AddPolicy(PolicyNames.IS_INTERNAL_USER,
        policy =>
        {
            policy.AddRequirements(new IsInternalUserRequirement());
        });
    // EXTERNAL user policies
    config.AddPolicy(PolicyNames.IS_EXTERNAL_USER,
        policy =>
        {
            policy.AddRequirements(new IsExternalUserRequirement());
        });
});

In summary, I have one combined scheme ("Multischeme") that wraps around "InternalScheme" and "Externalscheme", which both respectively connects to "InternalCookieScheme" and "ExternalCookieScheme".

Endpoints

In addition, i have 2 controller endpoints for each specific scheme:

image

As long as "ExternalScheme" is the default scheme, I can sign in / sign-out with the external OIDC provider and authorize just fine with it. However, I cant authorize with internal user. I manage to sign in with it, Cookie gets set with values, but under ...context.User.Identittesthere is no user with those claims available in authorization.

Appreciate any help with this. I'm stomped! 😣

Expected Behavior

The claims from both schemes should be available when signing in. Authhandlers should be able to find the claims for both signed in schemes when executing authorization handlers & requirements. Only the default identity provided from the default scheme seems to be available when dooing authorization (both in authorizationhandlers, but also in controllers!)

Steps To Reproduce

Unfortunately i'm not at liberty to expose the external providers provided. I hope the code i've provided will be sufficient to reproduce the issue.

Exceptions (if any)

None.

.NET Version

8.0.303

Anything else?

Another person on stackover flow seems to have a similar issue (unresolved). He only used a single oidc, but seemed to want another cookie. https://stackoverflow.com/questions/78533634/asp-net-core-blazor-multiple-authentication-schemes-oidc-custom-cookies

dotnet info output: image

olejsc commented 1 month ago

Some things i've tried:

olejsc commented 1 month ago

This seems to be releated to the part of the issue described here at first glance:

50122

Essentually, whatever is default will make any 2nd scheme not work.

olejsc commented 1 month ago

I managed to find a workaround by using this in program.cs:

app.MapRazorComponents<App>()
    .RequireAuthorization(new AuthorizeAttribute() { AuthenticationSchemes = "externalScheme" })
    .RequireAuthorization(new AuthorizeAttribute() { AuthenticationSchemes = ""internalScheme"" })
    .AllowAnonymous()
    .AddInteractiveServerRenderMode();

and in serviceregistration for authentication:

            services.AddAuthentication(opts =>
            {
                opts.DefaultChallengeScheme = "undefined"; < -- This scheme doesn't actually exist.
                opts.DefaultScheme = "default";     < -- This scheme doesn't actually exist.
            })
            // internal & external scheme registered as defined, we dont register "multischeme"

This is nowhere near what the documentation for multiple schemes state. I'm not even sure this is a good approach on how to do it, or if it has any serious drawbacks. Auth handlers seems to fire for every blazor request now (blazor.web.js for example), but since we have allowanonymous it passes.

MackinnonBuck commented 1 month ago

Let's use this issue to track updating the docs to clarify the behavior in this area.

In .NET 10, we could consider making the experience better by throwing a descriptive error message for this case.

olejsc commented 1 month ago

@MackinnonBuck What exsactly would that error message be ? Isn't multiple auth schemes intended to be supported ?

olejsc commented 1 month ago

it turns out this part isn't nescesarry:

            services.AddAuthentication(opts =>
            {
                opts.DefaultChallengeScheme = "undefined"; < -- This scheme doesn't actually exist.
                opts.DefaultScheme = "default";     < -- This scheme doesn't actually exist.
            })

You can just get away with adding empty authenticaiton configuration + the internal/external scheme and their respective cookies.

            services.AddAuthentication()
                        .AddOpenIdConnect("external"...)
                        .AddCookie("externalcookie"...)
                        .AddOpenIdConnect("internal"...)
                        .AddCookie("internalCookie"...)