From b7928a5255bc626b7f247a62f3e563d6bd40845f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 20 Sep 2024 15:18:25 -0400 Subject: [PATCH] fix #4638 - add Logout Everywhere option to User Profile --- Oqtane.Client/Modules/Admin/Login/Index.razor | 2 +- .../Modules/Admin/UserProfile/Index.razor | 29 +++++++++++++++++++ .../Modules/Admin/UserProfile/Index.resx | 3 ++ .../Services/Interfaces/IUserService.cs | 7 +++++ Oqtane.Client/Services/UserService.cs | 6 +++- Oqtane.Client/UI/Routes.razor | 2 +- Oqtane.Server/Controllers/UserController.cs | 20 +++++++++++-- ...taneSiteAuthenticationBuilderExtensions.cs | 2 +- Oqtane.Server/Infrastructure/LogManager.cs | 12 ++------ .../Managers/Interfaces/IUserManager.cs | 1 + Oqtane.Server/Managers/UserManager.cs | 13 +++++++-- Oqtane.Server/Pages/Logout.cshtml.cs | 6 +++- 12 files changed, 85 insertions(+), 18 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 48177a66..f3f22c9c 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -209,7 +209,7 @@ else if (user != null && user.IsAuthenticated) { - await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username); + await logger.LogInformation(LogFunction.Security, "Login Successful For {Username} From IP Address {IPAddress}", _username, SiteState.RemoteIPAddress); // return url is not specified if user navigated directly to login page var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path; diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index c67f3305..91a31585 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -9,6 +9,8 @@ @inject INotificationService NotificationService @inject IFileService FileService @inject IFolderService FolderService +@inject IJSRuntime jsRuntime +@inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -84,6 +86,7 @@
+
@@ -518,6 +521,32 @@ } } + private async Task Logout() + { + await logger.LogInformation("User Logout Everywhere For Username {Username}", PageState.User?.Username); + + var url = NavigateUrl(""); // home page + + if (PageState.Runtime == Shared.Runtime.Hybrid) + { + if (PageState.User != null) + { + // hybrid apps utilize an interactive logout + await UserService.LogoutUserEverywhereAsync(PageState.User); + var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider)); + authstateprovider.NotifyAuthenticationChanged(); + NavigationManager.NavigateTo(url, true); + } + } + else + { + // post to the Logout page to complete the logout process + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url, everywhere = true }; + var interop = new Interop(jsRuntime); + await interop.SubmitForm(Utilities.TenantUrl(PageState.Alias, "/pages/logout/"), fields); + } + } + private bool ValidateProfiles() { foreach (Profile profile in profiles) diff --git a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx index 7e6b222d..a6f0a739 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx @@ -243,4 +243,7 @@ No notifications have been sent + + Logout Everywhere + \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/IUserService.cs b/Oqtane.Client/Services/Interfaces/IUserService.cs index c159c4bd..534466a5 100644 --- a/Oqtane.Client/Services/Interfaces/IUserService.cs +++ b/Oqtane.Client/Services/Interfaces/IUserService.cs @@ -75,6 +75,13 @@ namespace Oqtane.Services /// Task LogoutUserAsync(User user); + /// + /// Logout a + /// + /// + /// + Task LogoutUserEverywhereAsync(User user); + /// /// Update e-mail verification status of a user. /// diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 286fc2d4..2133d2de 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -61,10 +61,14 @@ namespace Oqtane.Services public async Task LogoutUserAsync(User user) { - // best practices recommend post is preferrable to get for logout await PostJsonAsync($"{Apiurl}/logout", user); } + public async Task LogoutUserEverywhereAsync(User user) + { + await PostJsonAsync($"{Apiurl}/logouteverywhere", user); + } + public async Task VerifyEmailAsync(User user, string token) { return await PostJsonAsync($"{Apiurl}/verify?token={token}", user); diff --git a/Oqtane.Client/UI/Routes.razor b/Oqtane.Client/UI/Routes.razor index fb88abcb..de6503f2 100644 --- a/Oqtane.Client/UI/Routes.razor +++ b/Oqtane.Client/UI/Routes.razor @@ -61,7 +61,6 @@ { SiteState.AntiForgeryToken = AntiForgeryToken; SiteState.AuthorizationToken = AuthorizationToken; - SiteState.RemoteIPAddress = (_pageState != null) ? _pageState.RemoteIPAddress : ""; SiteState.Platform = Platform; SiteState.IsPrerendering = (HttpContext != null) ? true : false; @@ -80,6 +79,7 @@ { _pageState = PageState; SiteState.Alias = PageState.Alias; + SiteState.RemoteIPAddress = (PageState != null) ? PageState.RemoteIPAddress : ""; _installed = true; } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index a3521fed..9e80be8d 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -263,8 +263,24 @@ namespace Oqtane.Controllers [Authorize] public async Task Logout([FromBody] User user) { - await HttpContext.SignOutAsync(Constants.AuthenticationScheme); - _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout {Username}", (user != null) ? user.Username : ""); + if (_userPermissions.GetUser(User).UserId == user.UserId) + { + await HttpContext.SignOutAsync(Constants.AuthenticationScheme); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout {Username}", (user != null) ? user.Username : ""); + } + } + + // POST api//logout + [HttpPost("logouteverywhere")] + [Authorize] + public async Task LogoutEverywhere([FromBody] User user) + { + if (_userPermissions.GetUser(User).UserId == user.UserId) + { + await _userManager.LogoutUserEverywhere(user); + await HttpContext.SignOutAsync(Constants.AuthenticationScheme); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout Everywhere {Username}", (user != null) ? user.Username : ""); + } } // POST api//verify diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index a951a4e2..1729358b 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -645,7 +645,7 @@ namespace Oqtane.Extensions } } - _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName); + _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress, providerName); } } else // claims invalid diff --git a/Oqtane.Server/Infrastructure/LogManager.cs b/Oqtane.Server/Infrastructure/LogManager.cs index ed6ce928..91f128ee 100644 --- a/Oqtane.Server/Infrastructure/LogManager.cs +++ b/Oqtane.Server/Infrastructure/LogManager.cs @@ -165,17 +165,11 @@ namespace Oqtane.Infrastructure names.Add(message.Substring(index + 1, message.IndexOf("}", index) - index - 1)); if (values.Length > (names.Count - 1)) { - if (values[names.Count - 1] == null) - { - message = message.Replace("{" + names[names.Count - 1] + "}", "null"); - } - else - { - message = message.Replace("{" + names[names.Count - 1] + "}", values[names.Count - 1].ToString()); - } + var value = (values[names.Count - 1] == null) ? "null" : values[names.Count - 1].ToString(); + message = message.Replace("{" + names[names.Count - 1] + "}", value); } } - index = message.IndexOf("{", index + 1); + index = (index < message.Length - 1) ? message.IndexOf("{", index + 1) : -1; } // rebuild properties into dictionary Dictionary propertyDictionary = new Dictionary(); diff --git a/Oqtane.Server/Managers/Interfaces/IUserManager.cs b/Oqtane.Server/Managers/Interfaces/IUserManager.cs index afcf8a0a..5ada9827 100644 --- a/Oqtane.Server/Managers/Interfaces/IUserManager.cs +++ b/Oqtane.Server/Managers/Interfaces/IUserManager.cs @@ -13,6 +13,7 @@ namespace Oqtane.Managers Task UpdateUser(User user); Task DeleteUser(int userid, int siteid); Task LoginUser(User user, bool setCookie, bool isPersistent); + Task LogoutUserEverywhere(User user); Task VerifyEmail(User user, string token); Task ForgotPassword(User user); Task ResetPassword(User user, string token); diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 63c33d1c..e0e92e97 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -265,7 +265,6 @@ namespace Oqtane.Managers user = _users.UpdateUser(user); _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update); _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload); - _cache.Remove($"user:{user.UserId}:{alias.SiteKey}"); user.Password = ""; // remove sensitive information _logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user); } @@ -373,7 +372,7 @@ namespace Oqtane.Managers user.LastLoginOn = DateTime.UtcNow; user.LastIPAddress = LastIPAddress; _users.UpdateUser(user); - _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful For {Username} From IP Address {IPAddress}", user.Username, LastIPAddress); if (setCookie) { @@ -420,6 +419,16 @@ namespace Oqtane.Managers return user; } + public async Task LogoutUserEverywhere(User user) + { + var identityuser = await _identityUserManager.FindByNameAsync(user.Username); + if (identityuser != null) + { + await _identityUserManager.UpdateSecurityStampAsync(identityuser); + _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update); + _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload); + } + } public async Task VerifyEmail(User user, string token) { diff --git a/Oqtane.Server/Pages/Logout.cshtml.cs b/Oqtane.Server/Pages/Logout.cshtml.cs index 40b58fb5..42334416 100644 --- a/Oqtane.Server/Pages/Logout.cshtml.cs +++ b/Oqtane.Server/Pages/Logout.cshtml.cs @@ -23,7 +23,7 @@ namespace Oqtane.Pages _syncManager = syncManager; } - public async Task OnPostAsync(string returnurl) + public async Task OnPostAsync(string returnurl, string everywhere) { if (HttpContext.User != null) { @@ -31,6 +31,10 @@ namespace Oqtane.Pages var user = _userManager.GetUser(HttpContext.User.Identity.Name, alias.SiteId); if (user != null) { + if (everywhere == "true") + { + await _userManager.LogoutUserEverywhere(user); + } _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, SyncEventActions.Reload); }