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:
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;
}
}
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:The Backoffice looks like this:
The code I'm, using for this is as follows (see below for the entire code):
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
Hi Ben,
Did you ever find a solution to this? Bumping this, as I am running into the same issue.
Thanks for bumping this issue. It was solved here. Good luck!
is working on a reply...