mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
Properly make tokens unauthorized once the user's password changes
This commit is contained in:
@@ -90,7 +90,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return;
|
||||
}
|
||||
|
||||
await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, context.HttpContext.RequestAborted).ConfigureAwait(false);
|
||||
await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, context.SecurityToken.ValidFrom, context.HttpContext.RequestAborted).ConfigureAwait(false);
|
||||
|
||||
var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
|
||||
|
||||
@@ -100,11 +100,12 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
//if there's no instance user, do a weird thing and add all the instance roles
|
||||
//we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid
|
||||
var rightInt = RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null ? ~0 : authenticationContext.GetRight(I);
|
||||
//if user is null that means they got the token with an expired password
|
||||
var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0 : authenticationContext.GetRight(I);
|
||||
var rightEnum = RightsHelper.RightToType(I);
|
||||
var right = (Enum)Enum.ToObject(rightEnum, rightInt);
|
||||
foreach(Enum J in Enum.GetValues(rightEnum))
|
||||
if(right.HasFlag(J))
|
||||
foreach (Enum J in Enum.GetValues(rightEnum))
|
||||
if (right.HasFlag(J))
|
||||
claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
|
||||
}
|
||||
|
||||
@@ -132,6 +133,13 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <inheritdoc />
|
||||
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
if (AuthenticationContext != null && AuthenticationContext.User == null)
|
||||
{
|
||||
//valid token, expired password
|
||||
await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
//validate the headers
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
@@ -18,8 +20,14 @@ namespace Tgstation.Server.Host.Models
|
||||
/// <summary>
|
||||
/// The uppercase invariant of <see cref="Api.Models.Internal.User.Name"/>
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string CanonicalName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When <see cref="PasswordHash"/> was last changed
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastPasswordUpdate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="User"/>s created by this <see cref="User"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -11,7 +11,15 @@ namespace Tgstation.Server.Host.Security
|
||||
sealed class AuthenticationContext : IAuthenticationContext
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public User User { get; }
|
||||
public User User
|
||||
{
|
||||
get
|
||||
{
|
||||
if (user == null)
|
||||
throw new InvalidOperationException("AuthenticationContext has no user!");
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public InstanceUser InstanceUser { get; }
|
||||
@@ -20,14 +28,24 @@ namespace Tgstation.Server.Host.Security
|
||||
public ISystemIdentity SystemIdentity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="IAuthenticationContext"/>
|
||||
/// Backing field for <see cref="User"/>
|
||||
/// </summary>
|
||||
readonly User user;
|
||||
|
||||
/// <summary>
|
||||
/// Construct an empty <see cref="AuthenticationContext"/>
|
||||
/// </summary>
|
||||
public AuthenticationContext() { }
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="AuthenticationContext"/>
|
||||
/// </summary>
|
||||
/// <param name="systemIdentity">The value of <see cref="SystemIdentity"/></param>
|
||||
/// <param name="user">The value of <see cref="User"/></param>
|
||||
/// <param name="instanceUser">The value of <see cref="InstanceUser"/></param>
|
||||
public AuthenticationContext(ISystemIdentity systemIdentity, User user, InstanceUser instanceUser)
|
||||
{
|
||||
User = user ?? throw new ArgumentNullException(nameof(user));
|
||||
this.user = user ?? throw new ArgumentNullException(nameof(user));
|
||||
if (systemIdentity == null && User.SystemIdentifier != null)
|
||||
throw new ArgumentNullException(nameof(systemIdentity));
|
||||
InstanceUser = instanceUser;
|
||||
@@ -45,6 +63,9 @@ namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
var isInstance = RightsHelper.IsInstanceRight(rightsType);
|
||||
|
||||
//forces the null user check
|
||||
var pullThis = User;
|
||||
|
||||
if (isInstance && InstanceUser == null)
|
||||
return 0;
|
||||
var rightsEnum = RightsHelper.RightToType(rightsType);
|
||||
|
||||
@@ -42,12 +42,12 @@ namespace Tgstation.Server.Host.Security
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken)
|
||||
public async Task CreateAuthenticationContext(long userId, long? instanceId, DateTimeOffset validBefore, CancellationToken cancellationToken)
|
||||
{
|
||||
if (CurrentAuthenticationContext != null)
|
||||
throw new InvalidOperationException("Authentication context has already been loaded");
|
||||
|
||||
var userQuery = databaseContext.Users.Where(x => x.Id == userId).FirstAsync(cancellationToken);
|
||||
var userQuery = databaseContext.Users.Where(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
var instanceUser = instanceId.HasValue ? (await databaseContext.InstanceUsers
|
||||
.Where(x => x.UserId == userId)
|
||||
@@ -56,6 +56,8 @@ namespace Tgstation.Server.Host.Security
|
||||
.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false)) : null;
|
||||
|
||||
var user = await userQuery.ConfigureAwait(false);
|
||||
if (user == default)
|
||||
return;
|
||||
|
||||
ISystemIdentity systemIdentity;
|
||||
if (user.SystemIdentifier != null)
|
||||
@@ -65,7 +67,14 @@ namespace Tgstation.Server.Host.Security
|
||||
throw new InvalidOperationException("Cached system identity has expired!");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate > validBefore)
|
||||
{
|
||||
CurrentAuthenticationContext = new AuthenticationContext();
|
||||
return;
|
||||
}
|
||||
systemIdentity = null;
|
||||
}
|
||||
|
||||
CurrentAuthenticationContext = new AuthenticationContext(systemIdentity, user, instanceUser);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace Tgstation.Server.Host.Security
|
||||
if (String.IsNullOrEmpty(newPassword))
|
||||
throw new ArgumentNullException(nameof(newPassword));
|
||||
user.PasswordHash = passwordHasher.HashPassword(user, newPassword);
|
||||
user.LastPasswordUpdate = DateTimeOffset.Now;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -52,6 +53,7 @@ namespace Tgstation.Server.Host.Security
|
||||
return false;
|
||||
case PasswordVerificationResult.SuccessRehashNeeded:
|
||||
user.PasswordHash = passwordHasher.HashPassword(user, password);
|
||||
//don't update LastPasswordUpdate since it hasn't actually changed
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
@@ -20,8 +19,9 @@ namespace Tgstation.Server.Host.Security
|
||||
/// </summary>
|
||||
/// <param name="userId">The <see cref="Api.Models.Internal.User.Id"/> of the <see cref="IAuthenticationContext.User"/></param>
|
||||
/// <param name="instanceId">The <see cref="Api.Models.Instance.Id"/> of the operation</param>
|
||||
/// <param name="validBefore">The <see cref="DateTimeOffset"/> the resulting <see cref="IAuthenticationContext.User"/>'s password must be valid before</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken);
|
||||
Task CreateAuthenticationContext(long userId, long? instanceId, DateTimeOffset validBefore, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user