Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Bjorn 7 posts 119 karma points
    Nov 17, 2021 @ 10:51
    Bjorn
    0

    OnAutoLinking not working

    Hi,

    I'm trying to authenticate with an external login. In this case, I want to login via Azure AD B2C. The problem is the default B2C integration doesn't return the email claim, which is needed for autolink. I do not want to use custom policies, since the default user flows are working fine so far.

    The B2C authentication does return the 'emails' claim. So I want to intercept the emails claim, and add the email claim manually so Umbraco can use autolinking.

    The issue I'm facing is that OnAutoLinking callback doesn't seem to be called.

    In my startup.cs:

                services.AddUmbraco(_env, _config)
                .AddBackOffice()
                .AddWebsite()
                .AddOpenIdConnectAuthentication()
                .AddComposers()
                .Build();
    

    The AddOpenIdConnectAuthentication is a seperate class witht the following method (with the correct option values, for now I've hidden those with 'mytenatname' and 0000-0000...)

        public static IUmbracoBuilder AddOpenIdConnectAuthentication(this IUmbracoBuilder builder)
        {
            // Register OpenIdConnectBackOfficeExternalLoginProviderOptions here rather than require it in startup
            builder.Services.ConfigureOptions<OpenIdConnectBackOfficeExternalLoginProviderOptions>(); // https://our.umbraco.com/documentation/reference/security/auto-linking/
    
            builder.AddBackOfficeExternalLogins(logins =>
            {
                logins.AddBackOfficeLogin(
                    backOfficeAuthenticationBuilder =>
                    {
                        backOfficeAuthenticationBuilder.AddOpenIdConnect(
                            // The scheme must be set with this method to work for the back office
                            backOfficeAuthenticationBuilder.SchemeForBackOffice(OpenIdConnectBackOfficeExternalLoginProviderOptions.SchemeName),
                            options =>
                            {
                                // use cookies
                                options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                                // pass configured options along
                                options.Authority = "https://mytenantname.b2clogin.com/mytenantname.onmicrosoft.com/b2c_1_si/v2.0/"; // .well-known/openid-configuration
                                options.ClientId = "00000-00000-0000-0000";
                                options.ClientSecret = "00000-00000-0000-0000";
                                // Use the authorization code flow
                                options.ResponseType = "code id_token"; // code id_token token
                                options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
                                // map claims
                                options.TokenValidationParameters.NameClaimType = "name";
                                options.TokenValidationParameters.RoleClaimType = "role";
    
                                options.RequireHttpsMetadata = true;
                                options.GetClaimsFromUserInfoEndpoint = true;
                                options.SaveTokens = true;
    
                                // add scopes
                                options.Scope.Add("openid");
                                options.Scope.Add("profile");
                                options.Scope.Add("offline_access");
                                options.Scope.Add("email");
                                options.Scope.Add(options.ClientId);
    
                                options.UsePkce = true;
                            });
                    });
            });
            return builder;
        }
    

    The OpenIdConnectBackOfficeExternalLoginProviderOptions is as follows:

    public class OpenIdConnectBackOfficeExternalLoginProviderOptions : IConfigureNamedOptions<BackOfficeExternalLoginProviderOptions>
    {
        public const string SchemeName = "OpenIdConnect";
        public void Configure(string name, BackOfficeExternalLoginProviderOptions options)
        {
            if (name != "Umbraco." + SchemeName)
            {
                return;
            }
    
            Configure(options);
        }
    
        public void Configure(BackOfficeExternalLoginProviderOptions options)
        {
            options.ButtonStyle = "btn-danger";
            options.Icon = "fa fa-cloud";
            options.AutoLinkOptions = new ExternalSignInAutoLinkOptions(
                // must be true for auto-linking to be enabled
                autoLinkExternalAccount: true,
    
                // Optionally specify default user group, else
                // assign in the OnAutoLinking callback
                // (default is editor)
                // defaultUserGroups: new[] { Constants.Security.EditorGroupAlias },
    
                // Optionally specify the default culture to create
                // the user as. If null it will use the default
                // culture defined in the web.config, or it can
                // be dynamically assigned in the OnAutoLinking
                // callback.
    
                defaultCulture: null,
                // Optionally you can disable the ability to link/unlink
                // manually from within the back office. Set this to false
                // if you don't want the user to unlink from this external
                // provider.
                allowManualLinking: false
            )
            {
                // Optional callback
                OnAutoLinking = (autoLinkUser, loginInfo) =>
                {
                    // TODO: I can't debug here... It doens't seems to come here...?
    
                    // You can customize the user before it's linked.
                    // i.e. Modify the user's groups based on the Claims returned
                    // in the externalLogin info
                    var extClaim = loginInfo
                        .Principal
                        .FindFirst("MyClaim");
    
                    autoLinkUser.Claims.Add(new IdentityUserClaim<string>
                    {
                        ClaimType = extClaim.Type,
                        ClaimValue = extClaim.Value,
                        UserId = autoLinkUser.Id
                    });
                },
                OnExternalLogin = (user, loginInfo) =>
                {
                    var test = loginInfo;
                    var extClaim = loginInfo
                        .Principal
                        .FindFirst("MyClaim");
    
                    user.Claims.Add(new IdentityUserClaim<string>
                    {
                        ClaimType = extClaim.Type,
                        ClaimValue = extClaim.Value,
                        UserId = user.Id
                    });
    
                    return true; //returns a boolean indicating if sign in should continue or not.
                }
            };
    
            // Optionally you can disable the ability for users
            // to login with a username/password. If this is set
            // to true, it will disable username/password login
            // even if there are other external login providers installed.
            options.DenyLocalLogin = false;
    
            // Optionally choose to automatically redirect to the
            // external login provider so the user doesn't have
            // to click the login button. This is
            options.AutoRedirectLoginToExternalProvider = false;
        }
    }
    

    I hope anyone knows why OnAutoLinking isn't working.

  • Bjorn 7 posts 119 karma points
    Nov 18, 2021 @ 08:44
    Bjorn
    2

    Update about this issue: B2C doesn't return the 'email' claim (see also https://our.umbraco.com/forum/umbraco-9/107424-externallogin-azure-b2c-email-claim-in-umbraco-9).

    I thought I could use OnAutoLinking to hook into, and add the missing claim. It looks like OnAutoLinking will trigger later.

    So, I added OnAuthorizationCodeReceived. There I added the email claim by using the token from the original token provider (so not B2C, but the chosen identity provider. In this case that is Azure Active Directory).

        private static Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext ctx)
        {
            // This is where we appear after a successfull login, since we use AADB2C Open ID Connect, we already get a lot with the initial authorization code
            ctx.Options.Events.OnAuthorizationCodeReceived = async context =>
            {
                context.ProtocolMessage.RedirectUri = context.ProtocolMessage.RedirectUri.Replace("http:", "https:");
                await Task.FromResult(0);
            };
    
            // Get the current claims
            var claims = ctx.Principal?.Claims;
    
            // Find the original JWT token
            string jwtTokenFromIdentityProvider =
                claims.Where(m => m.Type.Equals("idp_access_token", StringComparison.OrdinalIgnoreCase))
                .Select(m => m.Value).FirstOrDefault();
    
            // Resolve the token to a readable format
            var handler = new JwtSecurityTokenHandler();
            var jsonToken = handler.ReadToken(jwtTokenFromIdentityProvider);
            var tokenFromIdentityProvider = (JwtSecurityToken)jsonToken;
    
            // Get the principal name (so: the login email)
            var principalName = tokenFromIdentityProvider.Claims.First(claim => claim.Type == "upn").Value;
    
            // Define the new claim
            var claimsToAdd = new List<Claim>
            {
                new Claim("email", principalName)
            };
    
            // Add the claim by adding a new ClaimsIdentity
            var appIdentity = new ClaimsIdentity(claimsToAdd);
            ctx.Principal.AddIdentity(appIdentity);
    
            return Task.CompletedTask;
        }
    

    Now something else happens... After this code is execute, the complete Umbraco envirionment won't show up: This site can't be reached.

    I noticed that I did get some new session cookies; UmbracoExternalCookieC1 UmbracoExternalCookieC2 UmbracoExternalCookieC3 UmbracoExternalCookieC4 UmbracoExternalCookieC5 UmbracoExternalCookieC6

    When delete these cookies, the Umbraco works again (of course I'm not logged in into the backoffice)...

    Anyone knows what could happen here that makes Umbraco unavaiable?

    When I delete one cookie, I got another response:

    Bad Request - Request Too Long HTTP Error 400. The size of the request headers is too long.

  • Bjorn 7 posts 119 karma points
    Dec 09, 2021 @ 13:28
    Bjorn
    100

    Unfortunately it is not possible (yet) to use External Login with Azure AD B2C.

    I've now created a controller that handles the request. In the startup.cs I configured the authentication (based on info from appsettings.json)

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(sharedOptions =>
            {
                sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
                .AddAzureAdB2C(options => _config.Bind("AzureAdB2C", options))
                .AddCookie();
    
                (...)
        }
    

    The 'AddAzureAdB2C' is a static method. Here are more configurations and the authentication event handling. There is where the magic happens to authenticate to Umbraco

    public static class AzureAdB2CAuthenticationBuilderExtensions
    {
        public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder, Action<AzureAdB2COptions> configureOptions)
        {
            builder.Services.Configure(configureOptions);
            builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectOptionsSetup>();
            builder.AddOpenIdConnect();
            return builder;
        }
    
        public class OpenIdConnectOptionsSetup : IConfigureNamedOptions<OpenIdConnectOptions>
        {
            private readonly IUserService _userService;
    
            public OpenIdConnectOptionsSetup(IOptions<AzureAdB2COptions> b2cOptions, IConfiguration configuration, IUserService userService)
            {
                AzureAdB2COptions = b2cOptions.Value;
                Configuration = configuration;
                _userService = userService;
            }
    
            public IConfiguration Configuration { get; set; }
            public AzureAdB2COptions AzureAdB2COptions { get; set; }
    
            /// <summary>
            /// Configure the authentication
            /// </summary>
            /// <param name="name"></param>
            /// <param name="options"></param>
            public void Configure(string name, OpenIdConnectOptions options)
            {
                options.ClientId = AzureAdB2COptions.ClientId;
                options.Authority = AzureAdB2COptions.Authority;
                options.UseTokenLifetime = true;
                options.TokenValidationParameters = new TokenValidationParameters() { NameClaimType = "name" };
                options.CallbackPath = AzureAdB2COptions.CallbackPath;
                options.SaveTokens = true;
                options.RequireHttpsMetadata = true;
    
                options.Events = new OpenIdConnectEvents()
                {
                    OnRedirectToIdentityProvider = OnRedirectToIdentityProvider,
                    OnRemoteFailure = OnRemoteFailure,
                    OnTokenValidated = OnTokenValidated
                };
            }
    
            (...)
    

    So, about that magic... it's in the OnTokenValidated. That function will trigger when the B2C authentication has succesfully come back with a token. That does NOT mean you are logged into Umbraco. You'll still need to authenticate the user. This is just a proof-of-concept. So you'll still need to add some error handling here;

            public Task OnTokenValidated(TokenValidatedContext context)
            {
                // First get user claims    
                var claims = context.Principal.Claims.ToList();
    
                // Find the user
                string emailUserClaim = getEmailClaimValue(claims);
                var backofficeUserManager = context.HttpContext.RequestServices.GetRequiredService<IBackOfficeUserManager>();
                var backofficeUser = Task.Run(async () => await backofficeUserManager.FindByEmailAsync(emailUserClaim)).Result;
                if (backofficeUser == null)
                {
                    backofficeUser = Task.Run(async () => await backofficeUserManager.FindByNameAsync(emailUserClaim)).Result;
                }
                if (backofficeUser == null)
                {
                    throw new Exception("Backoffice user not found");
                }
    
                // Log the user in
                var backofficeSignInManager = context.HttpContext.RequestServices.GetRequiredService<IBackOfficeSignInManager>();
                backofficeSignInManager.SignInAsync(backofficeUser, true);
    
                return Task.FromResult(0);
            }
    

    Finding out about the GetRequiredServcie

    I hope I helped anyone out there!

  • John A 6 posts 27 karma points
    Feb 04, 2022 @ 15:15
    John A
    0

    Bjorn, thanks for sharing so much knowledge.

    I just wanted to confirm this AzureB2C code sample is for BackOffice authentication and not for MemberAuthentication (which is what I'm trying to implement and open to suggestions!).

    Thanks

  • Bjorn 7 posts 119 karma points
    Feb 07, 2022 @ 07:38
    Bjorn
    0

    Hi John,

    Yes, this code is for the backoffice authentication indeed.

    I don't see a 'frontofficeSignInManger' or something like that. However, I found this example which might help you: https://gist.github.com/AaronSadlerUK/7b1163a508b11bed9ae92fd87f4ca9a2

    IMemberSignInManager is in Umbraco.Cms.Web.Common.Security. In this article there is a PasswordSignInAsync method, which requires a password. With B2C we don't have that, but there is another method avaiable: SignInAsync(). I guess with that method you might be able to archive a member signin with B2C.

    Good luck with your implementation!

  • ChuDatCN 12 posts 62 karma points
    Sep 06, 2022 @ 01:39
    ChuDatCN
    0

    Hi Bjorn I'm having the same problem implementating Auto-linking Users with IdentityServer4.

    I wonder if your solution can work with IS4 too.

    Thanks. Regards

  • Bjorn 7 posts 119 karma points
    Sep 06, 2022 @ 07:11
    Bjorn
    0

    Hi,

    I think it should work for IS4. I do not have experience with it, but since IdentityServer4 is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core... I should expect that you can implement this solution with it.

    The B2C solution uses OpenID Connect to authenticate with Azure Active Directory. IS4 is an OpenID Connect framework, thus I expect it to work.

    Again, I do not have experience with it, so can't say for sure.

  • Alin Răuțoiu 27 posts 125 karma points
    Sep 08, 2022 @ 10:52
    Alin Răuțoiu
    0

    With AddAzureAdB2C I think you more or less reimplemented the AddMicrosoftIdentityWebApp from the Microsoft.Identity.Web package. Which I don't think it's a bad thing. I found their configuration very finicky, but I think it's worthwhile to know for somebody who wants to setup the external login faster.

  • Gurumurthy 52 posts 125 karma points
    Jun 13, 2023 @ 06:10
    Gurumurthy
    0

    Hello All,

    I am integrating with azure b2c authentication using the open id connect, by using the github, shared (https://github.com/jbreuer/Umbraco-OpenIdConnect-Example). this is for member authentication I am able to authenticate to my azure b2c and receiving all the token, but one of the scope parameter is missing from my access token. This is was tested in umbraco v11.3, and we I do have v8 implementation for the same b2c to get access token, in dot net framework i am getting the scope parameter, but in core the scope parameter is not listing. Also, this si an external api scope that is configured.

    https://xxxxdevb2c.onmicrosoft.com/web-api/api-scope this is the scope value which used in v8 dot net framework, but not able to add this type of url scope in dot net core.. as per below:

    options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("offline_access");

    Any suggestions, on how to add the url type scope in dot net core openid connect.

    Thanks,

Please Sign in or register to post replies

Write your reply to:

Draft