diff --git a/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs b/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs index 9e664f0f..3a5ca2f4 100644 --- a/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs +++ b/Oqtane.Server/Extensions/ClaimsPrincipalExtensions.cs @@ -50,5 +50,25 @@ namespace Oqtane.Extensions return ""; } } + + public static int TenantId(this ClaimsPrincipal claimsPrincipal) + { + var sitekey = SiteKey(claimsPrincipal); + if (!string.IsNullOrEmpty(sitekey) && sitekey.Contains(":")) + { + return int.Parse(sitekey.Split(':')[0]); + } + return -1; + } + + public static int SiteId(this ClaimsPrincipal claimsPrincipal) + { + var sitekey = SiteKey(claimsPrincipal); + if (!string.IsNullOrEmpty(sitekey) && sitekey.Contains(":")) + { + return int.Parse(sitekey.Split(':')[1]); + } + return -1; + } } } diff --git a/Oqtane.Server/Providers/IdentityRevalidatingAuthenticationStateProvider.cs b/Oqtane.Server/Providers/IdentityRevalidatingAuthenticationStateProvider.cs new file mode 100644 index 00000000..d3de7e78 --- /dev/null +++ b/Oqtane.Server/Providers/IdentityRevalidatingAuthenticationStateProvider.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Security.Claims; +using System.Threading.Tasks; +using System.Threading; +using System; +using Oqtane.Infrastructure; +using Oqtane.Extensions; + +namespace Oqtane.Providers +{ + internal sealed class IdentityRevalidatingAuthenticationStateProvider( + ILoggerFactory loggerFactory, + IServiceScopeFactory scopeFactory, + IOptions options) + : RevalidatingServerAuthenticationStateProvider(loggerFactory) + { + protected override TimeSpan RevalidationInterval => TimeSpan.FromSeconds(20); + + protected override async Task ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken) + { + await using var scope = scopeFactory.CreateAsyncScope(); + var tenantManager = scope.ServiceProvider.GetRequiredService(); + tenantManager.SetTenant(authenticationState.User.TenantId()); + var userManager = scope.ServiceProvider.GetRequiredService>(); + return await ValidateSecurityStampAsync(userManager, authenticationState.User); + } + + private async Task ValidateSecurityStampAsync(UserManager userManager, ClaimsPrincipal principal) + { + var user = await userManager.FindByNameAsync(principal.Identity.Name); + if (user is null) + { + return false; + } + else if (!userManager.SupportsUserSecurityStamp) + { + return true; + } + else + { + var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType); + var userStamp = await userManager.GetSecurityStampAsync(user); + //return principalStamp == userStamp; // security stamps need to be persisted in principal - they are stored in AspNetUsers + return true; + } + } + } +} diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 0554b3f3..0f3583ea 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -20,6 +20,8 @@ using Microsoft.Extensions.Logging; using Oqtane.Components; using Oqtane.UI; using OqtaneSSR.Extensions; +using Microsoft.AspNetCore.Components.Authorization; +using Oqtane.Providers; namespace Oqtane { @@ -108,6 +110,7 @@ namespace Oqtane services.ConfigureOqtaneIdentityOptions(Configuration); services.AddCascadingAuthenticationState(); + services.AddScoped(); services.AddAuthorization(); services.AddAuthentication(options =>