Microsoft-identity-web: Sample documentation on how to authenticate with Azure AD AND Azure B2C in one application .net core 3.x

Created on 4 Sep 2020  路  22Comments  路  Source: AzureAD/microsoft-identity-web

  • [ X] documentation doesn't exist
  • [ X] documentation needs clarification
  • [ X] needs an example

Description of the issue

Can you show an example of how to create an application on which employees from an Azure AD can sign-in and with another url users from an Azure B2C instance? I can have them both work separately, but not together.

documentation enhancement fixed multiple auth schemes

Most helpful comment

@sven5 : I agree. I believe that we have a bug, as the options are configured independently of the authentication schemes.

All 22 comments

Would the following help, @UM001 ?
https://github.com/AzureAD/microsoft-identity-web/wiki/web-apis#using-multiple-authentication-schemes

(adapted to Web Apps)

Thank you. I will give it a try. I have no webapi's as given in this example by the way.

An unhandled exception occurred while processing the request. InvalidOperationException: No authentication handler is registered for the scheme 'OpenIdConnect'. The registered schemes are: Bearer, B2CScheme. Did you forget to call AddAuthentication().Add[SomeAuthHandler]("OpenIdConnect",...)?

I modified for now using webapi, but mvc. I get System.InvalidOperationException: 'Scheme already exists: Cookies'
I guess the openconnect is using identical cookie for both azuread and azureb2c. I guess loading azureb2c users into azuread is faster than getting this to work.

` services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration, "AzureAd");
//.EnableTokenAcquisitionToCallDownstreamApi()
//.AddInMemoryTokenCaches();
services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration, "AzureAdB2C", "B2CScheme");
//.EnableTokenAcquisitionToCallDownstreamApi();
// Sign-in users with the Microsoft identity platform
//services.AddMicrosoftIdentityWebAppAuthentication(Configuration);
//services.AddMicrosoftIdentityWebAppAuthentication(Configuration, AzureADB2CDefaults.AuthenticationScheme);
services.Configure(options =>
{
options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "emails" };
options.ProtocolValidator.NonceLifetime = TimeSpan.FromHours(1);
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
options.Scope.Add($"openid profile offline_access"); // {ReadTasksScope} {WriteTasksScope}"
});
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));

        })//.AddMicrosoftIdentityUI()`

I have difficulities understanding how this should work. I have no errors now in the startup, but getting it to work is another thing.

I did not add 4 schemes, only 2?

InvalidOperationException: No authentication handler is registered for the scheme 'AzureB2C'. The registered schemes are: AzureAd, AzureAD, b2c, AzureADB2C. Did you forget to call AddAuthentication().Add[SomeAuthHandler]("AzureB2C",...)?

a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn" asp-route-scheme="AzureAd">Sign in ad
a class="nav-link text-dark" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn" asp-route-scheme="AzureB2C">Sign in B2C

` services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd",
AzureADDefaults.AuthenticationScheme,
"AzureAd", false);

        services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C",
            AzureADB2CDefaults.AuthenticationScheme,
            "b2c", false);`

I would expect only to do this:
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd")
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C")

It keeps going to azure b2c, not to azure ad anymore:
` services.AddMicrosoftIdentityWebAppAuthentication(Configuration);

        services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C",
            AzureADB2CDefaults.AuthenticationScheme,
            "b2c", false);`

I think the issue lies in the fact that the configuration options for 2 authentication schemes are overwritten and/or only 1 is allowed.

services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd", "AzureAd", "AzureAdCookies"); services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C", "AzureAdB2C", "AzureAdB2CCookies");
Above makes 4 schemes, if AzureAd is not defined OpenConnectId is used although it is called 'AzureAd'. This is confusing by the way. In particular as I want to code functional and not in-depth into Azure AD/B2C, it is the start of most application, but not the goal on itself, like I am trying to resolve 2 authentication schemes now.

I am looking at the Woodgrove sample and your code is more or less the same...would be nice to have Woodgrove work with your library as that is exacly what I am looking for.....but with less code like yours.
` [HttpGet()]
public IActionResult SignInAzureAd()
{
var scheme = "AzureAd";
var redirectUrl = Url.Content("~/");
var challenge = new ChallengeResult(
scheme,
new AuthenticationProperties { RedirectUri = redirectUrl });

        return challenge;
    }
    [HttpGet()]
    public IActionResult SignInAzureAdB2C()
    {
        var scheme = "AzureAdB2C";
        var redirectUrl = Url.Content("~/");
        var challenge = new ChallengeResult(
            scheme,
            new AuthenticationProperties { RedirectUri = redirectUrl });

        return challenge;
    }`

I confirm something is wrong in your code. Not exactly sure where as I have no net 5.0 to add a default asp .netcore next to your code to see how that goes. If I add this piece of code from Woodgrove sample I get in my accountcontrollers 2 different (but still erronous) calls to Azure AD and Azure B2C, but as setup is not 100% both do not exactly work.

` private static void ConfigureAuthentication(IConfiguration configuration, IServiceCollection services)
{
var authenticationBuilder = services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});

        ConfigureCookieAuthentication(authenticationBuilder);
        ConfigureB2CAuthentication(configuration, services, authenticationBuilder);
        ConfigureB2BAuthentication(configuration, services, authenticationBuilder);
    }
    private static void ConfigureCookieAuthentication(AuthenticationBuilder authenticationBuilder)
    {
        authenticationBuilder.AddCookie();
    }
    private static void ConfigureB2BAuthentication(IConfiguration configuration, IServiceCollection services, AuthenticationBuilder authenticationBuilder)
    {
        var authenticationOptions = configuration.GetSection("AzureADB2C")
            .Get<AuthenticationConfig>();

        authenticationBuilder.AddOpenIdConnect("AzureADB2C", options =>
        {
            options.Authority = authenticationOptions.Authority;
            options.CallbackPath = new PathString("/b2b-signin-callback");
            options.ClientId = authenticationOptions.ClientId;
            options.SignedOutCallbackPath = new PathString("/b2b-signout-callback");

            options.TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "Name"
            };
        });
    }

    private static void ConfigureB2CAuthentication(IConfiguration configuration, IServiceCollection services, AuthenticationBuilder authenticationBuilder)
    {
        var authenticationOptions = configuration.GetSection("AzureAD")
            .Get<AuthenticationConfig>();

        authenticationBuilder.AddOpenIdConnect("AzureAD", options =>
        {
            options.Authority = authenticationOptions.Authority;
            options.CallbackPath = new PathString("/b2c-signin-callback");
            options.ClientId = authenticationOptions.ClientId;
            options.Scope.Remove("profile");
            options.SignedOutCallbackPath = new PathString("/b2c-signout-callback");

            options.TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = "Name"
            };
        });
    }`

I have it working with Woodgrove as example. Not using this library as I believe the MicrosoftIdentityOptions and scheme can only be one instead of multiple. Hope you get the idea so I can replace all this code with this library in future. 404 and cookie expiration to be investigated now.

        private void ConfigureAuthentication(IServiceCollection services)
        {
            var authenticationBuilder = services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                //options.DefaultAuthenticateScheme = Constants.AzureAdB2c;
            });

            ConfigureCookieAuthentication(authenticationBuilder);
            ConfigureB2CAuthentication(services, authenticationBuilder);
            ConfigureB2BAuthentication(services, authenticationBuilder);
        }
        private void ConfigureCookieAuthentication(AuthenticationBuilder authenticationBuilder)
        {
            authenticationBuilder.AddCookie(options => { options.ExpireTimeSpan = new TimeSpan(7, 0, 0, 0); });
        }
        private void ConfigureB2BAuthentication(IServiceCollection services, AuthenticationBuilder builder)
        {
            var openIdConnectScheme = Constants.AzureAd;
            var authenticationOptions = new MicrosoftIdentityOptions();
            Configuration.Bind(openIdConnectScheme, authenticationOptions);

            builder.AddOpenIdConnect(openIdConnectScheme, options => {
                Configuration.Bind(openIdConnectScheme, options);
                options.Authority = $"https://login.microsoftonline.com/{authenticationOptions.TenantId}/v2.0";

                options.CallbackPath = new PathString(authenticationOptions.CallbackPath);
                options.ClientId = authenticationOptions.ClientId;

                //options.ConfigurationManager = 
                //options.Events = CreateB2BOpenIdConnectEvents();

                options.SignedOutCallbackPath = new PathString(authenticationOptions.SignedOutCallbackPath);

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = ClaimConstants.PreferredUserName
                };
                options.ProtocolValidator.NonceLifetime = TimeSpan.FromHours(1);
            });
        }

        private void ConfigureB2CAuthentication(IServiceCollection services, AuthenticationBuilder builder)
        {
            var openIdConnectScheme = Constants.AzureAdB2c;
            var authenticationOptions = new MicrosoftIdentityOptions();
            Configuration.Bind(openIdConnectScheme, authenticationOptions);

            builder.AddOpenIdConnect(openIdConnectScheme, options => {
                Configuration.Bind(openIdConnectScheme, options);
                options.Authority = $"{authenticationOptions.Instance}tfp/{authenticationOptions.Domain}/{authenticationOptions.SignUpSignInPolicyId}";

                options.CallbackPath = new PathString(authenticationOptions.CallbackPath);
                options.ClientId = authenticationOptions.ClientId;

                //options.ConfigurationManager = 
                //options.Events = CreateB2COpenIdConnectEvents();
                options.Scope.Remove("profile");
                options.SignedOutCallbackPath = new PathString(authenticationOptions.SignedOutCallbackPath);

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = ClaimConstants.Name,                    
                };
                options.ProtocolValidator.NonceLifetime = TimeSpan.FromHours(1);
            });
        }`

AccountController:
`/// <summary>
        /// Handles the user sign-out.
        /// </summary>
        /// <param name="scheme">Authentication scheme.</param>
        /// <returns>Sign out result.</returns>
        [HttpGet("{scheme?}")]
        public async Task<IActionResult> SignOut([FromRoute] string scheme)
        {
            if (User.Identity.IsAuthenticated)
            {
                var authenticateResult = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

                var callbackUrl = Url.Page("/Account/SignedOut", pageHandler: null, values: null, protocol: Request.Scheme);

                await HttpContext.SignOutAsync(
                    authenticateResult.Properties.Items[".AuthScheme"],
                    new AuthenticationProperties()
                    {
                        RedirectUri = callbackUrl
                    });

                return new EmptyResult();
            }
            return RedirectToHome();
        }

Hi, is there any reference to an example using both Identity providers in one MVC app? I too am having the same issue where B2C overwrites AzureAD Authentication when trying to login to an MVC app configured for both Azure AD and B2C through separate links.

It's on the backlog, @Kev8144 to build such a sample. We don't have it yet

Hi,

I'm having the same question. I'd like to use Azure AD and Azure AD B2C together in one app.
However, when calling the controller actions of AccountController, then only the last registered Identity provider is used.

My code in Startup:

services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(Configuration, configSectionName: "AzureAd", cookieScheme: "AzureAdCookies");

services.AddAuthentication()
    .AddMicrosoftIdentityWebApp(Configuration, openIdConnectScheme: "AzureAdB2C", configSectionName: "AzureAdB2C", cookieScheme: "AzureAdB2CCookies");

services.AddControllersWithViews()
    .AddMicrosoftIdentityUI();

Now, when calling href="MicrosoftIdentity/Account/SignIn" the app redirects correctly to B2C login page.
I assumed I'll have to add the scheme to the url, but calling href="MicrosoftIdentity/Account/SignIn/OpenIdConnect" just does the same and not redirecting to my Azure AD login.

Edit: Ok it seems I'm assuming wrong and the default AccountController can only handle one MicrosoftIdentityOptions. I guess I'll have to rewrite the AccountController myself. Will try it tomorrow.

I can confirm that @UM001 answer works. Thanks for this!
I also think that this issue seems to be a limitation of MS Identity Web.

@sven5 : I agree. I believe that we have a bug, as the options are configured independently of the authentication schemes.

@jmprieur Do you have an idea when this could be fixed?

I'm still working around this limitation and copying code from the library to resolve it.

I still need some tweaks to get B2C working with custom policies. For instance, Authority needs to be constructed like in the code here. example:

  if (string.IsNullOrWhiteSpace(options.Authority))
                {
                    options.Authority = AuthorityHelpers.BuildAuthority(authenticationOptions);
                }

                // This is a Microsoft identity platform web app
                options.Authority = AuthorityHelpers.EnsureAuthorityIsV2(options.Authority);

Otherwise the password reset customer user flow doesn't work. This is really time-consuming.
@UM001 fyi

@sven5 : I've started a branch to achieve this work: https://github.com/AzureAD/microsoft-identity-web/tree/jmprieur/multipleSchemeInvestigation in case this can help.
It's high on our list, but we had even more urgent features to tackle first. This should come in the next release.

@jmprieur Thanks. Just one note: Pls keep in mind to also modify the AccountController so that it can deal with multiple MicrosoftIdentityOptions. I struggled with this yesterday.
I'm using IOptionsSnapshot<MicrosoftIdentityOptions> and named the instances according to the scheme.
Example:

        public AccountController(IOptionsSnapshot<MicrosoftIdentityOptions> options) microsoftIdentityOptions)
        {
            _adOptions = options.Get(Microsoft.Identity.Web.Constants.AzureAd);
            _adb2cOptions = options.Get(Microsoft.Identity.Web.Constants.AzureAdB2C);
        }

Edit: But I think in general, this should be generic to allow any combination of Auth providers.

@Kev8144 @sven5 @UM001 we have a preview package you can try, if you'd like to give us early feedback. Have not yet had time to update the wiki, but i'm sure you'll figure it out. If you're interested, please send us an email: [email protected] & [email protected]

Thanks

Was this page helpful?
0 / 5 - 0 ratings