Aspnetcore: [question] Is it possible to add AzureAD *and* AzureAD B2C authentication to the same web app?

Created on 8 Jul 2019  路  17Comments  路  Source: dotnet/aspnetcore

I've been struggling with this scenario for a bit and I'm starting to believe it might not be supported (at least in 2.2?), so I wanted to see if anyone can confirm that or point me on the right direction.

What I want to achieve is to allow different tenants in my multi-tenant application, to use different authentication providers (AzureAD for one, AzureAD B2C for another).

I'm registering separate schemes (Authentication, OpenID, Cookie, etc) for each tenant, like this (and similar for AzureAD):

var azureAdB2CConfigs = Configuration.GetSection("AzureAD_B2C").GetChildren();
foreach (var tenantSpecificConfig in azureAdB2CConfigs)
{
    var tenantName = tenantSpecificConfig.Key;
    services.AddAuthentication().AddAzureADB2C(
        Utils.GetTenantScopedScheme(AzureADB2CDefaults.AuthenticationScheme, tenantName),
        Utils.GetTenantScopedScheme(AzureADB2CDefaults.OpenIdScheme, tenantName),
        Utils.GetTenantScopedScheme(AzureADB2CDefaults.CookieScheme, tenantName),
        Utils.GetTenantScopedScheme(AzureADB2CDefaults.DisplayName, tenantName),
        options => tenantSpecificConfig.Bind(options));
}

If I only call AddAzureAD() or AddAzureADB2C() on the authentication builder, things work as expected. But if I call both, I start getting the error below when I make any request to my application (e.g. even just for the favicon). Of note, it doesn't happen immediately at startup, but only when I make the first request:

image

The order in which I register the AzureAD and AzureADB2C providers doesn't seem to matter.

I looked at the code for AddAzureAD() and AddAzureADB2C (for release/2.2) and I started suspecting that the issue comes from the fact that they both try to register singleton configurators for OpenIdConnectOptions (here and here), so maybe one of them isn't getting that object configured as it expects it to be. In master, those changed from TryAddSingleton to TryAddEnumerable (here and here). While I don't yet understand how each provider would get the correct OpenIdConnectOptions in this scenario, based on #4635 I imagine it might help...? @javiercn I hope it's not too intruding to tag you directly, but since you found the problem for #4635 and I think this might be similar I thought it might be useful to include you.

For completeness, this is the relevant section of my appsettings.json (which I believe is fine, because as I said, either AzureAD or AzureADB2C on its own works correctly).

"AzureAD": {
    "<tenant1>": {
        "Instance": "https://login.microsoftonline.com/",
        "Domain": "<tenant1-domain>.onmicrosoft.com",
        "TenantId": "organizations",
        "ClientId": "<tenant1-clientid>",
        "CallbackPath": "/signin-oidc"
    } 
},
"AzureAD_B2C": {
    "<tenant2>": {
        "Instance": "https://<tenant2-domain>.b2clogin.com/",
        "ClientId": "<tenant2-clientid>",
        "ClientSecret": "<secret>",
        "CallbackPath": "/b2c-signin-oidc",
        "Domain": "<tenant2-domain>.onmicrosoft.com",
        "SignUpSignInPolicyId": "B2C_1_SignUpOrSignIn"
    } 
}
area-security

Most helpful comment

Sure, give me a couple of days and I should be able to clean up my app to work as a generic example.

All 17 comments

@jmprieur is this doable?

@blowdart : I don't think it is. I need to try out

I think this is a known issue in 2.2. We fixed it in 3.0

The workaround is to do the options configuration yourself

@javiercn I thought about that but couldn't figure out a couple of things. First off I imagine I'd register a single implementation of IConfigureNamedOptions<OpenIdConnectOptions> before I call either of the auth providers, to ensure that my singleton is the one registered. Now, in order to know "who" am I building the options for (AzureAD or AzureADB2C), I'd need to check the scheme that was requested, correct? Which would be the name parameter of the Configure method in my options configurator class.

And the issue I hit was, the classes that do this for the framework (OpenIdConnectOptionsConfiguration in the AzureAD and AzureADB2C projects) depend on IOptions<AzureADSchemeOptions> (and a corresponding for B2C) but those are internal so I wouldn't be able to use them myself... do you have any pointers on what do those accomplish, and how would I be able to do it on my own?

I had this same issue and tried figuring out some way to make them play nice together but was not able to.

Doing the configuration manually did not seem practical as it would require essentially rewriting the module due to internal only configuration classes (eg. AzureADB2COpenIDConnectEventHandlers).

Okay spoke to soon, seem to have it working as ugly as it is...

In Startup.ConfigureServices:

// hack to just grab preconfigured options
var dummyCollection = new ServiceCollection();
dummyCollection.AddAuthentication("dummy").AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options));
var sp = dummyCollection.BuildServiceProvider();
var openIdSnapshot = sp.GetService<IOptionsSnapshot<OpenIdConnectOptions>>();
var b2cOpenIdOpts = openIdSnapshot.Get(AzureADB2CDefaults.OpenIdScheme);
var cookieSnapshot = sp.GetService<IOptionsSnapshot<CookieAuthenticationOptions>>();
var b2cCookieOpts = cookieSnapshot.Get(AzureADB2CDefaults.CookieScheme);

services.AddAuthentication("AADorB2C")
        .AddPolicyScheme("AADorB2C", "Selects AAD or B2C", o =>
        {
            o.ForwardDefaultSelector = ctx =>
            {
                 // app logic to select if you want to use AAD or B2C
            };
        })
        .AddAzureAD(options => Configuration.Bind("AzureAd", options))
        .AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options));

// configure the B2C options here by copying the pertinent values from above
services.Configure<OpenIdConnectOptions>(AzureADB2CDefaults.OpenIdScheme, c => {
    c.ClientId = b2cOpenIdOpts.ClientId;
    c.ClientSecret = b2cOpenIdOpts.ClientSecret;
    c.Authority = b2cOpenIdOpts.Authority;
    c.CallbackPath = b2cOpenIdOpts.CallbackPath;
    c.SignedOutCallbackPath = b2cOpenIdOpts.SignedOutCallbackPath;
    c.SignInScheme = b2cOpenIdOpts.SignInScheme;
    c.TokenValidationParameters = b2cOpenIdOpts.TokenValidationParameters;
    c.Events = b2cOpenIdOpts.Events;
    // add other non-default options you may want
});

// do same for cookies (although less important, just sets the redirect URLs)

I have this issue too, in 3.0 & 3.1.

Between AddAzureAd and AddAzureAdB2C, six schemes get added:

AzureADOpenID
AzureADCookie
AzureADB2C
AzureADB2COpenID
AzureADB2CCookie

The options configuration for both OpenID schemes gets run on both the AAD and B2C OIDC options configurations. This is fine for the respective scheme, but fails for the other (see table below) because the scheme doesn't exist in the mappings (which are added to the OpenIDMappings dictionary on the AzureAD(B2C)SchemeOptions by the auth builder extension), so the options for that specific scheme can't be retrieved. I'm assuming this is happening because there are two implementations of IConfigureOptions<OpenIdConnectOptions> added, one for both AddAzureAD and AddAzureADB2C? This is an assumption, so if someone can enlighten me it would be appreciated.

| Scheme | OptionsConfiguration | result |
| ---------|--------------------------|-------|
| AzureADOpenID | AzureADOpenIDConnectOptionsConfiguration| :heavy_check_mark: OK |
| AzureADB2COpenID | AzureADOpenIDConnectOptionsConfiguration| :x: fails |
| AzureADOpenID | AzureADB2COpenIdConnectOptionsConfiguration| :x: fails |
| AzureADB2COpenID | AzureADB2COpenIdConnectOptionsConfiguration| :heavy_check_mark: OK |

A null check on azureAdScheme and azureADB2CScheme gets past the problem, but that feels like it ignores the problem of differentiating between two implementations of IConfigureOptions<OpenIdConnectOptions>.

https://github.com/aspnet/AspNetCore/blob/e2def80a0ad5143fdbd72f2051a86b2082ca92ce/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L23-L24

https://github.com/aspnet/AspNetCore/blob/e2def80a0ad5143fdbd72f2051a86b2082ca92ce/src/Azure/AzureAD/Authentication.AzureADB2C.UI/src/AzureADB2COpenIdConnectOptionsConfiguration.cs#L25-L26

For what it's worth, this bubbled back up, only this time it was with JwtBearerOptions. In config it is doing a TryAddSingleton of IConfigureOptions<JwtBearerOptions>, which exits if one exists - whereas an AddSingleton will append. There are enough other checks for scheme names that it seems to work in 2.x. Looks like it has been resolved with TryAddEnumerable in dotnet 5.

So, I came back to this topic now that we finally moved our service to .NET Core 3.1 and while the error is different, the scenario still doesn't work. Is that the accepted consensus for 3.1? I was hopeful after @javiercn mentioned that it was fixed in 3.0.

A bit extra context that I found: the conflict seems to be not just between AzureAD and AzureADB2C, if I add AzureAD and plain OIDC (authBuilder.AddOpenIdConnect()) I get the same issue. And to expand on my last comment, the different error seems to be because adding AzureAD sets up an instance of IValidateOptions<AzureADOptions>, which somehow is executing with the settings from the other providers (AzureADB2C or OIDC)?

From the ASPNETCore source code (release/3.1):
image

In my app, with symbols for some Microsoft packages loaded:
image

One final comment from me: I tried the Microsoft.Identity.Web package and it seems to do the trick, no changes to my configuration, minor changes to my services configuration (.AddSignIn() instead of .AddAzureAD()/.AddAzureADB2C()), and I was able to get AzureAD, AzureADB2C, and generic OIDC working as providers on the same app.

@alexvy86 : we are very glad to read this (that was one of the goals of Microsoft.Identity.Web).
cc: @jennyf19 @pmaytak @henrik-me

Would you be willing to share (generic) configuration/code snippets to help others to achieve the same? (we intend to document it in MIcrosoft.Identity.Web documentation, but given you have done it, this would be a good head start :)).

Sure, give me a couple of days and I should be able to clean up my app to work as a generic example.

@jmprieur I created a repo with a sample app that uses 3 different auth providers, based on the official Microsoft samples. It does require some setup with the app registrations in AzureAD and AzureAD B2C, but hopefully it can serve as a starting point for someone.

Thank you @alexvy86 !
Awesome!

Thank you for contacting us. Due to a lack of activity on this discussion issue we're closing it in an effort to keep our backlog clean. If you believe there is a concern related to the ASP.NET Core framework, which hasn't been addressed yet, please file a new issue.

This issue will be locked after 30 more days of inactivity. If you still wish to discuss this subject after then, please create a new issue!

Was this page helpful?
0 / 5 - 0 ratings