Skip to content

Commit

Permalink
Merge pull request #4645 from sbwalker/dev
Browse files Browse the repository at this point in the history
fix #4638 - add Logout Everywhere option to User Profile
  • Loading branch information
sbwalker authored Sep 20, 2024
2 parents a83ff92 + b7928a5 commit 83d30eb
Show file tree
Hide file tree
Showing 12 changed files with 85 additions and 18 deletions.
2 changes: 1 addition & 1 deletion Oqtane.Client/Modules/Admin/Login/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
29 changes: 29 additions & 0 deletions Oqtane.Client/Modules/Admin/UserProfile/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
@inject INotificationService NotificationService
@inject IFileService FileService
@inject IFolderService FolderService
@inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer

Expand Down Expand Up @@ -84,6 +86,7 @@
<br />
<button type="button" class="btn btn-success" @onclick="Save">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<button type="button" class="btn btn-danger" @onclick="Logout">@Localizer["Logout Everywhere"]</button>
</TabPanel>
<TabPanel Name="Profile" ResourceKey="Profile">
<div class="container">
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,7 @@
<data name="NoNotificationsSent.Text" xml:space="preserve">
<value>No notifications have been sent</value>
</data>
<data name="Logout Everywhere" xml:space="preserve">
<value>Logout Everywhere</value>
</data>
</root>
7 changes: 7 additions & 0 deletions Oqtane.Client/Services/Interfaces/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ public interface IUserService
/// <returns></returns>
Task LogoutUserAsync(User user);

/// <summary>
/// Logout a <see cref="User"/>
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
Task LogoutUserEverywhereAsync(User user);

/// <summary>
/// Update e-mail verification status of a user.
/// </summary>
Expand Down
6 changes: 5 additions & 1 deletion Oqtane.Client/Services/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,14 @@ public async Task<User> LoginUserAsync(User user, bool setCookie, bool isPersist

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<User> VerifyEmailAsync(User user, string token)
{
return await PostJsonAsync<User>($"{Apiurl}/verify?token={token}", user);
Expand Down
2 changes: 1 addition & 1 deletion Oqtane.Client/UI/Routes.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -80,6 +79,7 @@
{
_pageState = PageState;
SiteState.Alias = PageState.Alias;
SiteState.RemoteIPAddress = (PageState != null) ? PageState.RemoteIPAddress : "";
_installed = true;
}
}
Expand Down
20 changes: 18 additions & 2 deletions Oqtane.Server/Controllers/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,24 @@ public async Task<User> Login([FromBody] User user, bool setCookie, bool isPersi
[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/<controller>/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/<controller>/verify
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ private static async Task<ClaimsIdentity> ValidateUser(string id, string name, s
}
}

_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
Expand Down
12 changes: 3 additions & 9 deletions Oqtane.Server/Infrastructure/LogManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,11 @@ private Log ProcessStructuredLog(Log log)
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<string, object> propertyDictionary = new Dictionary<string, object>();
Expand Down
1 change: 1 addition & 0 deletions Oqtane.Server/Managers/Interfaces/IUserManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public interface IUserManager
Task<User> UpdateUser(User user);
Task DeleteUser(int userid, int siteid);
Task<User> LoginUser(User user, bool setCookie, bool isPersistent);
Task LogoutUserEverywhere(User user);
Task<User> VerifyEmail(User user, string token);
Task ForgotPassword(User user);
Task<User> ResetPassword(User user, string token);
Expand Down
13 changes: 11 additions & 2 deletions Oqtane.Server/Managers/UserManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,6 @@ public async Task<User> UpdateUser(User user)
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);
}
Expand Down Expand Up @@ -373,7 +372,7 @@ public async Task<User> LoginUser(User user, bool setCookie, bool isPersistent)
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)
{
Expand Down Expand Up @@ -420,6 +419,16 @@ public async Task<User> LoginUser(User user, bool setCookie, bool isPersistent)

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<User> VerifyEmail(User user, string token)
{
Expand Down
6 changes: 5 additions & 1 deletion Oqtane.Server/Pages/Logout.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ public LogoutModel(IUserManager userManager, ISyncManager syncManager)
_syncManager = syncManager;
}

public async Task<IActionResult> OnPostAsync(string returnurl)
public async Task<IActionResult> OnPostAsync(string returnurl, string everywhere)
{
if (HttpContext.User != null)
{
var alias = HttpContext.GetAlias();
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);
}

Expand Down

0 comments on commit 83d30eb

Please sign in to comment.