| | | 1 | | using System.Security.Claims; |
| | | 2 | | using Elsa.Common.Multitenancy; |
| | | 3 | | using Elsa.Identity.Contracts; |
| | | 4 | | using Elsa.Identity.Models; |
| | | 5 | | using Elsa.Identity.Options; |
| | | 6 | | using JetBrains.Annotations; |
| | | 7 | | using Microsoft.Extensions.Caching.Memory; |
| | | 8 | | using Microsoft.Extensions.Options; |
| | | 9 | | |
| | | 10 | | namespace Elsa.Identity.Services; |
| | | 11 | | |
| | | 12 | | /// <summary>Rejects a token whose permission stamp no longer matches the user's current grants.</summary> |
| | | 13 | | [UsedImplicitly] |
| | 6 | 14 | | public class PermissionStampValidator( |
| | 6 | 15 | | IUserProvider userProvider, |
| | 6 | 16 | | IPermissionStampCalculator calculator, |
| | 6 | 17 | | IMemoryCache cache, |
| | 6 | 18 | | ITenantAccessor tenantAccessor, |
| | 6 | 19 | | IOptions<PermissionStampOptions> options) |
| | | 20 | | { |
| | | 21 | | /// <summary> |
| | | 22 | | /// Whether <paramref name="principal"/> still carries a current stamp. Returns <c>true</c> when the |
| | | 23 | | /// stamp is disabled, and when a token predates the feature being turned on -- an absent stamp is not |
| | | 24 | | /// treated as a mismatch, so enabling it does not sign everyone out. |
| | | 25 | | /// </summary> |
| | | 26 | | public async ValueTask<bool> IsCurrentAsync(ClaimsPrincipal principal, CancellationToken cancellationToken = default |
| | | 27 | | { |
| | 6 | 28 | | if (!options.Value.IsEnabled) |
| | 1 | 29 | | return true; |
| | | 30 | | |
| | 5 | 31 | | var presented = principal.FindFirst(PermissionStampCalculator.ClaimType)?.Value; |
| | | 32 | | |
| | 5 | 33 | | if (string.IsNullOrWhiteSpace(presented)) |
| | 1 | 34 | | return true; |
| | | 35 | | |
| | 4 | 36 | | var userName = principal.Identity?.Name; |
| | | 37 | | |
| | 4 | 38 | | if (string.IsNullOrWhiteSpace(userName)) |
| | 0 | 39 | | return true; |
| | | 40 | | |
| | | 41 | | // The cache key and the lookup are both tenant-scoped. User names are unique per tenant, not |
| | | 42 | | // globally, so keying on the name alone lets one tenant's cached stamp satisfy a revoked token |
| | | 43 | | // belonging to a same-named user in another tenant. |
| | 4 | 44 | | var tenantId = tenantAccessor.TenantId; |
| | | 45 | | |
| | 4 | 46 | | var current = await cache.GetOrCreateAsync($"elsa:permission-stamp:{tenantId}:{userName}", async entry => |
| | 4 | 47 | | { |
| | 4 | 48 | | entry.AbsoluteExpirationRelativeToNow = options.Value.CacheLifetime; |
| | 4 | 49 | | |
| | 4 | 50 | | var user = await userProvider.FindAsync(new UserFilter { Name = userName, TenantId = tenantId }, cancellatio |
| | 4 | 51 | | |
| | 4 | 52 | | return user is null ? null : await calculator.ComputeAsync(user, cancellationToken); |
| | 8 | 53 | | }); |
| | | 54 | | |
| | | 55 | | // A user that cannot be resolved is not evidence of a stale token; leave that to authentication. |
| | 4 | 56 | | return current is null || string.Equals(current, presented, StringComparison.Ordinal); |
| | 6 | 57 | | } |
| | | 58 | | } |