diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs
index 58186e1891..8767a735b9 100644
--- a/src/Tgstation.Server.Host/Controllers/ApiController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs
@@ -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
///
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
{
diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs
index b30c01969e..17432de245 100644
--- a/src/Tgstation.Server.Host/Models/User.cs
+++ b/src/Tgstation.Server.Host/Models/User.cs
@@ -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
///
/// The uppercase invariant of
///
+ [Required]
public string CanonicalName { get; set; }
+ ///
+ /// When was last changed
+ ///
+ public DateTimeOffset? LastPasswordUpdate { get; set; }
+
///
/// s created by this
///
diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs
index 47d3cac17e..32ff73c8e3 100644
--- a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs
+++ b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs
@@ -11,7 +11,15 @@ namespace Tgstation.Server.Host.Security
sealed class AuthenticationContext : IAuthenticationContext
{
///
- public User User { get; }
+ public User User
+ {
+ get
+ {
+ if (user == null)
+ throw new InvalidOperationException("AuthenticationContext has no user!");
+ return user;
+ }
+ }
///
public InstanceUser InstanceUser { get; }
@@ -20,14 +28,24 @@ namespace Tgstation.Server.Host.Security
public ISystemIdentity SystemIdentity { get; }
///
- /// Construct a
+ /// Backing field for
+ ///
+ readonly User user;
+
+ ///
+ /// Construct an empty
+ ///
+ public AuthenticationContext() { }
+
+ ///
+ /// Construct an
///
/// The value of
/// The value of
/// The value of
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);
diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs
index 18fa1536ce..97cc262684 100644
--- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs
+++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs
@@ -42,12 +42,12 @@ namespace Tgstation.Server.Host.Security
}
///
- 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);
}
diff --git a/src/Tgstation.Server.Host/Security/CryptographySuite.cs b/src/Tgstation.Server.Host/Security/CryptographySuite.cs
index b0f68e7a5c..f2b99a7ea0 100644
--- a/src/Tgstation.Server.Host/Security/CryptographySuite.cs
+++ b/src/Tgstation.Server.Host/Security/CryptographySuite.cs
@@ -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;
}
///
@@ -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;
diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs
index 121a9cc46b..ccc1b403de 100644
--- a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs
+++ b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs
@@ -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
///
/// The of the
/// The of the operation
+ /// The the resulting 's password must be valid before
/// The for the operation
/// A representing the running operation
- Task CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken);
+ Task CreateAuthenticationContext(long userId, long? instanceId, DateTimeOffset validBefore, CancellationToken cancellationToken);
}
}