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>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0-preview.3.24172.4"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.7"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7"/>
<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>

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.Interfaces;
using API.Hashing;
using API.Hashing.Interfaces;
using API.Services;
using API.Services.Interfaces;
using DAL.Contexts;
using DAL.Models;
using Microsoft.EntityFrameworkCore;
using Serilog;
using System.Reflection;
using ConfigurationManager = Microsoft.Extensions.Configuration.ConfigurationManager;
using InvalidOperationException = System.InvalidOperationException;
namespace API
@ -40,6 +45,23 @@ namespace API
builder.Services.AddTransient<IYesAuthentication, YesAuthentication>();
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();
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 =>
{
entity.HasOne<Color>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired();
.HasForeignKey(e => e.originalId).IsRequired();
});
builder.Entity<AuditEvent>(entity =>
{
entity.HasOne<Event>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired();
.HasForeignKey(e => e.originalId).IsRequired();
});
builder.Entity<AuditGrant>(entity =>
{
entity.HasOne<Grant>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired();
.HasForeignKey(e => e.originalId).IsRequired();
});
builder.Entity<AuditImage>(entity =>
{
entity.HasOne<Image>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired();
.HasForeignKey(e => e.originalId).IsRequired();
});
builder.Entity<AuditPermission>(entity =>
{
entity.HasOne<Permission>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired();
.HasForeignKey(e => e.originalId).IsRequired();
});
builder.Entity<AuditSavedEvent>(entity =>
{
entity.HasOne<SavedEvent>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired();
.HasForeignKey(e => e.originalId).IsRequired();
});
builder.Entity<AuditUser>(entity =>
{
entity.HasOne<User>().WithMany(e => e.audits)
.HasForeignKey(e => e.id).IsRequired();
.HasForeignKey(e => e.originalId).IsRequired();
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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