Compare commits

...

4 Commits

Author SHA1 Message Date
1086396ccd Added authcontroller and logins 2024-07-09 18:03:42 -05:00
ca059ce75d Added hashing algorithm and factory 2024-07-09 18:03:05 -05:00
8b6bcc7c37 Package updates & downgrades to 8.xx 2024-07-09 18:01:38 -05:00
a44fb7b278 Added keys to audit tables 2024-07-09 18:01:12 -05:00
21 changed files with 238 additions and 28 deletions

View File

@ -10,10 +10,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.1"/> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.7"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0-preview.3.24172.4"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7"/>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2-dev-00338"/> <PackageReference Include="Serilog.AspNetCore" Version="8.0.2-dev-00338"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,46 @@
using API.DTO.Base;
using API.DTO.Login;
using API.Services.Interfaces;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace API.Controllers
{
[ApiController]
[Route("api/v1/[controller]")]
public class AuthController : ControllerBase
{
private readonly ILogger<AuthController> _logger;
private readonly IUserManager _userManager;
public AuthController(ILogger<AuthController> logger, IUserManager userManager)
{
_logger = logger;
_userManager = userManager;
}
[HttpPost("login")]
public ActionResult<UserDTO> login(UserLoginDTO userLogin)
{
UserDTO? user = _userManager.AuthenticateUser(userLogin);
if (user == null)
return new UnauthorizedResult();
Claim[] claims =
{
new Claim(ClaimTypes.NameIdentifier, user.id.ToString())
};
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
//todo confirm if this is accurate
AuthenticationProperties authProperties = new AuthenticationProperties();
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties);
return Ok(user);
}
}
}

View File

@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace API.DTO.Login
{
public class UserLoginDTO
{
public ulong phoneNumber { get; set; }
[MaxLength(100)]
public string password { get; set; } = null!;
}
}

View File

@ -0,0 +1,19 @@
using API.Hashing.Interfaces;
using DAL.Models;
namespace API.Hashing
{
public class HashingFactory : IHashingFactory
{
public IHashingAlgorithm? getAlgorithm(HashingType type)
{
switch (type)
{
case HashingType.PBKDF2_SHA512_64_250000:
return new Pbkdf2();
default:
return null;
}
}
}
}

View File

@ -0,0 +1,9 @@
namespace API.Hashing.Interfaces
{
public interface IHashingAlgorithm
{
public string hash(string password, out byte[] salt);
public string hash(string password, byte[] salt);
}
}

View File

@ -0,0 +1,9 @@
using DAL.Models;
namespace API.Hashing.Interfaces
{
public interface IHashingFactory
{
public IHashingAlgorithm? getAlgorithm(HashingType type);
}
}

28
API/Hashing/Pbkdf2.cs Normal file
View File

@ -0,0 +1,28 @@
using API.Hashing.Interfaces;
using System.Security.Cryptography;
using System.Text;
namespace API.Hashing
{
public class Pbkdf2 : IHashingAlgorithm
{
private const int KeySize = 512;
private const int Iterations = 250000;
private readonly HashAlgorithmName _algorithmName = HashAlgorithmName.SHA512;
public string hash(string password, out byte[] salt)
{
salt = RandomNumberGenerator.GetBytes(KeySize);
byte[] hash = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, Iterations, _algorithmName, KeySize);
return Convert.ToHexString(hash);
}
public string hash(string password, byte[] salt)
{
byte[] hash = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, Iterations, _algorithmName, KeySize);
return Convert.ToHexString(hash);
}
}
}

View File

@ -1,10 +1,15 @@
using API.Authentication; using API.Authentication;
using API.Authentication.Interfaces; using API.Authentication.Interfaces;
using API.Hashing;
using API.Hashing.Interfaces;
using API.Services; using API.Services;
using API.Services.Interfaces;
using DAL.Contexts; using DAL.Contexts;
using DAL.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Serilog; using Serilog;
using System.Reflection; using System.Reflection;
using ConfigurationManager = Microsoft.Extensions.Configuration.ConfigurationManager;
using InvalidOperationException = System.InvalidOperationException; using InvalidOperationException = System.InvalidOperationException;
namespace API namespace API
@ -40,6 +45,23 @@ namespace API
builder.Services.AddTransient<IYesAuthentication, YesAuthentication>(); builder.Services.AddTransient<IYesAuthentication, YesAuthentication>();
builder.Services.AddTransient<IColorAuthentication, ColorAuthentication>(); builder.Services.AddTransient<IColorAuthentication, ColorAuthentication>();
builder.Services.AddTransient<IHashingFactory, HashingFactory>();
builder.Services.AddTransient<IHashingAlgorithm, Pbkdf2>();
builder.Services.AddTransient<IUserManager, UserManager>(options =>
{
UserService userService = options.GetRequiredService<UserService>();
IHashingFactory hashingFactory = options.GetRequiredService<IHashingFactory>();
ILogger<UserManager> logger = options.GetRequiredService<ILogger<UserManager>>();
HashingType hashingType;
if (!Enum.TryParse(builder.Configuration["preferredHashingType"], out hashingType))
throw new InvalidOperationException($"preferredHashingType not one of {String.Join(", ", Enum.GetNames(typeof(HashingType)))}");
return new UserManager(userService, hashingFactory, logger, hashingType);
});
WebApplication app = builder.Build(); WebApplication app = builder.Build();
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())

View File

@ -0,0 +1,10 @@
using API.DTO.Base;
using API.DTO.Login;
namespace API.Services.Interfaces
{
public interface IUserManager
{
UserDTO? AuthenticateUser(UserLoginDTO loginDTO);
}
}

View File

@ -0,0 +1,57 @@
using API.DTO.Base;
using API.DTO.Login;
using API.Hashing.Interfaces;
using API.Services.Interfaces;
using DAL.Models;
namespace API.Services
{
public class UserManager : IUserManager
{
private readonly IHashingFactory _hashingFactory;
private readonly ILogger<UserManager> _logger;
private readonly HashingType _preferredHashingType;
private readonly UserService _userService;
public UserManager(UserService userService, IHashingFactory hashingFactory, ILogger<UserManager> logger, HashingType preferredHashingType)
{
_userService = userService;
_hashingFactory = hashingFactory;
_logger = logger;
_preferredHashingType = preferredHashingType;
}
public UserDTO? AuthenticateUser(UserLoginDTO loginDTO)
{
User? user = _userService.getNoAuthentication(x => x.phoneNumber == loginDTO.phoneNumber).FirstOrDefault();
if (user == null)
return null;
IHashingAlgorithm? hashingAlgorithm = _hashingFactory.getAlgorithm(user.hashingType);
if (hashingAlgorithm == null)
{
_logger.Log(LogLevel.Warning, "User id '{id}' has a hashing type '{hashingType}' that isn't recognized by factory '{factory}'. Not logging in.", user.id, user.hashingType, nameof(_hashingFactory));
return null;
}
string hashedPassword = hashingAlgorithm.hash(loginDTO.password, user.salt);
if (!hashedPassword.Equals(user.password))
{
_logger.Log(LogLevel.Information, "Failed login attempt for user id '{id}.", user.id);
return null;
}
if (user.hashingType != _preferredHashingType)
{
// todo The user is logged in at this point. Their hashing type needs to be updated, we need to rehash & salt the password and save it now.
}
UserDTO dto = new UserDTO();
dto.adaptFromModel(user);
return dto;
}
}
}

View File

@ -94,43 +94,43 @@ namespace DAL.Contexts
builder.Entity<AuditColor>(entity => builder.Entity<AuditColor>(entity =>
{ {
entity.HasOne<Color>().WithMany(e => e.audits) entity.HasOne<Color>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired(); .HasForeignKey(e => e.originalId).IsRequired();
}); });
builder.Entity<AuditEvent>(entity => builder.Entity<AuditEvent>(entity =>
{ {
entity.HasOne<Event>().WithMany(e => e.audits) entity.HasOne<Event>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired(); .HasForeignKey(e => e.originalId).IsRequired();
}); });
builder.Entity<AuditGrant>(entity => builder.Entity<AuditGrant>(entity =>
{ {
entity.HasOne<Grant>().WithMany(e => e.audits) entity.HasOne<Grant>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired(); .HasForeignKey(e => e.originalId).IsRequired();
}); });
builder.Entity<AuditImage>(entity => builder.Entity<AuditImage>(entity =>
{ {
entity.HasOne<Image>().WithMany(e => e.audits) entity.HasOne<Image>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired(); .HasForeignKey(e => e.originalId).IsRequired();
}); });
builder.Entity<AuditPermission>(entity => builder.Entity<AuditPermission>(entity =>
{ {
entity.HasOne<Permission>().WithMany(e => e.audits) entity.HasOne<Permission>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired(); .HasForeignKey(e => e.originalId).IsRequired();
}); });
builder.Entity<AuditSavedEvent>(entity => builder.Entity<AuditSavedEvent>(entity =>
{ {
entity.HasOne<SavedEvent>().WithMany(e => e.audits) entity.HasOne<SavedEvent>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired(); .HasForeignKey(e => e.originalId).IsRequired();
}); });
builder.Entity<AuditUser>(entity => builder.Entity<AuditUser>(entity =>
{ {
entity.HasOne<User>().WithMany(e => e.audits) entity.HasOne<User>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired(); .HasForeignKey(e => e.originalId).IsRequired();
}); });
} }
} }

View File

@ -7,12 +7,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0-preview.3.24172.4"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0-preview.3.24172.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.0"/> <PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.5"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -6,7 +6,6 @@ namespace DAL.Models.Audits
{ {
[Index("id", Name = "audit_colors_colors_id_fk")] [Index("id", Name = "audit_colors_colors_id_fk")]
[Table("audit_colors")] [Table("audit_colors")]
[Keyless]
public class AuditColor : AuditModel<Color> public class AuditColor : AuditModel<Color>
{ {
[Column("red")] [Column("red")]
@ -26,7 +25,7 @@ namespace DAL.Models.Audits
{ {
return new Color return new Color
{ {
id = id, id = originalId,
red = red, red = red,
blue = blue, blue = blue,
green = green, green = green,

View File

@ -6,7 +6,6 @@ namespace DAL.Models.Audits
{ {
[Table("audit_event")] [Table("audit_event")]
[Index("id", Name = "audit_events_events_id_fk")] [Index("id", Name = "audit_events_events_id_fk")]
[Keyless]
public class AuditEvent : AuditModel<Event> public class AuditEvent : AuditModel<Event>
{ {
[Column("savedEventId")] [Column("savedEventId")]
@ -32,7 +31,7 @@ namespace DAL.Models.Audits
{ {
return new Event return new Event
{ {
id = id, id = originalId,
savedEventId = savedEventId, savedEventId = savedEventId,
name = name, name = name,
bgColorId = bgColorId, bgColorId = bgColorId,

View File

@ -6,7 +6,6 @@ namespace DAL.Models.Audits
{ {
[Table("audit_grants")] [Table("audit_grants")]
[Index("id", Name = "audit_grants_grants_id_fk")] [Index("id", Name = "audit_grants_grants_id_fk")]
[Keyless]
public class AuditGrant : AuditModel<Grant> public class AuditGrant : AuditModel<Grant>
{ {
[Column("name")] [Column("name")]
@ -20,7 +19,7 @@ namespace DAL.Models.Audits
{ {
return new Grant return new Grant
{ {
id = id, id = originalId,
name = name, name = name,
permissionId = permissionId, permissionId = permissionId,
updated = updated, updated = updated,

View File

@ -6,7 +6,6 @@ namespace DAL.Models.Audits
{ {
[Table("audit_images")] [Table("audit_images")]
[Index("id", Name = "audit_images_images_id_fk")] [Index("id", Name = "audit_images_images_id_fk")]
[Keyless]
public class AuditImage : AuditModel<Image> public class AuditImage : AuditModel<Image>
{ {
[Column("name")] [Column("name")]
@ -21,7 +20,7 @@ namespace DAL.Models.Audits
{ {
return new Image return new Image
{ {
id = id, id = originalId,
name = name, name = name,
filename = filename, filename = filename,
updated = updated, updated = updated,

View File

@ -5,9 +5,13 @@ namespace DAL.Models.Audits
{ {
public abstract class AuditModel<TModel> public abstract class AuditModel<TModel>
{ {
[Key]
[Column("id")] [Column("id")]
public ulong id { get; set; } public ulong id { get; set; }
[Column("originalId")]
public ulong originalId { get; set; }
[Column("updated")] [Column("updated")]
[DataType("datetime")] [DataType("datetime")]
public DateTime updated { get; set; } public DateTime updated { get; set; }

View File

@ -6,7 +6,6 @@ namespace DAL.Models.Audits
{ {
[Table("audit_permissions")] [Table("audit_permissions")]
[Index("id", Name = "audit_permissions_permissions_id_fk")] [Index("id", Name = "audit_permissions_permissions_id_fk")]
[Keyless]
public class AuditPermission : AuditModel<Permission> public class AuditPermission : AuditModel<Permission>
{ {
[Column("name")] [Column("name")]
@ -17,7 +16,7 @@ namespace DAL.Models.Audits
{ {
return new Permission return new Permission
{ {
id = id, id = originalId,
name = name, name = name,
updated = updated, updated = updated,
updater = updater updater = updater

View File

@ -6,7 +6,6 @@ namespace DAL.Models.Audits
{ {
[Table("audit_savedEvents")] [Table("audit_savedEvents")]
[Index("id", Name = "audit_savedEvents_savedEvents_id_fk")] [Index("id", Name = "audit_savedEvents_savedEvents_id_fk")]
[Keyless]
public class AuditSavedEvent : AuditModel<SavedEvent> public class AuditSavedEvent : AuditModel<SavedEvent>
{ {
[Column("name")] [Column("name")]
@ -26,7 +25,7 @@ namespace DAL.Models.Audits
{ {
return new SavedEvent return new SavedEvent
{ {
id = id, id = originalId,
name = name, name = name,
bgColorId = bgColorId, bgColorId = bgColorId,
fgColorId = fgColorId, fgColorId = fgColorId,

View File

@ -6,7 +6,6 @@ namespace DAL.Models.Audits
{ {
[Table("audit_users")] [Table("audit_users")]
[Index("id", Name = "audit_users_users_id_fk")] [Index("id", Name = "audit_users_users_id_fk")]
[Keyless]
public class AuditUser : AuditModel<User> public class AuditUser : AuditModel<User>
{ {
[Column("firstName")] [Column("firstName")]
@ -31,7 +30,7 @@ namespace DAL.Models.Audits
{ {
return new User return new User
{ {
id = id, id = originalId,
firstName = firstName, firstName = firstName,
lastName = lastName, lastName = lastName,
phoneNumber = phoneNumber, phoneNumber = phoneNumber,

View File

@ -9,7 +9,7 @@ namespace DAL.Models
[JsonConverter(typeof(JsonStringEnumConverter))] [JsonConverter(typeof(JsonStringEnumConverter))]
public enum HashingType public enum HashingType
{ {
PBKDF2_SHA512_64_210000 PBKDF2_SHA512_64_250000
} }
[Table("users")] [Table("users")]