Copied to clipboard

Flag this post as spam?

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


  • Ben Palmer 176 posts 842 karma points c-trib
    Feb 16, 2023 @ 17:31
    Ben Palmer
    0

    Can't set User Groups dynamically on login using External Login Provider

    I've implemented Azure Active Directory as an External Login Provider for the Umbraco Backoffice. I've got this setup and I'm successfully assigning groups to users based on the roles they have in AAD.

    I'm currently assigning roles when the user is auto-linked, and when the user logs in (to cover any situation where a user may have a role revoked).

    This appears to work for the most part, but when a user logs in, they see the Umbraco Backoffice but it hasn't fully loaded. If they refresh the page, everything loads as normal (and the groups have changed as expected).

    There is an error in the Console which causes Umbraco to fail loading:

    Possibly unhandled rejection: The user object is invalid, the remainingAuthSeconds is required.
    

    The Backoffice looks like this:

    enter image description here

    The code I'm, using for this is as follows (see below for the entire code):

    OnExternalLogin = (user, loginInfo) =>
    {
        // You can customize the user before it's saved whenever they have
        // logged in with the external provider.
        // i.e. Sync the user's name based on the Claims returned
        // in the externalLogin info
    
        var roles = loginInfo.Principal.FindAll(ClaimTypes.Role);
    
        IList<IReadOnlyUserGroup> groups = new List<IReadOnlyUserGroup>();
    
        // remove all groups and add them to ensure they don't have any groups which have since been removed in Azure
        user.SetGroups(groups as IReadOnlyCollection<IReadOnlyUserGroup>);
    
        var member = _userService.GetByUsername(user.UserName);
    
        member.ClearGroups();
    
        if (roles is not null && roles.Any())
        {
            foreach (var role in roles)
            {
                var group = _userService.GetUserGroupByAlias(role.Value) as IReadOnlyUserGroup;
    
                member.AddGroup(group);
            }
    
            _userService.Save(member);
        }
    
        return true; //returns a boolean indicating if sign in should continue or not.
    }
    

    It seems that Umbraco doesn't like me changing the user at this point in time but I'd like to understand the issue here and if there's any kind of workaround to get this working.


    Full code

    using Microsoft.AspNetCore.Authentication.Cookies;
    using Microsoft.AspNetCore.Authentication.OpenIdConnect;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.IdentityModel.Protocols.OpenIdConnect;
    using Example.Api.Features.Configuration;
    using Umbraco.Cms.Core.DependencyInjection;
    using Umbraco.Extensions;
    
    namespace Example.Api.Features.Authentication.Extensions;
    
    public static class UmbracoBuilderExtensions
    {
        public static IUmbracoBuilder AddOpenIdConnectAuthentication(this IUmbracoBuilder builder)
        {
            // Register OpenIdConnectBackOfficeExternalLoginProviderOptions here rather than require it in startup
            builder.Services.ConfigureOptions<OpenIdConnectBackOfficeExternalLoginProviderOptions>();
    
            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 =>
                            {
                                options.CallbackPath = "/umbraco-signin-microsoft/";
                                // use cookies
                                options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                                // pass configured options along
                                options.Authority = "https://login.microsoftonline.com/{tenantId}/v2.0";
                                options.ClientId = "{clientId}";
                                options.ClientSecret = "{clientSecret}";
                                // Use the authorization code flow
                                options.ResponseType = OpenIdConnectResponseType.Code;
                                options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
                                // map claims
                                options.TokenValidationParameters.NameClaimType = "name";
                                options.TokenValidationParameters.RoleClaimType = "role";
    
                                options.RequireHttpsMetadata = true;
                                options.GetClaimsFromUserInfoEndpoint = true;
                                options.SaveTokens = true;
                                options.UsePkce = true;
    
                                options.Scope.Add("email");
                            });
                    });
            });
            return builder;
        }
    }
    
    using System.Security.Claims;
    using Microsoft.Extensions.Options;
    using Umbraco.Cms.Core;
    using Umbraco.Cms.Core.Models.Membership;
    using Umbraco.Cms.Core.Services;
    using Umbraco.Cms.Web.BackOffice.Security;
    
    namespace Example.Api.Features.Configuration;
    
    public class OpenIdConnectBackOfficeExternalLoginProviderOptions : IConfigureNamedOptions<BackOfficeExternalLoginProviderOptions>
    {
        public const string SchemeName = "OpenIdConnect";
    
        private readonly IUserService _userService;
    
        public OpenIdConnectBackOfficeExternalLoginProviderOptions(IUserService userService)
        {
            _userService = userService;
        }
    
        public void Configure(string name, BackOfficeExternalLoginProviderOptions options)
        {
            if (name != "Umbraco." + SchemeName)
            {
                return;
            }
    
            Configure(options);
        }
    
        public void Configure(BackOfficeExternalLoginProviderOptions options)
        {
            options.AutoLinkOptions = new ExternalSignInAutoLinkOptions(
                // must be true for auto-linking to be enabled
                autoLinkExternalAccount: true,
    
                // assign in the OnAutoLinking callback
                // (default is editor)
                defaultUserGroups: new[] { Constants.Security.EditorGroupAlias },
    
                // 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) =>
                {
                    // 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 roles = loginInfo.Principal.FindAll(ClaimTypes.Role);
    
                    IList<IReadOnlyUserGroup> groups = new List<IReadOnlyUserGroup>();
    
                    if (roles is not null && roles.Any())
                    {
                        foreach (var role in roles)
                        {
                            groups.Add(_userService.GetUserGroupByAlias(role.Value) as IReadOnlyUserGroup);
                        }
    
                        autoLinkUser.SetGroups(groups as IReadOnlyCollection<IReadOnlyUserGroup>);
                    }
    
                    autoLinkUser.IsApproved = true;
                },
                OnExternalLogin = (user, loginInfo) =>
                {
                    // You can customize the user before it's saved whenever they have
                    // logged in with the external provider.
                    // i.e. Sync the user's name based on the Claims returned
                    // in the externalLogin info
    
                    var roles = loginInfo.Principal.FindAll(ClaimTypes.Role);
    
                    IList<IReadOnlyUserGroup> groups = new List<IReadOnlyUserGroup>();
    
                    // remove all groups and add them to ensure they don't have any groups which have since been removed in Azure
                    user.SetGroups(groups as IReadOnlyCollection<IReadOnlyUserGroup>);
    
                    var member = _userService.GetByUsername(user.UserName);
    
                    member.ClearGroups();
    
                    if (roles is not null && roles.Any())
                    {
                        foreach (var role in roles)
                        {
                            var group = _userService.GetUserGroupByAlias(role.Value) as IReadOnlyUserGroup;
    
                            member.AddGroup(group);
                        }
    
                        _userService.Save(member);
                    }
    
                    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;
        }
    }
    
Please Sign in or register to post replies

Write your reply to:

Draft