diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs index 2beb653a08f..e19628fd5a3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AccountController.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Settings; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; @@ -12,6 +15,8 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Notify; @@ -48,6 +53,12 @@ public class AccountController : AccountBaseController private readonly IDistributedCache _distributedCache; private readonly IEnumerable _externalLoginHandlers; + private static readonly JsonMergeSettings _jsonMergeSettings = new() + { + MergeArrayHandling = MergeArrayHandling.Replace, + MergeNullValueHandling = MergeNullValueHandling.Merge + }; + protected readonly IHtmlLocalizer H; protected readonly IStringLocalizer S; @@ -300,24 +311,31 @@ public IActionResult ExternalLogin(string provider, string returnUrl = null) private async Task ExternalLoginSignInAsync(IUser user, ExternalLoginInfo info) { - var claims = info.Principal.GetSerializableClaims(); + var externalClaims = info.Principal.GetSerializableClaims(); var userRoles = await _userManager.GetRolesAsync(user); - var context = new UpdateRolesContext(user, info.LoginProvider, claims, userRoles); + var userInfo = user as User; + var context = new UpdateUserContext(user, info.LoginProvider, externalClaims, userInfo.Properties) + { + UserClaims = userInfo.UserClaims, + UserRoles = userRoles, + }; foreach (var item in _externalLoginHandlers) { try { - await item.UpdateRoles(context); + await item.UpdateUserAsync(context); } catch (Exception ex) { - _logger.LogError(ex, "{ExternalLoginHandler}.UpdateRoles threw an exception", item.GetType()); + _logger.LogError(ex, "{ExternalLoginHandler}.UpdateUserAsync threw an exception", item.GetType()); } } - await _userManager.AddToRolesAsync(user, context.RolesToAdd.Distinct()); - await _userManager.RemoveFromRolesAsync(user, context.RolesToRemove.Distinct()); + if (await UpdateUserPropertiesAsync(_userManager, userInfo, context)) + { + await _userManager.UpdateAsync(user); + } var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); @@ -779,6 +797,61 @@ public async Task RemoveLogin(RemoveLoginViewModel model) return RedirectToAction(nameof(ExternalLogins)); } + public static async Task UpdateUserPropertiesAsync(UserManager userManager, User user, UpdateUserContext context) + { + await userManager.AddToRolesAsync(user, context.RolesToAdd.Distinct()); + await userManager.RemoveFromRolesAsync(user, context.RolesToRemove.Distinct()); + + var userNeedUpdate = false; + if (context.PropertiesToUpdate != null) + { + var currentProperties = user.Properties.DeepClone(); + user.Properties.Merge(context.PropertiesToUpdate, _jsonMergeSettings); + userNeedUpdate = !JsonNode.DeepEquals(currentProperties, user.Properties); + } + + var currentClaims = user.UserClaims. + Where(x => !x.ClaimType.IsNullOrEmpty()). + DistinctBy(x => new { x.ClaimType, x.ClaimValue }). + ToList(); + + var claimsChanged = false; + if (context.ClaimsToRemove != null) + { + var claimsToRemove = context.ClaimsToRemove.ToHashSet(); + foreach (var item in claimsToRemove) + { + var exists = currentClaims.FirstOrDefault(claim => claim.ClaimType == item.ClaimType && claim.ClaimValue == item.ClaimValue); + if (exists is not null) + { + currentClaims.Remove(exists); + claimsChanged = true; + } + } + } + + if (context.ClaimsToUpdate != null) + { + foreach (var item in context.ClaimsToUpdate) + { + var existing = currentClaims.FirstOrDefault(claim => claim.ClaimType == item.ClaimType && claim.ClaimValue == item.ClaimValue); + if (existing is null) + { + currentClaims.Add(item); + claimsChanged = true; + } + } + } + + if (claimsChanged) + { + user.UserClaims = currentClaims; + userNeedUpdate = true; + } + + return userNeedUpdate; + } + private async Task GenerateUsernameAsync(ExternalLoginInfo info) { var ret = string.Concat("u", IdGenerator.GenerateId()); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Handlers/ScriptExternalLoginEventHandler.cs b/src/OrchardCore.Modules/OrchardCore.Users/Handlers/ScriptExternalLoginEventHandler.cs index 14a3cdf45f2..96d49701be7 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Handlers/ScriptExternalLoginEventHandler.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Handlers/ScriptExternalLoginEventHandler.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Settings; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OrchardCore.Scripting; @@ -14,6 +16,11 @@ public class ScriptExternalLoginEventHandler : IExternalLoginEventHandler private readonly ILogger _logger; private readonly IScriptingManager _scriptingManager; private readonly ISiteService _siteService; + private static readonly JsonMergeSettings _jsonMergeSettings = new JsonMergeSettings + { + MergeArrayHandling = MergeArrayHandling.Union, + MergeNullValueHandling = MergeNullValueHandling.Merge + }; public ScriptExternalLoginEventHandler( ISiteService siteService, @@ -45,15 +52,46 @@ public async Task GenerateUserName(string provider, IEnumerable(); + UpdateUserInternal(context, loginSettings); + } + + public void UpdateUserInternal(UpdateUserContext context, LoginSettings loginSettings) + { if (loginSettings.UseScriptToSyncRoles) { var script = $"js: function syncRoles(context) {{\n{loginSettings.SyncRolesScript}\n}}\nvar context={JConvert.SerializeObject(context, JOptions.CamelCase)};\nsyncRoles(context);\nreturn context;"; dynamic evaluationResult = _scriptingManager.Evaluate(script, null, null, null); context.RolesToAdd.AddRange((evaluationResult.rolesToAdd as object[]).Select(i => i.ToString())); context.RolesToRemove.AddRange((evaluationResult.rolesToRemove as object[]).Select(i => i.ToString())); + + if (evaluationResult.claimsToUpdate is not null) + { + var claimsToUpdate = ((JsonArray)JArray.FromObject(evaluationResult.claimsToUpdate)).Deserialize>(JOptions.CamelCase); + context.ClaimsToUpdate.AddRange(claimsToUpdate); + } + + if (evaluationResult.claimsToRemove is not null) + { + var claimsToRemove = ((JsonArray)JArray.FromObject(evaluationResult.claimsToRemove)).Deserialize>(JOptions.CamelCase); + context.ClaimsToRemove.AddRange(claimsToRemove); + } + + if (evaluationResult.propertiesToUpdate is not null) + { + var result = (JsonObject)JObject.FromObject(evaluationResult.propertiesToUpdate); + if (context.PropertiesToUpdate is not null) + { + // Perhaps other provider will fill some values. we should keep exists value. + context.PropertiesToUpdate.Merge(result, _jsonMergeSettings); + } + else + { + context.PropertiesToUpdate = result; + } + } } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/LoginSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/LoginSettings.Edit.cshtml index 4761a5a8b95..8b274cfbc9e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/LoginSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/LoginSettings.Edit.cshtml @@ -60,101 +60,152 @@
- + @T["If selected, any IExternalLoginEventHandlers defined in modules are not triggered"]
-
-*************************************************************************************************
-* context           : {user,loginProvider,claims[],currentRoles[],rolesToRemove[],rolesToAdd[]} *
-* ============================================================================================= *
-* -user             : {userName}                                                                *
-*  -userName        : String                                                                    *
-* -loginProvider    : String                                                                    *
-* -externalClaims   : [{subject,issuer,originalIssuer,properties[],type,value,valueType}]       *
-*  -subject         : String                                                                    *
-*  -issuer          : String                                                                    *
-*  -originalIssuer  : String                                                                    *
-*  -properties      : [{key,value}]                                                             *
-*   -key            : String                                                                    *
-*   -value          : String                                                                    *
-*  -type            : String                                                                    *
-*  -value           : String                                                                    *
-*  -valueType       : String                                                                    *
-* -rolesToAdd       : [String]                                                                  *
-* -rolesToRemove    : [String]                                                                  *
-* ============================================================================================= *
-*    Description                                                                                *
-* --------------------------------------------------------------------------------------------- *
-*    Use the loginProvider and externalClaims properties of the context variable to inspect     *
-*    who authenticated the user and with what claims. Check currentRoles property and apply     *
-*    your business logic to fill the rolesToAdd and rolesToRemove arrays in order to update     *
-*    the roles of the user                                                                      *
-*************************************************************************************************
-
+
-
- - +
+ +
+
- + + +