Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
1 task done
olejsc opened this issue Jul 26, 2024 · 1 comment
Open
1 task done

Comments

@olejsc
Copy link

olejsc commented Jul 26, 2024

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

The application I work with utilizes two seperate authentication schemes:

  • Microsoft entra id for internal users + Cookie
  • An external OIDC identity provider for external users + Cookie

Regarding routing, this is the general structure:

  • In addition, any blazor page whose path starts with "/internal" is authorized on page with custom authorization policies to check for specific claims, for example ...RequireClaim(ctx=> ctx.FindFirst(InternalUserIdClaimType) is not null)
  • Same goes for internal users.
  • For authenticated users, they simply dont use the same paths/route in the application.
    However, they share the same entry point, route ("/"), which is the landing page for the application. This is configured with [AllowAnonymous].

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:

  • /auth/external/login
  • /auth/external/signout
  • /auth/internal/login
  • /auth/internal/signout

Main configuration (program.cs):

  • Auth handlers are registered earlier
  • Controllers are added
    image

Routes.Razor:

  • Has AuthorizeRouteView
    image

App.Razor:

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
Copy link
Author

olejsc commented Jul 26, 2024

Some things i've tried:

  • That both auth schemes map to same cookie.
  • To use the .RequireAuthorization (config=> config.addPolicySchemes(Multischeme) for the app. I've found that this creates other issues..
  • Disabled one scheme entirely (works fine with one auth scheme + one cookie scheme just fine).
  • Attempted to do `context.SignInAsync("NON-defaultscheme") in controllers as I cant get the auth state available for whichever schema is not defaulted. so, for internalSchema: auth/internal/login --> Cant find claims/ identity to the internal user since externalscheme is default....
  • Swapped the default scheme to ensure claims actually gets set and is configured right with respect to the identity providers. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant