increase security on webfront cookie state/update events

This commit is contained in:
RaidMax 2022-09-06 15:44:13 -05:00
parent ca35fbb19f
commit 400c5d1f4d

View File

@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Authentication; using System;
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
@ -8,6 +10,7 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models.Client; using Data.Models.Client;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using SharedLibraryCore.Commands;
using static SharedLibraryCore.GameEvent; using static SharedLibraryCore.GameEvent;
namespace WebfrontCore.Middleware namespace WebfrontCore.Middleware
@ -18,68 +21,25 @@ namespace WebfrontCore.Middleware
internal class ClaimsPermissionRemoval internal class ClaimsPermissionRemoval
{ {
private readonly IManager _manager; private readonly IManager _manager;
private readonly List<int> _privilegedClientIds; private static readonly ConcurrentDictionary<int, (ClaimsState, DateTimeOffset?)> PrivilegedClientIds = new();
private readonly RequestDelegate _nextRequest; private readonly RequestDelegate _nextRequest;
private enum ClaimsState
{
Current,
Tainted
}
public ClaimsPermissionRemoval(RequestDelegate nextRequest, IManager manager) public ClaimsPermissionRemoval(RequestDelegate nextRequest, IManager manager)
{ {
_manager = manager; _manager = manager;
_manager.OnGameEventExecuted += OnGameEvent; _manager.OnGameEventExecuted += OnGameEvent;
_privilegedClientIds = new List<int>();
_nextRequest = nextRequest; _nextRequest = nextRequest;
} }
/// <summary>
/// Callback for the game event
/// </summary>
/// <param name="sender"></param>
/// <param name="gameEvent"></param>
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) public async Task Invoke(HttpContext context)
{ {
// we want to load the initial list of privileged clients await Initialize();
bool hasAny;
lock (_privilegedClientIds)
{
hasAny = _privilegedClientIds.Any();
}
if (!hasAny)
{
var ids = (await _manager.GetClientService().GetPrivilegedClients())
.Select(client => client.ClientId);
lock (_privilegedClientIds)
{
_privilegedClientIds.AddRange(ids);
}
}
// sid stores the clientId // sid stores the clientId
var claimsId = context.User.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value; var claimsId = context.User.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value;
@ -87,14 +47,16 @@ namespace WebfrontCore.Middleware
if (!string.IsNullOrEmpty(claimsId)) if (!string.IsNullOrEmpty(claimsId))
{ {
var clientId = int.Parse(claimsId); var clientId = int.Parse(claimsId);
bool hasKey; bool isTainted;
lock (_privilegedClientIds) bool hasPrivilege;
lock (PrivilegedClientIds)
{ {
hasKey = _privilegedClientIds.Contains(clientId); hasPrivilege = PrivilegedClientIds.ContainsKey(clientId);
isTainted = hasPrivilege && PrivilegedClientIds[clientId].Item1 == ClaimsState.Tainted;
} }
// they've been removed if (!hasPrivilege || isTainted)
if (!hasKey && clientId != 1)
{ {
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
} }
@ -102,5 +64,141 @@ namespace WebfrontCore.Middleware
await _nextRequest.Invoke(context); 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;
}
} }
} }