diff --git a/API/Controllers/AuthController.cs b/API/Controllers/AuthController.cs new file mode 100644 index 0000000..3eeafaa --- /dev/null +++ b/API/Controllers/AuthController.cs @@ -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 _logger; + private readonly IUserManager _userManager; + + public AuthController(ILogger logger, IUserManager userManager) + { + _logger = logger; + _userManager = userManager; + } + + [HttpPost("login")] + public ActionResult 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); + } + } +} diff --git a/API/DTO/Login/UserLoginDTO.cs b/API/DTO/Login/UserLoginDTO.cs new file mode 100644 index 0000000..cde6a8d --- /dev/null +++ b/API/DTO/Login/UserLoginDTO.cs @@ -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!; + } +} diff --git a/API/Program.cs b/API/Program.cs index 99d8cbe..05495c4 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -3,10 +3,13 @@ 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 @@ -46,6 +49,19 @@ namespace API builder.Services.AddTransient(); + builder.Services.AddTransient(options => + { + UserService userService = options.GetRequiredService(); + IHashingFactory hashingFactory = options.GetRequiredService(); + ILogger logger = options.GetRequiredService>(); + + 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()) diff --git a/API/Services/Interfaces/IUserManager.cs b/API/Services/Interfaces/IUserManager.cs new file mode 100644 index 0000000..29658ec --- /dev/null +++ b/API/Services/Interfaces/IUserManager.cs @@ -0,0 +1,10 @@ +using API.DTO.Base; +using API.DTO.Login; + +namespace API.Services.Interfaces +{ + public interface IUserManager + { + UserDTO? AuthenticateUser(UserLoginDTO loginDTO); + } +} diff --git a/API/Services/UserManager.cs b/API/Services/UserManager.cs new file mode 100644 index 0000000..720c903 --- /dev/null +++ b/API/Services/UserManager.cs @@ -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 _logger; + private readonly HashingType _preferredHashingType; + private readonly UserService _userService; + + public UserManager(UserService userService, IHashingFactory hashingFactory, ILogger 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; + } + } +}