Authorization and authentication in place (not tested)

Authorization issues JWT and fingerprint
Created authentication middleware -> validate JWT, fingerprint and check
signed headers
Basically rewrote login (now AuthorizationController)
Name numnerous model changes (I think all reflected in DB create script)
Added to appsettings (hide none dev cause secret keys)
Some changes to JS ecc class for signing
security
Griffiths Lott 3 years ago
parent a3b1ff40d8
commit 09d868fc10
Signed by: gprog
GPG Key ID: AF76DEDF1D66B847
  1. 3
      .gitignore
  2. 5
      TODO.txt
  3. 259
      backend/Controllers/AuthorizationController.cs
  4. 97
      backend/Controllers/Login.cs
  5. 24
      backend/ExcelSave.cs
  6. 71
      backend/Hasher.cs
  7. 105
      backend/JwtAuthenticationMiddelware.cs
  8. 2
      backend/Migrations/20230314000540_InitialCreate.Designer.cs
  9. 2
      backend/Migrations/MyDbContextModelSnapshot.cs
  10. 4
      backend/Models/AuthLevels.cs
  11. 7
      backend/Models/Auths.cs
  12. 34
      backend/Models/LoginDetails.cs
  13. 2
      backend/Models/User.cs
  14. 5
      backend/PPTLDbContext.cs
  15. 32
      backend/Program.cs
  16. 8
      backend/appsettings.Development.json
  17. 6
      backend/backend.csproj
  18. 12
      backend/logs/portfolioportal20230319.txt
  19. 4
      database/CREATE_TABLES.sql
  20. 3
      src/components/FileUpload.vue
  21. 36
      src/ecc.js

3
.gitignore vendored

@ -30,4 +30,5 @@ pnpm-debug.log*
.leaf-ecc.txt
# DO INCULDE
!portfolio-portal-pub.key
!portfolio-portal-pub.key
!backend/appsettings.json

@ -0,0 +1,5 @@
###BACKEND
[ ] Set up production settings (JWT secrets, logging, db connection)
[X] Confirm working logging
[ ] Normalize appsettings case convention
[ ] TemplateHeaders controler use database, not config

@ -0,0 +1,259 @@
using Microsoft.AspNetCore.Mvc;
using Hasher;
using Models;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace backend.Controllers
{
[Route("api/login")]
[ApiController]
public class AuthorizationController : ControllerBase
{
private readonly IConfiguration _config;
private readonly ILogger _logger;
public AuthorizationController(IConfiguration config, ILogger<AuthorizationController> logger)
{
_config = config;
_logger = logger;
}
/// <summary>
/// Processes a user login request by checking the provided email and password against the database,
/// generating a JWT if successful, and setting the appropriate cookies in the response.
/// </summary>
/// <param name="requestBody">The login details submitted in the request body.</param>
/// <returns>An ActionResult indicating the success or failure of the login attempt.</returns>
[HttpPost]
public async Task<ActionResult> ProcessLogin(LoginDetails requestBody)
{
// Log the request (password is censored)
_logger.LogDebug($"Login request with body: {requestBody.ToString()}");
User? user;
using (var db = new PortfolioPortalDbContext())
{
// Check for a user with that email in the database
user = db.Users.FirstOrDefault(u => u.Email.ToLower() == requestBody.email.ToLower());
}
if (user != null)
{
_logger.LogDebug($"Found user: {user}");
// Make sure the user is enabled
if (!user.IsEnabled)
{ _logger.LogWarning($"Disabled user {user.Username} attempted to log in.");
return Unauthorized("Account disabled.");
}
// Check the password against the hash from the database
if (Argon2dHasher.VerifyArgon2Hash(requestBody.password, user.PasswordHash))
{
_logger.LogDebug($"{user.Username} successfully authenticated!");
// Reset the users failed attempts to 0
Task attemptResetTask = ResetFailedAttempts(user);
// Get the user authorizations
List<AuthWithLevel> auths = GetUserAuths(user);
if (auths.Count == 0) {
_logger.LogWarning($"{user.Username} has not authorization levels!");
return Unauthorized("No authorizations.");
}
// Generate a random string for fingerprinting, hashed used in JWT
string[] fingerprintSet = FingerPrinter.GetFingerprintSet();
string fingerprint = fingerprintSet[0];
string hashedFp = fingerprintSet[1];
int authExpiryMinutes = Convert.ToInt32(_config["JwtSettings:expiryTime"]);
DateTime authExpiration = DateTime.Now.AddMinutes(authExpiryMinutes);
string jwToken = GenerateJwt(user.UserId, auths,requestBody.clientVerificationKey,hashedFp, authExpiration);
// Add the JWT to the reponse cookies
SetHardenedCookie(HttpContext, "Authorization", jwToken, authExpiration);
SetHardenedCookie(HttpContext, "UserFingerprint", fingerprint, authExpiration);
_logger.LogDebug($"{user.Username} authorized with {Response.Cookies}");
_logger.LogInformation($"Authorized {user} with token {jwToken}");
await attemptResetTask;
return Ok($"Successfuly authorized! Session is valid for {authExpiryMinutes} minutes.");
}
// Password did not match records
// Record the failed attempt
bool accountLocked = RecordFailedAttempt(user);
if (accountLocked)
{
return Unauthorized("Account disabled.");
}
return Unauthorized("Incorrect username or password!");
}
// No User found | It's best not to distinguish between invalid pw and unknown un for security reasons
return Unauthorized("Invalid username or password.");
}
/// <summary>
/// Generates a JSON Web Token (JWT) for the specified user and authorizations using the provided client key, fingerprint hash, and expiry date.
/// </summary>
/// <param name="userId">The ID of the user for whom the JWT is being generated.</param>
/// <param name="authorizations">The list of authorizations for the user.</param>
/// <param name="clientKey">The client key for verifying header signatures.</param>
/// <param name="fingerprintHash">The hashed fingerprint for user identification.</param>
/// <param name="expiry">The expiry date for the JWT.</param>
/// <returns>The generated JWT.</returns>
private string GenerateJwt(int userId, List<AuthWithLevel> authorizations, string clientKey, string fingerprintHash, DateTime expiry)
{
// Create a new list of claims for the JWT
List<Claim> claimsList = new List<Claim>
{
// Add the user ID, client key, and fingerprint hash as claims
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim("ClientPublicKey", clientKey),
new Claim("Fingerprint", fingerprintHash)
};
// Add authorization claims for each authorization in the list
foreach (var auth in authorizations)
{
claimsList.Add(new Claim("Auth", auth.AuthLevel.AuthLevel));
}
// Convert the claims list to an array
var claims = claimsList.ToArray();
// Get the secret key from the app configuration and create signing credentials
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JwtSettings:SecretKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
// Create a new JWT with the specified issuer, audience, claims, expiry, and signing credentials
var token = new JwtSecurityToken(
issuer: _config["JwtSettings:issuer"],
audience: _config["JwtSettings:audience"],
claims: claims,
expires: expiry,
signingCredentials: creds);
// Write the JWT to a string and log it for debugging purposes
string jwToken = new JwtSecurityTokenHandler().WriteToken(token);
_logger.LogDebug($"New JWT created for {userId}\n{jwToken}");
return jwToken;
}
/// <summary>
/// Resets the failed attempt count for the specified user to zero in the database.
/// </summary>
/// <param name="user">The user whose failed attempt count is being reset.</param>
private async Task ResetFailedAttempts(User user)
{
// Create a new database context using the PortfolioPortalDbContext class
await using (var db = new PortfolioPortalDbContext())
{
// Set the user's failed attempt count to zero
user.FailedAttempts = 0;
// Save changes to the database asynchronously
await db.SaveChangesAsync();
}
}
/// <summary>
/// Retrieves the authorization levels for the specified user from the database.
/// </summary>
/// <param name="user">The user whose authorization levels are being retrieved.</param>
/// <returns>A list of AuthWithLevel objects representing the user's authorization levels.</returns>
private List<AuthWithLevel> GetUserAuths(User user)
{
List<AuthWithLevel> authsWithLevels;
// Create a new database context using the PortfolioPortalDbContext class
using (var db = new PortfolioPortalDbContext())
{
// Reset the user's failed attempt count to zero
user.FailedAttempts = 0;
// Save changes to the database
db.SaveChanges();
// Retrieve the authorization levels for the specified user from the database
authsWithLevels = (from auth in db.Auths
join level in db.AuthLevels on auth.AuthLevelId equals level.AuthLevelId into authLevelGroup
from authLevel in authLevelGroup.DefaultIfEmpty()
where auth.UserId == user.UserId
select new AuthWithLevel
{
Auth = auth,
AuthLevel = authLevel
}).ToList();
// Log a message indicating the user's authorization levels
_logger.LogDebug($"{user.Username} auths: {authsWithLevels}");
}
// Return a list of Auth objects representing the user's authorization levels
return authsWithLevels;
}
/// <summary>
/// Records a failed login attempt for the specified user, and updates the user's failed attempt count and account lock status if necessary.
/// </summary>
/// <param name="user">The user whose login attempt failed.</param>
/// <returns>A boolean value indicating whether the user's account is currently enabled.</returns>
private bool RecordFailedAttempt(User user)
{
// Create a new database context using the PortfolioPortalDbContext class
using (var db = new PortfolioPortalDbContext())
{
// Increment the user's failed attempt count
user.FailedAttempts += 1;
// Log a message indicating that the user's login attempt failed, and how many failed attempts they have made so far
_logger.LogInformation($"{user.Username} failed authentication! ({user.FailedAttempts}/5)");
// If the user has made too many failed attempts, lock their account
if (user.FailedAttempts > 4)
{
_logger.LogWarning($"{user.Username} has had their account locked for too many failed attempts!");
user.IsEnabled = false;
}
// Save changes to the database
db.SaveChanges();
}
// Return a boolean value indicating whether the user's account is currently enabled
return user.IsEnabled;
}
/// <summary>
/// Sets a hardened cookie in the HTTP response with the given cookie name, value, and expiry time.
/// </summary>
/// <param name="httpContext">The HttpContext object for the current request.</param>
/// <param name="cookieName">The name of the cookie.</param>
/// <param name="cookieValue">The value of the cookie.</param>
/// <param name="expiry">The expiry time of the cookie.</param>
private void SetHardenedCookie(HttpContext httpContext, string cookieName, string cookieValue, DateTime expiry)
{
// Set the cookie options
var cookieOptions = new CookieOptions
{
HttpOnly = true, // The cookie cannot be accessed by JavaScript
Secure = true, // The cookie can only be sent over HTTPS
SameSite = SameSiteMode.Strict, // The cookie can only be sent to the same domain that sent it
Expires = expiry, // The cookie expires at the given time (see appsettings.json)
IsEssential = true // The cookie is essential for the operation of the application (prevents browesers from blocking)
};
// Add the "__Secure-" prefix to the cookie name to indicate that it is a hardened cookie
cookieName = "__Secure-" + cookieName;
// Append the cookie to the HTTP response
httpContext.Response.Cookies.Append(cookieName, cookieValue, cookieOptions);
}
}
}
//TODO: Consider separating the database access code into a separate repository class to follow the Repository pattern.
//This would help you decouple the controller from the data access layer, making the code more maintainable and testable.

@ -1,97 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using Hasher;
using System.Linq;
using Models;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
namespace backend.Controllers
{
[Route("api/login")]
[ApiController]
public class Login : ControllerBase
{
private readonly IConfiguration _config;
public Login(IConfiguration config)
{
_config = config;
}
[HttpPost]
public void ProcessLogin([FromBody] dynamic requestBody)
{
string email = requestBody.username;
string password = requestBody.password;
string clientPublicKey = requestBody.client_verification_key;
// Try to get a name from the database
using (var db = new MyDbContext())
{
User user = db.Users.FirstOrDefault(u => u.Email.ToLower() == email.ToLower());
if (user != null)
{
int userId = user.UserId;
string passwordHash = user.PasswordHash;
// Check the hash
if (Argon2dHasher.VerifyArgon2Hash(password, passwordHash))
{
// Get the user authorizations
List<Auth> auths = db.Auths.Where((auth) => auth.UserId == userId).ToList();
// Generate a random string for fingerprinting
var fingerprint = GenerateRandomString(32);
//Create a JWT
// Claims: [userId, clientPublicKey, auths]
var claims = new[]
{
// Add the user ID as a subject claim
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
// Add the client public key as a custom claim
new Claim("ClientPublicKey", clientPublicKey)
// Add the fingerprint as a hashed custom claim
new Claim("Fingerprint", HashFingerprint(fingerprint))
};
// Add the user authorizations as custom claims
foreach (var auth in auths)
{
claims.Append(new Claim("Auth", auth.AuthId.ToString()));
}
}
}
else
{
//TODO Handle case where no user was found with the given email
}
}
}
// Helper function to generate a random string
public static string GetRandomString(int length)
{
var r = new Random();
return new String(Enumerable.Range(0, length).Select(n => (Char)(r.Next(32, 127))).ToArray());
}
// Helper function to hash the fingerprint using SHA-256
private string HashFingerprint(string fingerprint)
{
using var sha256 = SHA256.Create();
var hashedFingerprint = sha256.ComputeHash(Encoding.UTF8.GetBytes(fingerprint));
return Convert.ToBase64String(hashedFingerprint);
}
}
}

@ -0,0 +1,24 @@
using System;
using System.IO;
namespace customExcel
{
public class ExcelSaver
{
public static void SaveBase64ExcelFile(string base64ExcelData, string outputPath)
{
try
{
// Decode the Base64 string to a byte array
byte[] excelBytes = Convert.FromBase64String(base64ExcelData);
// Write the byte array to a file with the appropriate Excel file extension
File.WriteAllBytes(outputPath, excelBytes);
}
catch (Exception ex)
{
// Handle exceptions as needed
Console.WriteLine("Error saving Excel file: " + ex.Message);
}
}
}
}

@ -2,6 +2,8 @@ using Isopoh.Cryptography.Argon2;
using Isopoh.Cryptography.SecureArray;
using System.Security.Cryptography;
using System.Text;
using Chaos.NaCl;
namespace Hasher
{
@ -63,5 +65,74 @@ namespace Hasher
return valid;
}
}
/// <summary>
/// The FingerPrinter class provides methods to generate and hash fingerprints.
/// </summary>
public class FingerPrinter
{
/// <summary>
/// Helper function to generate a random string of a given length.
/// </summary>
/// <param name="length">The length of the string to generate.</param>
/// <returns>A random string of the specified length.</returns>
private static string GetRandomString(int length)
{
var r = new Random();
return new String(Enumerable.Range(0, length).Select(n => (Char)(r.Next(32, 127))).ToArray());
}
/// <summary>
/// Helper function to hash a fingerprint using SHA-256.
/// </summary>
/// <param name="fingerprint">The fingerprint to hash.</param>
/// <returns>The SHA-256 hash of the input fingerprint as a base64-encoded string.</returns>
private static string HashFingerprint(string fingerprint)
{
using var sha256 = SHA256.Create();
var hashedFingerprint = sha256.ComputeHash(Encoding.UTF8.GetBytes(fingerprint));
return Convert.ToBase64String(hashedFingerprint);
}
/// <summary>
/// Returns an array containing a randomly generated plain text fingerprint and its hashed value.
/// </summary>
/// <param name="length">The length of the plain text fingerprint to generate (default is 32).</param>
/// <returns>An array of two strings: the plain text fingerprint and its hashed value.</returns>
public static string[] GetFingerprintSet(int length = 32)
{
string plainText = GetRandomString(length);
string hashed = HashFingerprint(plainText);
return new string[] { plainText, hashed };
}
/// <summary>
/// Validates whether the specified fingerprint matches the provided hashed fingerprint using SHA256 encryption.
/// </summary>
/// <param name="fingerprint">The fingerprint string to be validated.</param>
/// <param name="fpHash">The hashed fingerprint to be compared against the fingerprint.</param>
/// <returns>Returns a boolean value indicating whether the fingerprint and its hash match or not.</returns>
public static bool ValidateFingerpint(string fingerprint, string fpHash)
{
using var sha256 = SHA256.Create();
var newFpHash = sha256.ComputeHash(Encoding.UTF8.GetBytes(fingerprint));
return Convert.ToBase64String(newFpHash) == fpHash ;
}
}
public class KeyHelper
{
private static byte[] FromBase64String(string input)
{
return Convert.FromBase64String(input);
}
public static bool validateSignature(string signature, string message, string publicKey)
{
return Ed25519.Verify(FromBase64String(signature), Encoding.UTF8.GetBytes(message), FromBase64String(publicKey));
}
}
}

@ -0,0 +1,105 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Hasher;
public class JwtAuthenticationMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private readonly ILogger _logger;
public JwtAuthenticationMiddleware(RequestDelegate next, IConfiguration configuration, ILogger<JwtAuthenticationMiddleware> logger)
{
_next = next;
_configuration = configuration;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogDebug($"Request cookies: {context.Request.Cookies}");
// Read the Authorization and UserFingerprint cookies
context.Request.Cookies.TryGetValue("__Secure-Authorization", out string token);
context.Request.Cookies.TryGetValue("__Secure-UserFingerprint", out string fingerprint);
if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(fingerprint))
{
// Validate the JWT and fingerprint
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = _configuration["JwtSettings:issuer"],
ValidAudience = _configuration["JwtSettings:audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSettings:SecretKey"])),
ClockSkew = TimeSpan.Zero
};
try
{
var tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken);
// Extract claims from the validated token
var jwtToken = (JwtSecurityToken)validatedToken;
var claims = jwtToken.Claims;
// Validate the fingerprint
var fingerprintClaim = claims.FirstOrDefault(c => c.Type == "Fingerprint");
if (fingerprintClaim != null)
{
var hashedFingerprint = fingerprintClaim.Value;
_logger.LogDebug($"HashedFp: {hashedFingerprint} | Cookie Fp: {fingerprintClaim}");
// Verify the fingerprint here, based on how you generate the fingerprint hash
// If the fingerprint is valid, set the User property with the claims
if (FingerPrinter.ValidateFingerpint(fingerprint,hashedFingerprint))
{
// Record the claims so that they can be accessed by controllers
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.Select(c => c.Type == "Auth" ? new Claim(ClaimTypes.Role, c.Value) : c), "Jwt"));
}
}
// Validate signed headers
string payloadSig = context.Request.Headers["X-Payload-Signature"];
// Read the request body content
string payload;
using (var streamReader = new StreamReader(context.Request.Body))
{
payload = await streamReader.ReadToEndAsync();
}
string clientPublicKey = claims.FirstOrDefault(c => c.Type == "ClientPublicKey").Value;
_logger.LogDebug($"PayloadSig: {payloadSig} | payload: {payload} | Key: {clientPublicKey}");
//FIXME This could use some testing
try
{
bool validSig = KeyHelper.validateSignature(payloadSig, payload, clientPublicKey);
_logger.LogDebug($"Valid sig!");
}
catch
{
_logger.LogDebug($"INVALID sig!");
}
}
catch (SecurityTokenValidationException)
{
//FIXME Log the validation error or perform necessary actions
_logger.LogWarning("Invlaid JWT Exception!");
}
}
// Call the next middleware in the pipeline
await _next(context);
}
}

@ -11,7 +11,7 @@ using Models;
namespace backend.Migrations
{
[DbContext(typeof(MyDbContext))]
[DbContext(typeof(PortfolioPortalDbContext))]
[Migration("20230314000540_InitialCreate")]
partial class InitialCreate
{

@ -10,7 +10,7 @@ using Models;
namespace backend.Migrations
{
[DbContext(typeof(MyDbContext))]
[DbContext(typeof(PortfolioPortalDbContext))]
partial class MyDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)

@ -1,10 +1,10 @@
namespace Models
{
public class AuthLevel
public class AuthLevels
{
public int AuthLevelId { get; set; }
public string AuthLevelAuthLevel { get; set; }
public string AuthLevel { get; set; }
public string Description { get; set; }
}
}

@ -7,4 +7,11 @@ namespace Models
public int UserId { get; set; }
public int AuthLevelId { get; set; }
}
public class AuthWithLevel
{
public Auth Auth { get; set; }
public AuthLevels AuthLevel { get; set; }
}
}

@ -0,0 +1,34 @@
using Newtonsoft.Json;
namespace Models
{
// TODO: Do we want them to be able to log in with username?
// Could be nice, but adds complexity not worth at the moment
public class LoginDetails
{
// Email associated with an account
public string email;
// Plain text password, must be hashed (Argon2d) and compared against database
public string password;
// This is a Ed25519 public key which can be used to verify that
// a header was signed by a client. It will be included in the JWT
public string clientVerificationKey;
/// <summary>
/// Returns a string representation of the object but replaces all of the characters in password with *.
/// </summary>
/// <returns>A string representation of the object with password replaced by *</returns>
public override string ToString()
{
// Create a copy of the object so that we can modify its password field
var copy = (LoginDetails)this.MemberwiseClone();
// Replace the characters in the password field with asterisks
copy.password = new string('*', copy.password.Length);
// Return the string representation of the modified copy
return JsonConvert.SerializeObject(copy, Formatting.Indented);
}
}
}

@ -13,5 +13,7 @@ namespace Models
public DateTimeOffset PasswordUpdated { get; set; }
public int CompanyId { get; set; }
public DateTimeOffset LastLogin { get; set; }
public bool IsEnabled { get; set; }
public int FailedAttempts { get; set; }
}
}

@ -3,11 +3,11 @@ using Models;
namespace Models
{
public class MyDbContext : DbContext
public class PortfolioPortalDbContext : DbContext
{
public DbSet<Portfolio> Portfolios { get; set; }
public DbSet<PortfolioData> PortfolioData { get; set; }
public DbSet<AuthLevel> AuthLevels { get; set; }
public DbSet<AuthLevels> AuthLevels { get; set; }
public DbSet<Auth> Auths { get; set; }
public DbSet<Company> Companies { get; set; }
public DbSet<User> Users { get; set; }
@ -16,6 +16,7 @@ namespace Models
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//FIXME: This couldn
optionsBuilder.UseSqlServer("Server=localhost,3341;Database=PortfolioPortalTest;User Id=SA;Password=LEAF-portal-test-enviroment1;");
}
}

@ -1,12 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Models;
using Serilog;
//TODO Make this configurable from appsettings
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.WriteTo.File("logs/portfolioportal.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();
Log.Information("Starting Portfolio Portal ASP.NET Core Backend");
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers()
.AddNewtonsoftJson();
builder.Services.AddDbContext<MyDbContext>();
builder.Services.AddDbContext<PortfolioPortalDbContext>();
builder.Logging.AddSerilog();
var app = builder.Build();
@ -16,4 +27,21 @@ app.UseAuthorization();
app.MapControllers();
// Custom JWT handling with Ed25519 validation
app.UseMiddleware<JwtAuthenticationMiddleware>();
// If this is the production build make sure we're always using HTTPS
if (!app.Environment.IsDevelopment())
{
// If an unhandled exception is encountered redirect to Error endpoint
//TODO Create an error endpoint
app.UseExceptionHandler("/Error");
app.UseHsts();
}
else
{
// Generates a rich error page with detailed information such as the exception type, message, stack trace, request details, and more
app.UseDeveloperExceptionPage();
}
app.Run();

@ -1,13 +1,19 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"TestConnectionString" : "Server=localhost,3341;Database=PortfolioPortalTest;User Id=SA;Password=LEAF-portal-test-enviroment1;"
},
"JwtSettings": {
"issuer": "LEAFPPTL-Test",
"audience": "PortfolioPartner-Test",
"SecretKey": "DEV-KEY-PortPortal",
"expirtyTime": 60
},
"AllowedHosts": "*",
"UploadSavePath": "../Uploaded/",
"validateData": false,

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chaos.NaCl.Standard" Version="1.0.0" />
<PackageReference Include="ExcelDataReader" Version="3.7.0-develop00310" />
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="1.1.12" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.2" />
@ -20,6 +21,11 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.1-dev-10338" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

@ -0,0 +1,12 @@
2023-03-19 00:31:01.401 -04:00 [INF] Starting Portfolio Portal ASP.NET Core Backend
2023-03-19 00:31:01.529 -04:00 [DBG] Registered model binder providers, in the following order: ["Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BinderTypeModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ServicesModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.HeaderModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.FloatingPointTypeModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.EnumTypeModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DateTimeModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.TryParseModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.SimpleTypeModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.CancellationTokenModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ByteArrayModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.FormFileModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.FormCollectionModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.KeyValuePairModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.DictionaryModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ArrayModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.CollectionModelBinderProvider","Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexObjectModelBinderProvider"]
2023-03-19 00:31:01.571 -04:00 [DBG] Hosting starting
2023-03-19 00:31:01.586 -04:00 [INF] Now listening on: http://localhost:5252
2023-03-19 00:31:01.586 -04:00 [DBG] Loaded hosting startup assembly backend
2023-03-19 00:31:01.586 -04:00 [INF] Application started. Press Ctrl+C to shut down.
2023-03-19 00:31:01.586 -04:00 [INF] Hosting environment: Development
2023-03-19 00:31:01.586 -04:00 [INF] Content root path: /home/g/Code/JavaScript/portfolio_vue/backend
2023-03-19 00:31:01.587 -04:00 [DBG] Hosting started
2023-03-19 00:31:41.888 -04:00 [INF] Application is shutting down...
2023-03-19 00:31:41.889 -04:00 [DBG] Hosting stopping
2023-03-19 00:31:41.895 -04:00 [DBG] Hosting stopped

@ -1,3 +1,5 @@
--//TODO: Review reservered words
-- Auth table
CREATE TABLE AuthLevels (
AuthLevelID TINYINT IDENTITY(1,1) PRIMARY KEY,
@ -41,6 +43,8 @@ CREATE TABLE Users (
PasswordUpdated DATETIMEOFFSET,
CompanyID SMALLINT FOREIGN KEY REFERENCES Company(CompanyID) NOT NULL,
LastLogin DATETIMEOFFSET,
FailedAttempts INT NOT NULL DEFAULT 0,
IsEnabled BIT NOT NULL DEFAULT 0
);
-- Link the company and users tables
ALTER TABLE Company

@ -12,6 +12,9 @@
</div>
</template>
TODO: When sending binary data like an Excel file, you should consider using a different encoding, such as Base64, to ensure that the binary data is transmitted correctly.
Base64 encoding converts binary data into a string representation that can be safely transmitted over text-based protocols like HTTP.
<script>
import { read, utils } from "xlsx";

@ -34,35 +34,6 @@ export class ECCManager {
return ed.sign(msg, this.CSK);
}
/**
* Adds a timestamp and digital signature to a given set of HTTP headers.
* @async
* @param {Headers} currentHeaders - The current set of HTTP headers to add the timestamp and signature to.
* @returns {Promise<Headers>} A Promise that resolves to the updated set of HTTP headers.
*/
async addTimeStampHeaders(currentHeaders) {
// Get the current timestamp as an ISO string
const timestamp = new Date().toISOString();
// Create a new TextEncoder object
const encoder = new TextEncoder();
// Convert the timestamp to a Uint8Array object using the TextEncoder object
const msg = encoder.encode(timestamp);
// Sign the Uint8Array message using the ed25519 algorithm and the client's private key
const signature = await ed.sign(msg, this.CSK);
// Append the X-Timestamp header to the given set of HTTP headers, with the value set to the timestamp string
currentHeaders.append("X-Timestamp", timestamp);
// Append the X-TSignature header to the given set of HTTP headers, with the value set to the signature Uint8Array object
currentHeaders.append("X-TSignature", signature);
// Return the updated set of HTTP headers as a Promise
return currentHeaders;
}
/**
* Adds a header with the digital signature of a given payload to a set of HTTP headers.
* @async
@ -78,8 +49,11 @@ export class ECCManager {
// Sign the payload using the ed25519 algorithm and the client's private key
const signature = await ed.sign(payloadBytes, this.CSK);
// Append the X-Payload-Signature header to the given set of HTTP headers, with the value set to the signature Uint8Array object
currentHeaders.append("X-Payload-Signature", signature);
//FIXME depricated function | Encode the signature using base64
const base64Signature = btoa(String.fromCharCode.apply(null, signature));
// Append the X-Payload-Signature header to the given set of HTTP headers, with the value set to the base64-encoded signature
currentHeaders.append("X-Payload-Signature", base64Signature);
// Return the updated set of HTTP headers as a Promise
return currentHeaders;

Loading…
Cancel
Save