Fix release build, generate token signing key each run

This commit is contained in:
Cyberboss
2018-04-16 15:42:05 -04:00
parent 92cb9992f5
commit 41b738debb
15 changed files with 174 additions and 111 deletions
+1 -1
View File
@@ -110,7 +110,7 @@ namespace Tgstation.Server.Api
}
/// <summary>
/// Construct and validates <see cref="ApiHeaders"/> from a <see cref="IHeaderDictionary"/>
/// Construct and validates <see cref="ApiHeaders"/> from a set of <paramref name="requestHeaders"/>
/// </summary>
/// <param name="requestHeaders">The <see cref="RequestHeaders"/> containing the <see cref="ApiHeaders"/></param>
public ApiHeaders(RequestHeaders requestHeaders)
+13 -10
View File
@@ -25,16 +25,6 @@ namespace Tgstation.Server.Api.Rights
{ RightsType.InstanceUser, typeof(InstanceUserRights) }
};
static readonly IReadOnlyDictionary<Type, RightsType> rightMap = CreateRightsMap();
static IReadOnlyDictionary<Type, RightsType> CreateRightsMap()
{
var dic = new Dictionary<Type, RightsType>();
foreach (var I in typeMap)
dic.Add(I.Value, I.Key);
return dic;
}
/// <summary>
/// Map a given <paramref name="rightsType"/> to its respective <see cref="Enum"/> <see cref="Type"/>
/// </summary>
@@ -42,7 +32,20 @@ namespace Tgstation.Server.Api.Rights
/// <returns>The <see cref="Enum"/> <see cref="Type"/> of the given <paramref name="rightsType"/></returns>
public static Type RightToType(RightsType rightsType) => typeMap[rightsType];
/// <summary>
/// Gets the role claim name used for a given <paramref name="right"/>
/// </summary>
/// <typeparam name="TRight">The <see cref="RightsType"/></typeparam>
/// <param name="right">The <typeparamref name="TRight"/></param>
/// <returns>A <see cref="string"/> representing the claim role name</returns>
public static string RoleName<TRight>(TRight right) => String.Concat(typeof(TRight).Name, '.', right.ToString());
/// <summary>
/// Gets the role claim name used for a given <paramref name="rightsType"/> and <paramref name="right"/>
/// </summary>
/// <param name="rightsType">The <see cref="RightsType"/></param>
/// <param name="right">The right value</param>
/// <returns>A <see cref="string"/> representing the claim role name</returns>
public static string RoleName(RightsType rightsType, int right)
{
var enumType = typeMap[rightsType];
@@ -1,18 +0,0 @@
namespace Tgstation.Server.Host.Configuration
{
/// <summary>
/// General configuration options
/// </summary>
sealed class GeneralConfiguration
{
/// <summary>
/// The key for the <see cref="Microsoft.Extensions.Configuration.IConfigurationSection"/> the <see cref="GeneralConfiguration"/> resides in
/// </summary>
public const string Section = "General";
/// <summary>
/// The string used to validate JWTs
/// </summary>
public string TokenSigningKey { get; set; }
}
}
@@ -17,18 +17,38 @@ using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
/// <summary>
/// A <see cref="Controller"/> for API functions
/// </summary>
[Produces(ApiHeaders.ApplicationJson)]
[Consumes(ApiHeaders.ApplicationJson)]
public abstract class ApiController : Controller
{
/// <summary>
/// The <see cref="ApiHeaders"/> for the operation
/// </summary>
protected ApiHeaders ApiHeaders { get; private set; }
/// <summary>
/// The <see cref="IDatabaseContext"/> for the operation
/// </summary>
protected IDatabaseContext DatabaseContext { get; }
/// <summary>
/// The <see cref="IAuthenticationContext"/> for the operation
/// </summary>
protected IAuthenticationContext AuthenticationContext { get; }
/// <summary>
/// The <see cref="Instance"/> for the operation
/// </summary>
protected Instance Instance { get; }
/// <summary>
/// Runs after a <see cref="Api.Models.Token"/> has been validated. Creates the <see cref="IAuthenticationContext"/> for the <see cref="ControllerBase.Request"/>
/// </summary>
/// <param name="context">The <see cref="TokenValidatedContext"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
public static async Task OnTokenValidated(TokenValidatedContext context)
{
var databaseContext = context.HttpContext.RequestServices.GetRequiredService<IDatabaseContext>();
@@ -75,6 +95,11 @@ namespace Tgstation.Server.Host.Controllers
context.Principal.AddIdentity(new ClaimsIdentity(claims));
}
/// <summary>
/// Construct an <see cref="ApiController"/>
/// </summary>
/// <param name="databaseContext">The value of <see cref="DatabaseContext"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
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;
}
/// <inheritdoc />
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
//validate the headers
@@ -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
{
/// <summary>
/// Main <see cref="ApiController"/> for the <see cref="Application"/>
/// </summary>
[Route("/")]
public sealed class HomeController : ApiController
{
@@ -25,21 +28,38 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
readonly ISystemIdentityFactory systemIdentityFactory;
/// <summary>
/// The <see cref="IPasswordHasher"/> for the <see cref="HomeController"/>
/// The <see cref="IPasswordHasher{TUser}"/> for the <see cref="HomeController"/>
/// </summary>
readonly IPasswordHasher<User> passwordHasher;
readonly ICryptographySuite cryptographySuite;
public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, IPasswordHasher<User> passwordHasher) : base(databaseContext, authenticationContextFactory)
/// <summary>
/// Construct a <see cref="HomeController"/>
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
/// <param name="tokenFactory">The value of <see cref="tokenFactory"/></param>
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/></param>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
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));
}
/// <summary>
/// Returns the version of the <see cref="Application"/>
/// </summary>
/// <returns><see cref="Application.Version"/></returns>
[Authorize]
[HttpGet]
public JsonResult Home() => Json(Assembly.GetExecutingAssembly().GetName().Version);
public JsonResult Home() => Json(Application.Version);
/// <summary>
/// Attempt to authenticate a <see cref="User"/> using <see cref="ApiController.ApiHeaders"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpPost]
public async Task<IActionResult> 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
@@ -8,24 +8,64 @@ using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
{
public abstract class ModelController<TModel> : ApiController
/// <summary>
/// An <see cref="ApiController"/> representing a <typeparamref name="TModel"/>
/// </summary>
/// <typeparam name="TModel">The model being represented</typeparam>
public abstract class ModelController<TModel> : ApiController where TModel : class
{
/// <summary>
/// The <see cref="ModelAttribute"/> of the <typeparamref name="TModel"/>
/// </summary>
protected static readonly ModelAttribute ModelAttribute = (ModelAttribute)typeof(TModel).GetCustomAttributes(typeof(ModelAttribute), true).First();
/// <summary>
/// Construct a <see cref="ModelController{TModel}"/>
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) : base(databaseContext, authenticationContextFactory) { }
/// <summary>
/// Attempt to create a <paramref name="model"/>
/// </summary>
/// <param name="model">The <typeparamref name="TModel"/> being created</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpPut]
public virtual Task<IActionResult> Create([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
/// <summary>
/// Attempt to read a <typeparamref name="TModel"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpGet]
public virtual Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
/// <summary>
/// Attempt to update a <paramref name="model"/>
/// </summary>
/// <param name="model">The <typeparamref name="TModel"/> being updated</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpPost]
public virtual Task<IActionResult> Update([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
/// <summary>
/// Attempt to delete a <paramref name="model"/>
/// </summary>
/// <param name="model">The <typeparamref name="TModel"/> being deleted</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpDelete]
public virtual Task<IActionResult> Delete([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
/// <summary>
/// Attempt to list entries of the <typeparamref name="TModel"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpGet("/List")]
public virtual Task<IActionResult> List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
}
+11 -19
View File
@@ -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
/// </summary>
sealed class Application
{
/// <summary>
/// The version of the <see cref="Application"/>
/// </summary>
public static readonly Version Version = Assembly.GetExecutingAssembly().GetName().Version;
/// <summary>
/// The <see cref="IConfiguration"/> for the <see cref="Application"/>
/// </summary>
@@ -64,17 +62,9 @@ namespace Tgstation.Server.Host.Core
var workingDir = Environment.CurrentDirectory;
var databaseConfigurationSection = configuration.GetSection(DatabaseConfiguration.Section);
services.Configure<DatabaseConfiguration>(databaseConfigurationSection);
var generalConfigSection = configuration.GetSection(GeneralConfiguration.Section);
services.Configure<GeneralConfiguration>(generalConfigSection);
services.AddMvc();
services.AddOptions();
var signingKey = generalConfigSection.Get<GeneralConfiguration>().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<DatabaseConfiguration>();
void ConfigureDatabase(DbContextOptionsBuilder builder)
@@ -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 <see cref="ISystemIdentityFactory"/> for the <see cref="AuthenticationContextFactory"/>
/// </summary>
readonly ISystemIdentityFactory systemIdentityFactory;
/// <summary>
/// The <see cref="IDatabaseContext"/> for the <see cref="AuthenticationContextFactory"/>
/// </summary>
@@ -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
{
/// <summary>
/// The length of secure strings used in the application
/// Generates a secure set of <see cref="byte"/>s
/// </summary>
public const int SecureStringLength = 40;
/// <summary>
/// Generates a secure ascii <see cref="string"/> of length <see cref="SecureStringLength"/>
/// </summary>
/// <returns>A secure ascii <see cref="string"/> of length <see cref="SecureStringLength"/></returns>
static string GenerateSecureString()
/// <returns>A secure set of <see cref="byte"/>s</returns>
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);
}
/// <inheritdoc />
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;
}
}
}
@@ -19,6 +19,11 @@ namespace Tgstation.Server.Host.Security
/// </summary>
InstanceUser InstanceUser { get; }
/// <summary>
/// Get the value of a given <paramref name="rightsType"/>
/// </summary>
/// <param name="rightsType">The <see cref="RightsType"/> of the right to get</param>
/// <returns>The value of <paramref name="rightsType"/>. Note that if <see cref="InstanceUser"/> is <see langword="null"/> all <see cref="Instance"/> based rights will return 0</returns>
int GetRight(RightsType rightsType);
/// <summary>
@@ -8,8 +8,18 @@ namespace Tgstation.Server.Host.Security
/// </summary>
public interface IAuthenticationContextFactory
{
/// <summary>
/// The <see cref="IAuthenticationContext"/> the <see cref="IAuthenticationContextFactory"/> created
/// </summary>
IAuthenticationContext CurrentAuthenticationContext { get; }
/// <summary>
/// Create an <see cref="IAuthenticationContext"/> to populate <see cref="CurrentAuthenticationContext"/>
/// </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="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);
}
}
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Security
/// <summary>
/// Contains various cryptographic functions
/// </summary>
interface ICryptographySuite
public interface ICryptographySuite
{
/// <summary>
/// Sets a <see cref="User.PasswordHash"/> for a given <paramref name="user"/>
@@ -13,5 +13,13 @@ namespace Tgstation.Server.Host.Security
/// <param name="user">The <see cref="User"/> whos <see cref="User.PasswordHash"/> is to be set</param>
/// <param name="newPassword">The new password for the <see cref="User"/></param>
void SetUserPassword(User user, string newPassword);
/// <summary>
/// Checks a given <paramref name="password"/> matches a given <paramref name="user"/>'s <see cref="User.PasswordHash"/>. This may result in <see cref="User.PasswordHash"/> being modified and this should be persisted
/// </summary>
/// <param name="user">The <see cref="User"/> to check</param>
/// <param name="password">The password to check</param>
/// <returns><see langword="true"/> if <paramref name="password"/> matches the hash, <see langword="false"/> otherwise</returns>
bool CheckUserPassword(User user, string password);
}
}
@@ -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;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="TokenFactory"/>
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// Construct a <see cref="TokenFactory"/>
/// </summary>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/></param>
public TokenFactory(IOptions<GeneralConfiguration> generalConfigurationOptions) => generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
public static readonly byte[] TokenSigningKey = CryptographySuite.GetSecureBytes(256);
/// <inheritdoc />
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) };
@@ -19,15 +19,11 @@
</PropertyGroup>
<ItemGroup>
<None Remove="appsettings.Development.json" />
<None Remove="appsettings.Docker.json" />
<None Remove="appsettings.json" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="appsettings.Docker.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -17,9 +17,6 @@
}
}
},
"General": {
"TokenSigningKey": "Default"
},
"Database": {
"DatabaseType": "Sqlite",
"ConnectionString": "Fake"