Skip to content

Commit

Permalink
Make IExternalLoginEventHandler support update User‘s properties (#12845
Browse files Browse the repository at this point in the history
)

Co-authored-by: Hisham Bin Ateya <[email protected]>
Co-authored-by: Georg von Kries <[email protected]>
  • Loading branch information
3 people authored May 29, 2024
1 parent a2f15aa commit 08e5f6e
Show file tree
Hide file tree
Showing 9 changed files with 488 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -48,6 +53,12 @@ public class AccountController : AccountBaseController
private readonly IDistributedCache _distributedCache;
private readonly IEnumerable<IExternalLoginEventHandler> _externalLoginHandlers;

private static readonly JsonMergeSettings _jsonMergeSettings = new()
{
MergeArrayHandling = MergeArrayHandling.Replace,
MergeNullValueHandling = MergeNullValueHandling.Merge
};

protected readonly IHtmlLocalizer H;
protected readonly IStringLocalizer S;

Expand Down Expand Up @@ -300,24 +311,31 @@ public IActionResult ExternalLogin(string provider, string returnUrl = null)

private async Task<SignInResult> 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);

Expand Down Expand Up @@ -779,6 +797,61 @@ public async Task<IActionResult> RemoveLogin(RemoveLoginViewModel model)
return RedirectToAction(nameof(ExternalLogins));
}

public static async Task<bool> UpdateUserPropertiesAsync(UserManager<IUser> 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<string> GenerateUsernameAsync(ExternalLoginInfo info)
{
var ret = string.Concat("u", IdGenerator.GenerateId());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -45,15 +52,46 @@ public async Task<string> GenerateUserName(string provider, IEnumerable<Serializ
return string.Empty;
}

public async Task UpdateRoles(UpdateRolesContext context)
public async Task UpdateUserAsync(UpdateUserContext context)
{
var loginSettings = (await _siteService.GetSiteSettingsAsync()).As<LoginSettings>();
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<List<UserClaim>>(JOptions.CamelCase);
context.ClaimsToUpdate.AddRange(claimsToUpdate);
}

if (evaluationResult.claimsToRemove is not null)
{
var claimsToRemove = ((JsonArray)JArray.FromObject(evaluationResult.claimsToRemove)).Deserialize<List<UserClaim>>(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;
}
}
}
}
}
Expand Down
Loading

0 comments on commit 08e5f6e

Please sign in to comment.