Compare commits
4 Commits
6876b623ac
...
1086396ccd
Author | SHA1 | Date | |
---|---|---|---|
1086396ccd | |||
ca059ce75d | |||
8b6bcc7c37 | |||
a44fb7b278 |
@ -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>
|
||||
|
46
API/Controllers/AuthController.cs
Normal file
46
API/Controllers/AuthController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
12
API/DTO/Login/UserLoginDTO.cs
Normal file
12
API/DTO/Login/UserLoginDTO.cs
Normal 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!;
|
||||
}
|
||||
}
|
19
API/Hashing/HashingFactory.cs
Normal file
19
API/Hashing/HashingFactory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
API/Hashing/Interfaces/IHashingAlgorithm.cs
Normal file
9
API/Hashing/Interfaces/IHashingAlgorithm.cs
Normal 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);
|
||||
}
|
||||
}
|
9
API/Hashing/Interfaces/IHashingFactory.cs
Normal file
9
API/Hashing/Interfaces/IHashingFactory.cs
Normal 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
28
API/Hashing/Pbkdf2.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
|
10
API/Services/Interfaces/IUserManager.cs
Normal file
10
API/Services/Interfaces/IUserManager.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using API.DTO.Base;
|
||||
using API.DTO.Login;
|
||||
|
||||
namespace API.Services.Interfaces
|
||||
{
|
||||
public interface IUserManager
|
||||
{
|
||||
UserDTO? AuthenticateUser(UserLoginDTO loginDTO);
|
||||
}
|
||||
}
|
57
API/Services/UserManager.cs
Normal file
57
API/Services/UserManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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; }
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -9,7 +9,7 @@ namespace DAL.Models
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum HashingType
|
||||
{
|
||||
PBKDF2_SHA512_64_210000
|
||||
PBKDF2_SHA512_64_250000
|
||||
}
|
||||
|
||||
[Table("users")]
|
||||
|
Loading…
x
Reference in New Issue
Block a user