diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs
index 16fa1704dd..0fc660be6c 100644
--- a/src/Tgstation.Server.Host/Controllers/ApiController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs
@@ -1,20 +1,14 @@
-using Microsoft.AspNetCore.Authentication.JwtBearer;
-using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
-using System.Collections.Generic;
using System.Globalization;
-using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net;
-using System.Security.Claims;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
-using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
@@ -57,64 +51,6 @@ namespace Tgstation.Server.Host.Controllers
///
readonly bool requireInstance;
- ///
- /// Runs after a has been validated. Creates the for the
- ///
- /// The for the operation
- /// A representing the running operation
- public static async Task OnTokenValidated(TokenValidatedContext context)
- {
- var databaseContext = context.HttpContext.RequestServices.GetRequiredService();
- var authenticationContextFactory = context.HttpContext.RequestServices.GetRequiredService();
-
- var userIdClaim = context.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
-
- if (userIdClaim == default(Claim))
- throw new InvalidOperationException("Missing required claim!");
-
- long userId;
- try
- {
- userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
- }
- catch (Exception e)
- {
- throw new InvalidOperationException("Failed to parse user ID!", e);
- }
-
- ApiHeaders apiHeaders;
- try
- {
- apiHeaders = new ApiHeaders(context.HttpContext.Request.GetTypedHeaders());
- }
- catch
- {
- //let OnActionExecutionAsync handle the reponse
- return;
- }
-
- await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, context.SecurityToken.ValidFrom, context.HttpContext.RequestAborted).ConfigureAwait(false);
-
- var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
-
- var enumerator = Enum.GetValues(typeof(RightsType));
- var claims = new List();
- foreach (RightsType I in enumerator)
- {
- //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
- //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) ? ~0U : 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))
- claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
- }
-
- context.Principal.AddIdentity(new ClaimsIdentity(claims));
- }
-
///
/// Construct an
///
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index 86475ae07c..6233129ca1 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -101,6 +101,8 @@ namespace Tgstation.Server.Host.Core
services.AddOptions();
+ services.AddScoped();
+
const string scheme = "JwtBearer";
services.AddAuthentication((options) =>
{
@@ -128,9 +130,11 @@ namespace Tgstation.Server.Host.Core
};
jwtBearerOptions.Events = new JwtBearerEvents
{
- OnTokenValidated = ApiController.OnTokenValidated
+ //Application is our composition root so this monstrosity of a line is okay
+ OnTokenValidated = ctx => ctx.HttpContext.RequestServices.GetRequiredService().InjectClaimsIntoContext(ctx, ctx.HttpContext.RequestAborted)
};
});
+
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //fucking converts 'sub' to M$ bs
services.AddMvc().AddJsonOptions(options =>
diff --git a/src/Tgstation.Server.Host/Security/ClaimsInjector.cs b/src/Tgstation.Server.Host/Security/ClaimsInjector.cs
new file mode 100644
index 0000000000..3e7f202b2e
--- /dev/null
+++ b/src/Tgstation.Server.Host/Security/ClaimsInjector.cs
@@ -0,0 +1,95 @@
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Api;
+using Tgstation.Server.Api.Rights;
+using Tgstation.Server.Host.Models;
+
+namespace Tgstation.Server.Host.Security
+{
+ ///
+ sealed class ClaimsInjector : IClaimsInjector
+ {
+ ///
+ /// The for the
+ ///
+ readonly IDatabaseContext databaseContext;
+
+ ///
+ /// The for the
+ ///
+ readonly IAuthenticationContextFactory authenticationContextFactory;
+
+ ///
+ /// Construct a
+ ///
+ /// The value of
+ /// The value of
+ public ClaimsInjector(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory)
+ {
+ this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
+ this.authenticationContextFactory = authenticationContextFactory ?? throw new ArgumentNullException(nameof(authenticationContextFactory));
+ }
+
+ ///
+ public async Task InjectClaimsIntoContext(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
+ {
+ if (tokenValidatedContext == null)
+ throw new ArgumentNullException(nameof(tokenValidatedContext));
+
+ //Find the user id in the token
+ var userIdClaim = tokenValidatedContext.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
+ if (userIdClaim == default)
+ throw new InvalidOperationException("Missing required claim!");
+
+ long userId;
+ try
+ {
+ userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
+ }
+ catch (Exception e)
+ {
+ throw new InvalidOperationException("Failed to parse user ID!", e);
+ }
+
+ ApiHeaders apiHeaders;
+ try
+ {
+ apiHeaders = new ApiHeaders(tokenValidatedContext.HttpContext.Request.GetTypedHeaders());
+ }
+ catch (InvalidOperationException)
+ {
+ //we are not responsible for handling header validation issues
+ return;
+ }
+
+ //This populates the CurrentAuthenticationContext field for use by us and subsequent controllers
+ await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, tokenValidatedContext.SecurityToken.ValidFrom, cancellationToken).ConfigureAwait(false);
+
+ var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
+
+ var enumerator = Enum.GetValues(typeof(RightsType));
+ var claims = new List();
+ foreach (RightsType I in enumerator)
+ {
+ //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
+ //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) ? ~0U : 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))
+ claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
+ }
+
+ tokenValidatedContext.Principal.AddIdentity(new ClaimsIdentity(claims));
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Security/IClaimsInjector.cs b/src/Tgstation.Server.Host/Security/IClaimsInjector.cs
new file mode 100644
index 0000000000..216b03b0f0
--- /dev/null
+++ b/src/Tgstation.Server.Host/Security/IClaimsInjector.cs
@@ -0,0 +1,20 @@
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.Security
+{
+ ///
+ /// For injecting s that can look for
+ ///
+ interface IClaimsInjector
+ {
+ ///
+ /// Setup the s for a given
+ ///
+ /// The containing the and of the request and the to add s to
+ /// The for the operation
+ /// A representing the running operation
+ Task InjectClaimsIntoContext(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken);
+ }
+}
\ No newline at end of file