fix #4580 - add logout everywhere support using SecurityStamp

This commit is contained in:
sbwalker 2024-09-17 08:45:27 -04:00
parent 1f2e2148d5
commit 48f2079f88
13 changed files with 242 additions and 216 deletions

View File

@ -8,74 +8,77 @@
@inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<AuthorizeView Roles="@RoleNames.Registered">
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
<ModuleMessage Message="@Localizer["Info.SignedIn"]" Type="MessageType.Info" />
</Authorized>
<NotAuthorized>
@if (!twofactor)
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
@if (_allowexternallogin)
{
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<br /><br />
}
@if (_allowsitelogin)
{
<div class="form-group">
<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required />
</div>
<div class="form-group mt-2">
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
<div class="input-group">
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required />
@if (PageState.User != null)
{
<ModuleMessage Message="@Localizer["Info.SignedIn"]" Type="MessageType.Info" />
}
else
{
@if (!twofactor)
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
@if (_allowexternallogin)
{
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<br />
<br />
}
@if (_allowsitelogin)
{
<div class="form-group">
<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required />
</div>
<div class="form-group mt-2">
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
<div class="input-group">
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
<div class="form-group mt-2">
@if (!_alwaysremember)
{
<div class="form-check">
<input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Remember Me?</Label>
</div>
}
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br /><br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (PageState.Site.AllowRegistration)
</div>
</div>
<div class="form-group mt-2">
@if (!_alwaysremember)
{
<br /><br />
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink>
<div class="form-check">
<input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Remember Me?</Label>
</div>
}
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br />
<br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (PageState.Site.AllowRegistration)
{
<br />
<br />
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink>
}
</div>
</form>
}
else
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container Oqtane-Modules-Admin-Login">
<div class="form-group">
<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label>
<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Cancel"]</button>
</div>
</form>
}
</NotAuthorized>
</AuthorizeView>
}
</div>
</form>
}
else
{
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container Oqtane-Modules-Admin-Login">
<div class="form-group">
<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label>
<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Cancel"]</button>
</div>
</form>
}
}
@code {
private bool _allowsitelogin = true;
@ -204,7 +207,7 @@
user = await UserService.VerifyTwoFactorAsync(user, _code);
}
if (user.IsAuthenticated)
if (user != null && user.IsAuthenticated)
{
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username);
@ -228,7 +231,7 @@
}
else
{
if (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "required" || user.TwoFactorRequired)
if (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "required" || (user != null && user.TwoFactorRequired))
{
twofactor = true;
validated = false;
@ -239,12 +242,12 @@
if (!twofactor)
{
await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
}
else
{
await logger.LogInformation(LogFunction.Security, "Two Factor Verification Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.TwoFactor.Fail"], MessageType.Error);
AddModuleMessage(Localizer["Error.TwoFactor.Fail"], MessageType.Error);
}
}
}

View File

@ -11,65 +11,64 @@
{
if (!_userCreated)
{
<AuthorizeView Roles="@RoleNames.Registered">
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
<ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" />
</Authorized>
<NotAuthorized>
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_username" maxlength="256" required />
</div>
if (PageState.User != null)
{
<ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" />
}
else
{
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_username" maxlength="256" required />
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@_email" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" />
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@_email" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
@if (_allowsitelogin)
{
<br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
@if (_allowsitelogin)
{
<br /><br />
<NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink>
}
</form>
</NotAuthorized>
</AuthorizeView>
<br />
<NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink>
}
</form>
}
}
}
else

View File

@ -4,31 +4,28 @@
@inject IStringLocalizer<SharedResources> SharedLocalizer
<span class="app-login">
<AuthorizeView Roles="@RoleNames.Registered">
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
@if (PageState.Runtime == Runtime.Hybrid)
{
<button type="button" class="btn btn-primary" @onclick="LogoutUser">@Localizer["Logout"]</button>
}
else
{
<form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm">
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="hidden" name="returnurl" value="@returnurl" />
<button type="submit" class="btn btn-primary">@Localizer["Logout"]</button>
</form>
}
</Authorized>
<NotAuthorized>
@if (ShowLogin)
{
<a href="@loginurl" class="btn btn-primary">@SharedLocalizer["Login"]</a>
}
</NotAuthorized>
</AuthorizeView>
@if (PageState.User != null)
{
@if (PageState.Runtime == Runtime.Hybrid)
{
<button type="button" class="btn btn-primary" @onclick="LogoutUser">@Localizer["Logout"]</button>
}
else
{
<form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm">
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="hidden" name="returnurl" value="@returnurl" />
<button type="submit" class="btn btn-primary">@Localizer["Logout"]</button>
</form>
}
}
else
{
@if (ShowLogin)
{
<a href="@loginurl" class="btn btn-primary">@SharedLocalizer["Login"]</a>
}
}
</span>
@code

View File

@ -6,20 +6,17 @@
@inject NavigationManager NavigationManager
<span class="app-profile">
<AuthorizeView Roles="@RoleNames.Registered">
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
<a href="@NavigateUrl("profile", "returnurl=" + _returnurl)" class="btn btn-primary">@context.User.Identity.Name</a>
</Authorized>
<NotAuthorized>
@if (ShowRegister && PageState.Site.AllowRegistration)
{
<a href="@NavigateUrl("register", "returnurl=" + _returnurl)" class="btn btn-primary">@Localizer["Register"]</a>
}
</NotAuthorized>
</AuthorizeView>
@if (PageState.User != null)
{
<a href="@NavigateUrl("profile", "returnurl=" + _returnurl)" class="btn btn-primary">@PageState.User.Username</a>
}
else
{
@if (ShowRegister && PageState.Site.AllowRegistration)
{
<a href="@NavigateUrl("register", "returnurl=" + _returnurl)" class="btn btn-primary">@Localizer["Register"]</a>
}
}
</span>
@code {

View File

@ -157,7 +157,7 @@
// verify user is authenticated for current site
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == "sitekey" && item.Value == SiteState.Alias.SiteKey))
if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == Constants.SiteKeyClaimType && item.Value == SiteState.Alias.SiteKey))
{
// get user
var userid = int.Parse(authState.User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);

View File

@ -1,6 +1,5 @@
using System.Linq;
using System.Security.Claims;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.Extensions
@ -41,9 +40,9 @@ namespace Oqtane.Extensions
public static string SiteKey(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.HasClaim(item => item.Type == "sitekey"))
if (claimsPrincipal.HasClaim(item => item.Type == Constants.SiteKeyClaimType))
{
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == "sitekey").Value;
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == Constants.SiteKeyClaimType).Value;
}
else
{
@ -71,6 +70,18 @@ namespace Oqtane.Extensions
return -1;
}
public static string SecurityStamp(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.HasClaim(item => item.Type == Constants.SecurityStampClaimType))
{
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == Constants.SecurityStampClaimType).Value;
}
else
{
return "";
}
}
public static bool IsOnlyInRole(this ClaimsPrincipal claimsPrincipal, string role)
{
var identity = claimsPrincipal.Identities.FirstOrDefault(item => item.AuthenticationType == Constants.AuthenticationScheme);

View File

@ -231,6 +231,7 @@ namespace Oqtane.Managers
{
identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password);
await _identityUserManager.UpdateAsync(identityuser);
await _identityUserManager.UpdateSecurityStampAsync(identityuser); // will force user to sign in again
}
else
{
@ -241,7 +242,8 @@ namespace Oqtane.Managers
if (user.Email != identityuser.Email)
{
await _identityUserManager.SetEmailAsync(identityuser, user.Email);
identityuser.Email = user.Email;
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
// if email address changed and it is not confirmed, verification is required for new email address
if (!user.EmailConfirmed)

View File

@ -10,6 +10,7 @@ using System;
using Oqtane.Infrastructure;
using Oqtane.Extensions;
using Oqtane.Managers;
using System.Security.Claims;
namespace Oqtane.Providers
{
@ -41,6 +42,8 @@ namespace Oqtane.Providers
else
{
return true;
//var principalStamp = authState.User.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType);
//return principalStamp == user.SecurityStamp;
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Infrastructure;
@ -14,13 +15,15 @@ namespace Oqtane.Repository
private readonly IDbContextFactory<TenantDBContext> _dbContextFactory;
private readonly IRoleRepository _roles;
private readonly ITenantManager _tenantManager;
private readonly UserManager<IdentityUser> _identityUserManager;
private readonly IMemoryCache _cache;
public UserRoleRepository(IDbContextFactory<TenantDBContext> dbContextFactory, IRoleRepository roles, ITenantManager tenantManager, IMemoryCache cache)
public UserRoleRepository(IDbContextFactory<TenantDBContext> dbContextFactory, IRoleRepository roles, ITenantManager tenantManager, UserManager<IdentityUser> identityUserManager, IMemoryCache cache)
{
_dbContextFactory = dbContextFactory;
_roles = roles;
_tenantManager = tenantManager;
_identityUserManager = identityUserManager;
_cache = cache;
}
@ -69,10 +72,8 @@ namespace Oqtane.Repository
DeleteUserRoles(userRole.UserId);
}
var alias = _tenantManager.GetAlias();
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
UpdateSecurityStamp(userRole.UserId);
return userRole;
}
@ -82,9 +83,7 @@ namespace Oqtane.Repository
db.Entry(userRole).State = EntityState.Modified;
db.SaveChanges();
var alias = _tenantManager.GetAlias();
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
UpdateSecurityStamp(userRole.UserId);
return userRole;
}
@ -144,9 +143,7 @@ namespace Oqtane.Repository
db.UserRole.Remove(userRole);
db.SaveChanges();
var alias = _tenantManager.GetAlias();
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
UpdateSecurityStamp(userRole.UserId);
}
public void DeleteUserRoles(int userId)
@ -158,9 +155,30 @@ namespace Oqtane.Repository
}
db.SaveChanges();
UpdateSecurityStamp(userId);
}
private void UpdateSecurityStamp(int userId)
{
// update user security stamp
using var db = _dbContextFactory.CreateDbContext();
var user = db.User.Find(userId);
if (user != null)
{
var identityuser = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult();
if (identityuser != null)
{
_identityUserManager.UpdateSecurityStampAsync(identityuser);
}
}
// refresh cache
var alias = _tenantManager.GetAlias();
_cache.Remove($"user:{userId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userId}:{alias.SiteKey}");
if (alias != null)
{
_cache.Remove($"user:{userId}:{alias.SiteKey}");
_cache.Remove($"userroles:{userId}:{alias.SiteKey}");
}
}
}
}

View File

@ -13,14 +13,17 @@ namespace Oqtane.Security
public class ClaimsPrincipalFactory<TUser> : UserClaimsPrincipalFactory<TUser> where TUser : IdentityUser
{
private readonly ITenantManager _tenants;
// cannot utilize IUserManager due to circular references - which is fine as this method is only called on login
private readonly IUserRepository _users;
private readonly IUserRoleRepository _userRoles;
private readonly UserManager<TUser> _userManager;
public ClaimsPrincipalFactory(UserManager<TUser> userManager, IOptions<IdentityOptions> optionsAccessor, ITenantManager tenants, IUserRepository users, IUserRoleRepository userroles) : base(userManager, optionsAccessor)
{
_tenants = tenants;
_users = users;
_userRoles = userroles;
_userManager = userManager;
}
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(TUser identityuser)
@ -33,6 +36,7 @@ namespace Oqtane.Security
Alias alias = _tenants.GetAlias();
if (alias != null)
{
user.SecurityStamp = await _userManager.GetSecurityStampAsync(identityuser);
List<UserRole> userroles = _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList();
identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
}

View File

@ -3,12 +3,11 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Oqtane.Infrastructure;
using Oqtane.Repository;
using Oqtane.Models;
using System.Collections.Generic;
using Oqtane.Extensions;
using Oqtane.Shared;
using System.IO;
using Oqtane.Managers;
namespace Oqtane.Security
{
@ -24,49 +23,38 @@ namespace Oqtane.Security
// check if framework is installed
if (config.IsInstalled() && !path.StartsWith("/_")) // ignore Blazor framework requests
{
// get current site
var _logger = context.HttpContext.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
var alias = context.HttpContext.GetAlias();
if (alias != null)
{
var claims = context.Principal.Claims;
var userManager = context.HttpContext.RequestServices.GetService(typeof(IUserManager)) as IUserManager;
var user = userManager.GetUser(context.Principal.UserId(), alias.SiteId); // cached
// check if principal has roles and matches current site
if (!claims.Any(item => item.Type == ClaimTypes.Role) || !claims.Any(item => item.Type == "sitekey" && item.Value == alias.SiteKey))
// check if user is valid, not deleted, has roles, and security stamp has not changed
if (user != null && !user.IsDeleted && user.Roles.Any() && context.Principal.SecurityStamp() == user.SecurityStamp)
{
var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository;
var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
var _logger = context.HttpContext.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
User user = userRepository.GetUser(context.Principal.Identity.Name);
if (user != null)
// validate sitekey in case user has changed sites in installation
if (context.Principal.SiteKey() != alias.SiteKey || !context.Principal.Roles().Any())
{
// replace principal with roles for current site
List<UserRole> userroles = userRoleRepository.GetUserRoles(user.UserId, alias.SiteId).ToList();
if (userroles.Any())
{
var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
context.ReplacePrincipal(new ClaimsPrincipal(identity));
context.ShouldRenew = true;
Log(_logger, alias, "Permissions Updated For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
}
else
{
// user has no roles - remove principal
Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
context.RejectPrincipal();
}
}
else
{
// user does not exist - remove principal
Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
context.RejectPrincipal();
// refresh principal
var identity = UserSecurity.CreateClaimsIdentity(alias, user);
context.ReplacePrincipal(new ClaimsPrincipal(identity));
context.ShouldRenew = true;
Log(_logger, alias, "Permissions Refreshed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
}
}
else
{
// remove principal (ie. log user out)
Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
context.RejectPrincipal();
}
}
else
{
// user is signed in but tenant cannot be determined
// user is signed in but site cannot be determined
Log(_logger, alias, "Alias Could Not Be Resolved For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
}
}
}

View File

@ -99,8 +99,8 @@ namespace Oqtane.Security
if (alias != null && user != null && !user.IsDeleted)
{
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()));
identity.AddClaim(new Claim("sitekey", alias.SiteKey));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()));
identity.AddClaim(new Claim(Constants.SiteKeyClaimType, alias.SiteKey));
if (user.Roles.Contains(RoleNames.Host))
{
// host users are site admins by default
@ -115,6 +115,7 @@ namespace Oqtane.Security
identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
}
identity.AddClaim(new Claim(Constants.SecurityStampClaimType, user.SecurityStamp));
}
return identity;
}

View File

@ -67,6 +67,9 @@ namespace Oqtane.Shared
public static readonly string AntiForgeryTokenHeaderName = "X-XSRF-TOKEN-HEADER";
public static readonly string AntiForgeryTokenCookieName = "X-XSRF-TOKEN-COOKIE";
public static readonly string SecurityStampClaimType = "AspNet.Identity.SecurityStamp";
public static readonly string SiteKeyClaimType = "Oqtane.Identity.SiteKey";
public static readonly string DefaultVisitorFilter = "bot,crawler,slurp,spider,(none),??";
public static readonly string HttpContextAliasKey = "Alias";