diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 23d7360f..48177a66 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -8,74 +8,77 @@ @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer - - - ... - - - - - - @if (!twofactor) - { -
-
+ + } + else + { +
+ +
+ } +} @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); } } } diff --git a/Oqtane.Client/Modules/Admin/Register/Index.razor b/Oqtane.Client/Modules/Admin/Register/Index.razor index 1239a512..88ccdd56 100644 --- a/Oqtane.Client/Modules/Admin/Register/Index.razor +++ b/Oqtane.Client/Modules/Admin/Register/Index.razor @@ -11,65 +11,64 @@ { if (!_userCreated) { - - - ... - - - - - - -
-
-
- -
- -
+ if (PageState.User != null) + { + + } + else + { + + +
+
+ +
+
-
- -
-
- - -
-
-
-
- -
-
- - -
-
-
-
- -
- -
-
-
- -
- +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + @if (_allowsitelogin) + {
- - - @if (_allowsitelogin) - { -

- @Localizer["Login"] - } - - - + +
+ @Localizer["Login"] + } + + } } } else diff --git a/Oqtane.Client/Themes/Controls/Theme/Login.razor b/Oqtane.Client/Themes/Controls/Theme/Login.razor index f070d2c1..8b01e34c 100644 --- a/Oqtane.Client/Themes/Controls/Theme/Login.razor +++ b/Oqtane.Client/Themes/Controls/Theme/Login.razor @@ -4,31 +4,28 @@ @inject IStringLocalizer SharedLocalizer @code diff --git a/Oqtane.Client/Themes/Controls/Theme/UserProfile.razor b/Oqtane.Client/Themes/Controls/Theme/UserProfile.razor index dee72bbe..7646d320 100644 --- a/Oqtane.Client/Themes/Controls/Theme/UserProfile.razor +++ b/Oqtane.Client/Themes/Controls/Theme/UserProfile.razor @@ -6,20 +6,17 @@ @inject NavigationManager NavigationManager - - - ... - - - @context.User.Identity.Name - - - @if (ShowRegister && PageState.Site.AllowRegistration) - { - @Localizer["Register"] - } - - + @if (PageState.User != null) + { + @PageState.User.Username + } + else + { + @if (ShowRegister && PageState.Site.AllowRegistration) + { + @Localizer["Register"] + } + } @code { diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 7c4cde03..2a9a7f60 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -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); diff --git a/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs b/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs index bc5c94bd..1749c421 100644 --- a/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs +++ b/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs @@ -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); diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 6772eb2d..63c33d1c 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -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) diff --git a/Oqtane.Server/Providers/IdentityRevalidatingAuthenticationStateProvider.cs b/Oqtane.Server/Providers/IdentityRevalidatingAuthenticationStateProvider.cs index 3ec145f9..46af70f0 100644 --- a/Oqtane.Server/Providers/IdentityRevalidatingAuthenticationStateProvider.cs +++ b/Oqtane.Server/Providers/IdentityRevalidatingAuthenticationStateProvider.cs @@ -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; } } } diff --git a/Oqtane.Server/Repository/UserRoleRepository.cs b/Oqtane.Server/Repository/UserRoleRepository.cs index 0a4a04eb..c438bdb4 100644 --- a/Oqtane.Server/Repository/UserRoleRepository.cs +++ b/Oqtane.Server/Repository/UserRoleRepository.cs @@ -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 _dbContextFactory; private readonly IRoleRepository _roles; private readonly ITenantManager _tenantManager; + private readonly UserManager _identityUserManager; private readonly IMemoryCache _cache; - public UserRoleRepository(IDbContextFactory dbContextFactory, IRoleRepository roles, ITenantManager tenantManager, IMemoryCache cache) + public UserRoleRepository(IDbContextFactory dbContextFactory, IRoleRepository roles, ITenantManager tenantManager, UserManager 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}"); + } } } } diff --git a/Oqtane.Server/Security/ClaimsPrincipalFactory.cs b/Oqtane.Server/Security/ClaimsPrincipalFactory.cs index 00e072fa..0bb0b43e 100644 --- a/Oqtane.Server/Security/ClaimsPrincipalFactory.cs +++ b/Oqtane.Server/Security/ClaimsPrincipalFactory.cs @@ -13,14 +13,17 @@ namespace Oqtane.Security public class ClaimsPrincipalFactory : UserClaimsPrincipalFactory 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 _userManager; public ClaimsPrincipalFactory(UserManager userManager, IOptions optionsAccessor, ITenantManager tenants, IUserRepository users, IUserRoleRepository userroles) : base(userManager, optionsAccessor) { _tenants = tenants; _users = users; _userRoles = userroles; + _userManager = userManager; } protected override async Task GenerateClaimsAsync(TUser identityuser) @@ -33,6 +36,7 @@ namespace Oqtane.Security Alias alias = _tenants.GetAlias(); if (alias != null) { + user.SecurityStamp = await _userManager.GetSecurityStampAsync(identityuser); List userroles = _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList(); identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles); } diff --git a/Oqtane.Server/Security/PrincipalValidator.cs b/Oqtane.Server/Security/PrincipalValidator.cs index 269fbc5b..9d7f74e0 100644 --- a/Oqtane.Server/Security/PrincipalValidator.cs +++ b/Oqtane.Server/Security/PrincipalValidator.cs @@ -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 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); } } } diff --git a/Oqtane.Shared/Security/UserSecurity.cs b/Oqtane.Shared/Security/UserSecurity.cs index 427503d7..4d954037 100644 --- a/Oqtane.Shared/Security/UserSecurity.cs +++ b/Oqtane.Shared/Security/UserSecurity.cs @@ -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; } diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index dc76786d..c8284c18 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -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";