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

Sample documentation on how to authenticate with Azure AD AND Azure B2C in one application .net core 3.x #549

Closed
UM001 opened this issue Sep 4, 2020 · 39 comments
Labels
documentation Improvements or additions to documentation enhancement New feature or request fixed multiple auth schemes supported in v.1.10
Milestone

Comments

@UM001
Copy link

UM001 commented Sep 4, 2020

  • [ 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.

@UM001 UM001 added the documentation Improvements or additions to documentation label Sep 4, 2020
@UM001 UM001 changed the title [Documentation] Sample documentation on how to authenticate with Azure AD AND Azure B2C in one application .net core 3.x Sep 4, 2020
@jmprieur
Copy link
Collaborator

jmprieur commented Sep 4, 2020

@UM001
Copy link
Author

UM001 commented Sep 4, 2020

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",...)?

@UM001
Copy link
Author

UM001 commented Sep 4, 2020

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()`

@jmprieur
Copy link
Collaborator

jmprieur commented Sep 4, 2020

@UM001 : the overrides have default parameters, including the cookie scheme:

you could use a different name, and that would work.

@jmprieur jmprieur added the question Further information is requested label Sep 4, 2020
@UM001
Copy link
Author

UM001 commented Sep 4, 2020

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")

@UM001
Copy link
Author

UM001 commented Sep 4, 2020

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

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

@UM001
Copy link
Author

UM001 commented Sep 5, 2020

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;
    }`

@UM001
Copy link
Author

UM001 commented Sep 5, 2020

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"
            };
        });
    }`

@UM001
Copy link
Author

UM001 commented Sep 5, 2020

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();
        }

@jmprieur jmprieur removed the question Further information is requested label Nov 30, 2020
@jmprieur
Copy link
Collaborator

See also #173

@Kev8144
Copy link

Kev8144 commented Dec 24, 2020

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.

@jmprieur
Copy link
Collaborator

jmprieur commented Jan 4, 2021

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

@sven5
Copy link

sven5 commented Feb 3, 2021

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.

@sven5
Copy link

sven5 commented Feb 5, 2021

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.

@jmprieur jmprieur added the enhancement New feature or request label Feb 5, 2021
@jmprieur
Copy link
Collaborator

jmprieur commented Feb 8, 2021

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

@jmprieur
Copy link
Collaborator

Duplicate of #955

@jennyf19 jennyf19 added multiple auth schemes supported in v.1.10 fixed labels May 5, 2021
@jennyf19 jennyf19 modified the milestones: 1.10.0, 1.11.0 May 14, 2021
@jennyf19
Copy link
Collaborator

Included in 1.11.0 release and documentation here.

@sven5
Copy link

sven5 commented Jul 7, 2021

So now I'm trying to implement this solution in one of my apps.
I'm using Blazor Server app that should provide two auth providers:

  • Azure AD B2C for b2c users
  • Azure AD for the owner's company users, say admins

I don't need any downstream API.
@jennyf19 Could you please provide a sample how I can manage this?

Edit: my sample code

Startup.cs

services.AddAuthentication()
                    .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"), 
Microsoft.Identity.Web.Constants.AzureAd);
services.AddAuthentication()
                    .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAdB2C"), Microsoft.Identity.Web.Constants.AzureAdB2C, "cookiesB2C");

appsettings.json

"AzureAd": {
   "Instance": "https://login.microsoftonline.com/",
   "Domain": "mycompany.onmicrosoft.com",
   "TenantId": "de35dcf14-4b1f-4e24-91a8-639f1f4fdf45",
   "ClientId": "921e39ae-d140-4091-8383-ca0c37ffd239",
   "CallbackPath": "/B2BLogin",
   "SignedOutCallbackPath": "/B2BLogout",
   "ClientSecret": "...secre..t"
 },
 "AzureAdB2C": {
   "Instance": "https://xxxxx.b2clogin.com",
   "ClientId": "5d64ca1c-8d55-4267-c903-39fba9202137",
   "CallbackPath": "/signin-oidc",
   "Domain": "xxxx2c.onmicrosoft.com",
   "SignUpSignInPolicyId": "B2C_1A_SignUp_SignIn",
   "ResetPasswordPolicyId": "B2C_1A_PasswordReset",
   "EditProfilePolicyId": "B2C_1_EditProfile"
 },

Now when clicking on a link with url=MicrosoftIdentity/Account/SignIn/AzureAd I'm getting login dialog for Azure AD. However, after entering credentials I'm redirected to my app but without an authenticated user, that means user.Identity.IsAuthenticated==false

@jmprieur
Copy link
Collaborator

jmprieur commented Jul 7, 2021

@sven5
Copy link

sven5 commented Jul 7, 2021

@jmprieur Thanks, I already looked into this.

However, it's not working on my side. I'd like to be able that users of both Azure AD and Azure AD B2C are able to login to my application.
I think my requirements are different than what is shown in the sample code.

@jmprieur
Copy link
Collaborator

jmprieur commented Jul 7, 2021

@sven5
Copy link

sven5 commented Jul 7, 2021

@jmprieur Yes, I'm currently trying to find the cause of my issues. Perhaps I have a misconfiguration.

@sven5
Copy link

sven5 commented Jul 7, 2021

@jmprieur I've created a minimal repo sample here: https://github.com/sven5/MultipleAuthTest
The User.Identity.IsAuthenticated is always false. What am I missing here?
@jennyf19 Do you have any idea?

Thanks!

edit: I tried several combinations now and couldn't get it working

@sven5
Copy link

sven5 commented Jul 9, 2021

Update. After lots of trial & error and looking into the source code I've finally found a working solution.

The key is to use Cookie authentication as default and passing the value of null to the cookieScheme parameter.

My code now looks like:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
        options.ExpireTimeSpan = new TimeSpan(7, 0, 0, 0);
});

services.AddAuthentication()
       .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"), Microsoft.Identity.Web.Constants.AzureAd, null);

services.AddAuthentication()
       .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAdB2C"), Microsoft.Identity.Web.Constants.AzureAdB2C, null);

@jmprieur
Copy link
Collaborator

jmprieur commented Jul 9, 2021

Thanks for sharing your findings, @sven5.
I'll update the documentation in the wiki

@Contengo
Copy link

Contengo commented Sep 6, 2021

Is there a sample anywhere of how this can be used in a Blazor Server app? I've configured the Startup.cs as shown above, but am getting "No authenticationScheme was specified, and there was no DefaultChallengeScheme found". How should the preferred authenticationScheme be specified ?

@sven5
Copy link

sven5 commented Sep 6, 2021

@Contengo Blazor Server is the same as plain ASP.NET Core and Razor pages. Just have a look at my sample above. I have it working with a Blazor Server app.

@Contengo
Copy link

Contengo commented Sep 6, 2021

Thanks for the reply - not sure if it's the reason it's not working for me but my use case is slightly different: I'm trying to enable it to call a downstream API. The scenario is a Blazor App which calls an ASP.NET Core API - I'm trying to enable them to be accessed from both B2C and B2B logins. Trying to figure out the best architecture for that to save implementing as two instances of each one. If anyone can get a sample of that working that would be brilliant....

@sven5
Copy link

sven5 commented Sep 7, 2021

@Contengo For your scenario there is a nice wiki article here.

@damienbod
Copy link

Hi @Contengo I did an example for this here:

https://damienbod.com/2021/07/26/securing-asp-net-core-razor-pages-web-apis-with-azure-b2c-external-and-azure-ad-internal-identities/

The APIs need to use the right scheme as well

Greetings Damien

@Contengo
Copy link

Many thanks @damienbod and @sven5 - with those inputs I think it's now nailed. I must say this enforced separation of B2B from B2C identities does make life a whole lot more complicated than it needs to be - I do wish MS would drop it and converge them!

@sven5
Copy link

sven5 commented Sep 24, 2021

@Contengo Nice that it's working now at your side.
The separation of B2B and B2C is by design and absolutely makes sense. Otherwise you get mixed-up environments. The fundamental part of B2C is that it's a stripped-down AAD tenant.

For application developers, there also is another way to go: You could use B2C even for your B2B accounts to allow logging in. But you have to adjust the custom policies, which is an extra complex step. You could read more about here: https://docs.microsoft.com/en-us/azure/active-directory-b2c/identity-provider-azure-ad-single-tenant?pivots=b2c-custom-policy

@babarkhalid
Copy link

Could someone please talk about this issue?

My Startup.cs authentication code:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
options.LoginPath = "/LoginPage";
options.ExpireTimeSpan = new TimeSpan(7, 0, 0, 0);
});

services.AddAuthentication()
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"), Microsoft.Identity.Web.Constants.AzureAd, null)
.EnableTokenAcquisitionToCallDownstreamApi(Configuration.GetValue("DownstreamApi:Scopes")?.Split(' '))
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();

The statement where exception occurs:
var res = await GraphClient.Me.Events.Request().GetAsync();

Exception:
Code: generalException
Message: An error occurred sending the request.

InnerException {"IDW10503: Cannot determine the cloud Instance. The provided authentication scheme was ''. Microsoft.Identity.Web inferred 'Cookies' as the authentication scheme. Available authentication schemes are 'Cookies,AzureAd'. See https://aka.ms/id-web/authSchemes. "}

System.Exception {System.InvalidOperationException}

I really appreciate any help you can provide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation enhancement New feature or request fixed multiple auth schemes supported in v.1.10
Projects
None yet
Development

No branches or pull requests

7 participants