diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index d0a10ed87b..0235c5d401 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -110,7 +110,7 @@ namespace Tgstation.Server.Api } /// - /// Construct and validates from a + /// Construct and validates from a set of /// /// The containing the public ApiHeaders(RequestHeaders requestHeaders) diff --git a/src/Tgstation.Server.Api/Rights/RightsHelper.cs b/src/Tgstation.Server.Api/Rights/RightsHelper.cs index 9ccbf29e22..2b3e0ddbc0 100644 --- a/src/Tgstation.Server.Api/Rights/RightsHelper.cs +++ b/src/Tgstation.Server.Api/Rights/RightsHelper.cs @@ -25,16 +25,6 @@ namespace Tgstation.Server.Api.Rights { RightsType.InstanceUser, typeof(InstanceUserRights) } }; - static readonly IReadOnlyDictionary rightMap = CreateRightsMap(); - - static IReadOnlyDictionary CreateRightsMap() - { - var dic = new Dictionary(); - foreach (var I in typeMap) - dic.Add(I.Value, I.Key); - return dic; - } - /// /// Map a given to its respective /// @@ -42,7 +32,20 @@ namespace Tgstation.Server.Api.Rights /// The of the given public static Type RightToType(RightsType rightsType) => typeMap[rightsType]; + /// + /// Gets the role claim name used for a given + /// + /// The + /// The + /// A representing the claim role name public static string RoleName(TRight right) => String.Concat(typeof(TRight).Name, '.', right.ToString()); + + /// + /// Gets the role claim name used for a given and + /// + /// The + /// The right value + /// A representing the claim role name public static string RoleName(RightsType rightsType, int right) { var enumType = typeMap[rightsType]; diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs deleted file mode 100644 index e914de955f..0000000000 --- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Tgstation.Server.Host.Configuration -{ - /// - /// General configuration options - /// - sealed class GeneralConfiguration - { - /// - /// The key for the the resides in - /// - public const string Section = "General"; - - /// - /// The string used to validate JWTs - /// - public string TokenSigningKey { get; set; } - } -} diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index be7d20f51f..2df14a9d56 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -17,18 +17,38 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { + /// + /// A for API functions + /// [Produces(ApiHeaders.ApplicationJson)] [Consumes(ApiHeaders.ApplicationJson)] public abstract class ApiController : Controller { + /// + /// The for the operation + /// protected ApiHeaders ApiHeaders { get; private set; } + /// + /// The for the operation + /// protected IDatabaseContext DatabaseContext { get; } + /// + /// The for the operation + /// protected IAuthenticationContext AuthenticationContext { get; } + /// + /// The for the operation + /// protected Instance Instance { get; } + /// + /// 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(); @@ -75,6 +95,11 @@ namespace Tgstation.Server.Host.Controllers context.Principal.AddIdentity(new ClaimsIdentity(claims)); } + /// + /// Construct an + /// + /// The value of + /// The for the public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) { DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext)); @@ -84,6 +109,7 @@ namespace Tgstation.Server.Host.Controllers Instance = AuthenticationContext?.InstanceUser?.Instance; } + /// public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { //validate the headers diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/HomeController.cs index 58dec0d80d..02947c7272 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/HomeController.cs @@ -4,15 +4,18 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; +using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { + /// + /// Main for the + /// [Route("/")] public sealed class HomeController : ApiController { @@ -25,21 +28,38 @@ namespace Tgstation.Server.Host.Controllers /// readonly ISystemIdentityFactory systemIdentityFactory; /// - /// The for the + /// The for the /// - readonly IPasswordHasher passwordHasher; + readonly ICryptographySuite cryptographySuite; - public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, IPasswordHasher passwordHasher) : base(databaseContext, authenticationContextFactory) + /// + /// Construct a + /// + /// The for the + /// The for the + /// The value of + /// The value of + /// The value of + public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite) : base(databaseContext, authenticationContextFactory) { this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory)); this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); - this.passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher)); + this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); } + /// + /// Returns the version of the + /// + /// [Authorize] [HttpGet] - public JsonResult Home() => Json(Assembly.GetExecutingAssembly().GetName().Version); + public JsonResult Home() => Json(Application.Version); + /// + /// Attempt to authenticate a using + /// + /// The for the operation + /// A resulting in the of the operation [HttpPost] public async Task CreateToken(CancellationToken cancellationToken) { @@ -56,17 +76,15 @@ namespace Tgstation.Server.Host.Controllers if (user == null) return Unauthorized(); - if(user.PasswordHash != null) + if (user.PasswordHash != null) { - var hashResult = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, ApiHeaders.Password); - switch (hashResult) + var originalHash = user.PasswordHash; + if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password)) + return Unauthorized(); + if (user.PasswordHash != originalHash) { - case PasswordVerificationResult.Failed: - return Unauthorized(); - case PasswordVerificationResult.SuccessRehashNeeded: - user.PasswordHash = passwordHasher.HashPassword(user, ApiHeaders.Password); - await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - break; + DatabaseContext.Users.Attach(user); + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); } } else diff --git a/src/Tgstation.Server.Host/Controllers/ModelController.cs b/src/Tgstation.Server.Host/Controllers/ModelController.cs index c97e498c3c..59d2b5c19a 100644 --- a/src/Tgstation.Server.Host/Controllers/ModelController.cs +++ b/src/Tgstation.Server.Host/Controllers/ModelController.cs @@ -8,24 +8,64 @@ using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Controllers { - public abstract class ModelController : ApiController + /// + /// An representing a + /// + /// The model being represented + public abstract class ModelController : ApiController where TModel : class { + /// + /// The of the + /// protected static readonly ModelAttribute ModelAttribute = (ModelAttribute)typeof(TModel).GetCustomAttributes(typeof(ModelAttribute), true).First(); + /// + /// Construct a + /// + /// The for the + /// The for the public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) : base(databaseContext, authenticationContextFactory) { } + /// + /// Attempt to create a + /// + /// The being created + /// The for the operation + /// A resulting in the of the operation [HttpPut] public virtual Task Create([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); + /// + /// Attempt to read a + /// + /// The for the operation + /// A resulting in the of the operation [HttpGet] public virtual Task Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); + /// + /// Attempt to update a + /// + /// The being updated + /// The for the operation + /// A resulting in the of the operation [HttpPost] public virtual Task Update([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); + /// + /// Attempt to delete a + /// + /// The being deleted + /// The for the operation + /// A resulting in the of the operation [HttpDelete] public virtual Task Delete([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); + /// + /// Attempt to list entries of the + /// + /// The for the operation + /// A resulting in the of the operation [HttpGet("/List")] public virtual Task List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); } diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 53a3e1639b..2cedce3c5e 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -2,22 +2,15 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using System; -using System.Collections.Generic; using System.Globalization; using System.IdentityModel.Tokens.Jwt; -using System.Linq; using System.Reflection; -using System.Security.Claims; -using System.Text; -using Tgstation.Server.Api; -using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Models; @@ -30,6 +23,11 @@ namespace Tgstation.Server.Host.Core /// sealed class Application { + /// + /// The version of the + /// + public static readonly Version Version = Assembly.GetExecutingAssembly().GetName().Version; + /// /// The for the /// @@ -64,17 +62,9 @@ namespace Tgstation.Server.Host.Core var workingDir = Environment.CurrentDirectory; var databaseConfigurationSection = configuration.GetSection(DatabaseConfiguration.Section); services.Configure(databaseConfigurationSection); - var generalConfigSection = configuration.GetSection(GeneralConfiguration.Section); - services.Configure(generalConfigSection); - - services.AddMvc(); - services.AddOptions(); - - var signingKey = generalConfigSection.Get().TokenSigningKey; - - if (signingKey == "default") - throw new InvalidOperationException("Do not use the default signing key!"); + services.AddOptions(); + const string scheme = "JwtBearer"; services.AddAuthentication((options) => { @@ -85,7 +75,7 @@ namespace Tgstation.Server.Host.Core jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)), + IssuerSigningKey = new SymmetricSecurityKey(TokenFactory.TokenSigningKey), ValidateIssuer = true, ValidIssuer = TokenFactory.TokenIssuer, @@ -105,7 +95,9 @@ namespace Tgstation.Server.Host.Core OnTokenValidated = ApiController.OnTokenValidated }; }); - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //fucking converts 'sub' to M$ bs + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //fucking converts 'sub' to M$ bs + + services.AddMvc(); var databaseConfiguration = databaseConfigurationSection.Get(); void ConfigureDatabase(DbContextOptionsBuilder builder) diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index 7994ea69b6..7c7e3465c7 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using System; using System.Linq; using System.Threading; @@ -18,6 +17,7 @@ namespace Tgstation.Server.Host.Security /// The for the /// readonly ISystemIdentityFactory systemIdentityFactory; + /// /// The for the /// diff --git a/src/Tgstation.Server.Host/Security/CryptographySuite.cs b/src/Tgstation.Server.Host/Security/CryptographySuite.cs index 7fa35d77ee..a3c5553030 100644 --- a/src/Tgstation.Server.Host/Security/CryptographySuite.cs +++ b/src/Tgstation.Server.Host/Security/CryptographySuite.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Identity; using System; using System.Security.Cryptography; -using System.Text; using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Security @@ -10,31 +9,16 @@ namespace Tgstation.Server.Host.Security sealed class CryptographySuite : ICryptographySuite { /// - /// The length of secure strings used in the application + /// Generates a secure set of s /// - public const int SecureStringLength = 40; - - /// - /// Generates a secure ascii of length - /// - /// A secure ascii of length - static string GenerateSecureString() + /// A secure set of s + public static byte[] GetSecureBytes(int amount) { using (var rng = new RNGCryptoServiceProvider()) { - var byt = new byte[1]; - var result = new StringBuilder - { - Capacity = SecureStringLength - }; - while (result.Length < SecureStringLength) - { - rng.GetBytes(byt); - var chr = (char)byt[0]; - if (Char.IsLetterOrDigit(chr)) - result.Append(chr); - } - return result.ToString(); + var byt = new byte[amount]; + rng.GetBytes(byt); + return byt; } } @@ -58,5 +42,19 @@ namespace Tgstation.Server.Host.Security throw new ArgumentNullException(nameof(newPassword)); user.PasswordHash = passwordHasher.HashPassword(user, newPassword); } + + /// + public bool CheckUserPassword(User user, string password) + { + switch(passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password)) + { + case PasswordVerificationResult.Failed: + return false; + case PasswordVerificationResult.SuccessRehashNeeded: + user.PasswordHash = passwordHasher.HashPassword(user, password); + break; + } + return true; + } } } diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs index a5af46a902..2b1ee41a25 100644 --- a/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs +++ b/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs @@ -19,6 +19,11 @@ namespace Tgstation.Server.Host.Security /// InstanceUser InstanceUser { get; } + /// + /// Get the value of a given + /// + /// The of the right to get + /// The value of . Note that if is all based rights will return 0 int GetRight(RightsType rightsType); /// diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs index 4051035c0c..e0f3a225f2 100644 --- a/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/IAuthenticationContextFactory.cs @@ -8,8 +8,18 @@ namespace Tgstation.Server.Host.Security /// public interface IAuthenticationContextFactory { + /// + /// The the created + /// IAuthenticationContext CurrentAuthenticationContext { get; } + /// + /// Create an to populate + /// + /// The of the + /// The of the operation + /// The for the operation + /// A representing the running operation Task CreateAuthenticationContext(long userId, long? instanceId, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Security/ICryptographySuite.cs b/src/Tgstation.Server.Host/Security/ICryptographySuite.cs index 21b3de1a7a..b19f805871 100644 --- a/src/Tgstation.Server.Host/Security/ICryptographySuite.cs +++ b/src/Tgstation.Server.Host/Security/ICryptographySuite.cs @@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Security /// /// Contains various cryptographic functions /// - interface ICryptographySuite + public interface ICryptographySuite { /// /// Sets a for a given @@ -13,5 +13,13 @@ namespace Tgstation.Server.Host.Security /// The whos is to be set /// The new password for the void SetUserPassword(User user, string newPassword); + + /// + /// Checks a given matches a given 's . This may result in being modified and this should be persisted + /// + /// The to check + /// The password to check + /// if matches the hash, otherwise + bool CheckUserPassword(User user, string password); } } diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs index 174f5858ee..e0e4c9af7b 100644 --- a/src/Tgstation.Server.Host/Security/TokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Tokens; using System; using System.Globalization; using System.IdentityModel.Tokens.Jwt; @@ -7,7 +6,6 @@ using System.Reflection; using System.Security.Claims; using System.Text; using Tgstation.Server.Api.Models; -using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Security { @@ -16,17 +14,7 @@ namespace Tgstation.Server.Host.Security { public static readonly string TokenAudience = typeof(Token).Assembly.GetName().Name; public static readonly string TokenIssuer = Assembly.GetExecutingAssembly().GetName().Name; - - /// - /// The for the - /// - readonly GeneralConfiguration generalConfiguration; - - /// - /// Construct a - /// - /// The containing the value of - public TokenFactory(IOptions generalConfigurationOptions) => generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + public static readonly byte[] TokenSigningKey = CryptographySuite.GetSecureBytes(256); /// public Token CreateToken(Models.User user) @@ -43,7 +31,7 @@ namespace Tgstation.Server.Host.Security new Claim(JwtRegisteredClaimNames.Aud, TokenAudience) }; - var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(generalConfiguration.TokenSigningKey)); + var key = new SymmetricSecurityKey(TokenSigningKey); var token = new JwtSecurityToken(new JwtHeader(new SigningCredentials(key, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims)); return new Token { Bearer = new JwtSecurityTokenHandler().WriteToken(token) }; diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index a2125b38fb..2370aabf52 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -19,15 +19,11 @@ - - - PreserveNewest - PreserveNewest diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index c2960461f8..bfcbeffa32 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -17,9 +17,6 @@ } } }, - "General": { - "TokenSigningKey": "Default" - }, "Database": { "DatabaseType": "Sqlite", "ConnectionString": "Fake"