diff --git a/WebfrontCore/Middleware/ClaimsPermissionRemoval.cs b/WebfrontCore/Middleware/ClaimsPermissionRemoval.cs index 83736979f..0aeaae841 100644 --- a/WebfrontCore/Middleware/ClaimsPermissionRemoval.cs +++ b/WebfrontCore/Middleware/ClaimsPermissionRemoval.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Authentication; +using System; +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using SharedLibraryCore; using SharedLibraryCore.Interfaces; @@ -8,6 +10,7 @@ using System.Security.Claims; using System.Threading.Tasks; using Data.Models.Client; using Microsoft.AspNetCore.Authentication.Cookies; +using SharedLibraryCore.Commands; using static SharedLibraryCore.GameEvent; namespace WebfrontCore.Middleware @@ -18,68 +21,25 @@ namespace WebfrontCore.Middleware internal class ClaimsPermissionRemoval { private readonly IManager _manager; - private readonly List _privilegedClientIds; + private static readonly ConcurrentDictionary PrivilegedClientIds = new(); private readonly RequestDelegate _nextRequest; + private enum ClaimsState + { + Current, + Tainted + } + public ClaimsPermissionRemoval(RequestDelegate nextRequest, IManager manager) { _manager = manager; _manager.OnGameEventExecuted += OnGameEvent; - _privilegedClientIds = new List(); _nextRequest = nextRequest; } - /// - /// Callback for the game event - /// - /// - /// - private void OnGameEvent(object sender, GameEvent gameEvent) - { - if (gameEvent.Type != EventType.ChangePermission || gameEvent.Extra is not EFClient.Permission perm) - { - return; - } - - lock (_privilegedClientIds) - { - switch (perm) - { - // we want to remove the claims when the client is demoted - case < EFClient.Permission.Trusted: - { - _privilegedClientIds.RemoveAll(id => id == gameEvent.Target.ClientId); - break; - } - // and add if promoted - case > EFClient.Permission.Trusted when !_privilegedClientIds.Contains(gameEvent.Target.ClientId): - { - _privilegedClientIds.Add(gameEvent.Target.ClientId); - break; - } - } - } - } - public async Task Invoke(HttpContext context) { - // we want to load the initial list of privileged clients - bool hasAny; - lock (_privilegedClientIds) - { - hasAny = _privilegedClientIds.Any(); - } - - if (!hasAny) - { - var ids = (await _manager.GetClientService().GetPrivilegedClients()) - .Select(client => client.ClientId); - - lock (_privilegedClientIds) - { - _privilegedClientIds.AddRange(ids); - } - } + await Initialize(); // sid stores the clientId var claimsId = context.User.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value; @@ -87,14 +47,16 @@ namespace WebfrontCore.Middleware if (!string.IsNullOrEmpty(claimsId)) { var clientId = int.Parse(claimsId); - bool hasKey; - lock (_privilegedClientIds) + bool isTainted; + bool hasPrivilege; + + lock (PrivilegedClientIds) { - hasKey = _privilegedClientIds.Contains(clientId); + hasPrivilege = PrivilegedClientIds.ContainsKey(clientId); + isTainted = hasPrivilege && PrivilegedClientIds[clientId].Item1 == ClaimsState.Tainted; } - // they've been removed - if (!hasKey && clientId != 1) + if (!hasPrivilege || isTainted) { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } @@ -102,5 +64,141 @@ namespace WebfrontCore.Middleware await _nextRequest.Invoke(context); } + + private void OnGameEvent(object sender, GameEvent gameEvent) + { + if (gameEvent.Extra?.GetType() == typeof(SetPasswordCommand)) + { + lock (PrivilegedClientIds) + { + PrivilegedClientIds[gameEvent.Origin.ClientId] = (ClaimsState.Tainted, DateTimeOffset.UtcNow); + } + + return; + } + + if (gameEvent.Type != EventType.ChangePermission || gameEvent.Extra is not EFClient.Permission perm) + { + return; + } + + lock (PrivilegedClientIds) + { + switch (perm) + { + // we want to remove the claims when the client is demoted + case < EFClient.Permission.Trusted when PrivilegedClientIds.ContainsKey(gameEvent.Target.ClientId): + { + PrivilegedClientIds.Remove(gameEvent.Target.ClientId, out _); + break; + } + // and add if promoted + case > EFClient.Permission.Trusted: + { + if (!PrivilegedClientIds.ContainsKey(gameEvent.Target.ClientId)) + { + PrivilegedClientIds.TryAdd(gameEvent.Target.ClientId, (ClaimsState.Current, null)); + } + else + { + // they've been intra-moted, so we need to taint their claims + PrivilegedClientIds[gameEvent.Target.ClientId] = + (ClaimsState.Tainted, DateTimeOffset.UtcNow); + } + + break; + } + } + } + } + + private async Task Initialize() + { + // we want to load the initial list of privileged clients + bool hasAny; + lock (PrivilegedClientIds) + { + hasAny = PrivilegedClientIds.Any(); + } + + if (!hasAny) + { + var ids = (await _manager.GetClientService().GetPrivilegedClients()) + .Select(client => client.ClientId); + + lock (PrivilegedClientIds) + { + foreach (var id in ids) + { + PrivilegedClientIds.TryAdd(id, (ClaimsState.Current, null)); + } + } + } + } + + public static async Task ValidateAsync(CookieValidatePrincipalContext context) + { + if (context.Principal is null) + { + return; + } + + var claimsId = context.Principal.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value; + + if (string.IsNullOrEmpty(claimsId)) + { + return; + } + + var clientId = int.Parse(claimsId); + + bool shouldSignOut; + + lock (PrivilegedClientIds) + { + // we want to log them out if they aren't in the privileged clients list + // or the token is tainted or the taint event occured after the token was generated + shouldSignOut = PrivilegedClientIds.ContainsKey(clientId) && + (PrivilegedClientIds[clientId].Item1 == ClaimsState.Tainted || + PrivilegedClientIds[clientId].Item2 is not null && + PrivilegedClientIds[clientId].Item2.Value - context.Properties.IssuedUtc > + TimeSpan.FromSeconds(30)); + } + + if (shouldSignOut) + { + context.RejectPrincipal(); + await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + } + } + + public static Task OnSignedIn(CookieSignedInContext context) + { + if (context.Principal is null) + { + return Task.CompletedTask; + } + + var claimsId = context.Principal.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value; + + if (string.IsNullOrEmpty(claimsId)) + { + return Task.CompletedTask; + } + + var clientId = int.Parse(claimsId); + + lock (PrivilegedClientIds) + { + if (PrivilegedClientIds.ContainsKey(clientId)) + { + PrivilegedClientIds[clientId] = PrivilegedClientIds[clientId].Item1 == ClaimsState.Tainted + ? (ClaimsState.Current, DateTimeOffset.UtcNow) + : (ClaimsState.Current, null); + } + } + + return Task.CompletedTask; + } } }