Properly make tokens unauthorized once the user's password changes

This commit is contained in:
Jordan Brown
2018-08-02 09:18:02 -04:00
parent 3005d7a24d
commit b7d84765d8
6 changed files with 60 additions and 12 deletions
@@ -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
{
+9 -1
View File
@@ -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);
}
}