increase security on webfront cookie state/update events
This commit is contained in:
parent
ca35fbb19f
commit
400c5d1f4d
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user