diff --git a/src/Core/API/API.Core/Controllers/AuthController.cs b/src/Core/API/API.Core/Controllers/AuthController.cs index 18668d6..aaf22db 100644 --- a/src/Core/API/API.Core/Controllers/AuthController.cs +++ b/src/Core/API/API.Core/Controllers/AuthController.cs @@ -1,12 +1,12 @@ -using BusinessLayer.Services; using DataAccessLayer.Entities; using Microsoft.AspNetCore.Mvc; +using ServiceCore.Services; namespace WebAPI.Controllers { [ApiController] [Route("api/[controller]")] - public class AuthController(IAuthService auth) : ControllerBase + public class AuthController(IAuthService auth, IJwtService jwtService) : ControllerBase { public record RegisterRequest( string Username, @@ -39,9 +39,15 @@ namespace WebAPI.Controllers [HttpPost("login")] public async Task Login([FromBody] LoginRequest req) { - var ok = await auth.LoginAsync(req.UsernameOrEmail, req.Password); - if (!ok) return Unauthorized(); - return Ok(new { success = true }); + var userAccount = await auth.LoginAsync(req.UsernameOrEmail, req.Password); + if (userAccount is null) + { + return Unauthorized(); + } + + var jwt = jwtService.GenerateJwt(userAccount.UserAccountId, userAccount.Username, userAccount.DateOfBirth); + + return Ok(new { AccessToken = jwt, Message = "Logged in successfully." }); } } -} +} \ No newline at end of file diff --git a/src/Core/API/API.Core/Controllers/UserController.cs b/src/Core/API/API.Core/Controllers/UserController.cs index fb1d867..ce2b11a 100644 --- a/src/Core/API/API.Core/Controllers/UserController.cs +++ b/src/Core/API/API.Core/Controllers/UserController.cs @@ -1,6 +1,6 @@ -using BusinessLayer.Services; using DataAccessLayer.Entities; using Microsoft.AspNetCore.Mvc; +using ServiceCore.Services; namespace WebAPI.Controllers { diff --git a/src/Core/API/API.Core/Program.cs b/src/Core/API/API.Core/Program.cs index 98fb89f..48ca639 100644 --- a/src/Core/API/API.Core/Program.cs +++ b/src/Core/API/API.Core/Program.cs @@ -1,7 +1,7 @@ -using BusinessLayer.Services; using DataAccessLayer.Repositories.UserAccount; using DataAccessLayer.Repositories.UserCredential; using DataAccessLayer.Sql; +using ServiceCore.Services; var builder = WebApplication.CreateBuilder(args); @@ -16,6 +16,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); app.UseSwagger(); diff --git a/src/Core/Repository/Repository.Core/Repositories/UserCredential/UserCredentialRepository.cs b/src/Core/Repository/Repository.Core/Repositories/UserCredential/UserCredentialRepository.cs index e27e03b..b5aca86 100644 --- a/src/Core/Repository/Repository.Core/Repositories/UserCredential/UserCredentialRepository.cs +++ b/src/Core/Repository/Repository.Core/Repositories/UserCredential/UserCredentialRepository.cs @@ -14,7 +14,7 @@ namespace DataAccessLayer.Repositories.UserCredential command.CommandText = "USP_RotateUserCredential"; command.CommandType = CommandType.StoredProcedure; - AddParameter(command, "@UserAccountId", userAccountId); + AddParameter(command, "@UserAccountId_", userAccountId); AddParameter(command, "@Hash", credential.Hash); await command.ExecuteNonQueryAsync(); @@ -27,7 +27,7 @@ namespace DataAccessLayer.Repositories.UserCredential command.CommandText = "USP_GetActiveUserCredentialByUserAccountId"; command.CommandType = CommandType.StoredProcedure; - AddParameter(command, "@UserAccountId", userAccountId); + AddParameter(command, "@UserAccountId_", userAccountId); await using var reader = await command.ExecuteReaderAsync(); return await reader.ReadAsync() ? MapToEntity(reader) : null; @@ -40,7 +40,7 @@ namespace DataAccessLayer.Repositories.UserCredential command.CommandText = "USP_InvalidateUserCredential"; command.CommandType = CommandType.StoredProcedure; - AddParameter(command, "@UserAccountId", userAccountId); + AddParameter(command, "@UserAccountId_", userAccountId); await command.ExecuteNonQueryAsync(); } diff --git a/src/Core/Service/Service.Core/Service.Core.csproj b/src/Core/Service/Service.Core/Service.Core.csproj index d80bd8b..9c34a8f 100644 --- a/src/Core/Service/Service.Core/Service.Core.csproj +++ b/src/Core/Service/Service.Core/Service.Core.csproj @@ -3,7 +3,7 @@ net10.0 enable enable - BusinessLayer + ServiceCore diff --git a/src/Core/Service/Service.Core/Services/AuthService.cs b/src/Core/Service/Service.Core/Services/AuthService.cs index 64aa5b0..72731d2 100644 --- a/src/Core/Service/Service.Core/Services/AuthService.cs +++ b/src/Core/Service/Service.Core/Services/AuthService.cs @@ -1,8 +1,7 @@ using DataAccessLayer.Entities; using DataAccessLayer.Repositories.UserAccount; -using DataAccessLayer.Repositories.UserCredential; -namespace BusinessLayer.Services +namespace ServiceCore.Services { public class AuthService(IUserAccountRepository userRepo, IUserCredentialRepository credRepo) : IAuthService { @@ -26,18 +25,31 @@ namespace BusinessLayer.Services return userAccount; } - public async Task LoginAsync(string usernameOrEmail, string password) + public async Task LoginAsync(string usernameOrEmail, string password) { // Attempt lookup by username, then email - var user = await userRepo.GetByUsernameAsync(usernameOrEmail) + var user = await userRepo.GetByUsernameAsync(usernameOrEmail) ?? await userRepo.GetByEmailAsync(usernameOrEmail); - - if (user is null) return false; - + // the user was not found + if (user is null) + { + return null; + } + + // they don't have an active credential + // @todo handle expired passwords var activeCred = await credRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId); - if (activeCred is null) return false; + if (activeCred is null) + { + return null; + } - return PasswordHasher.Verify(password, activeCred.Hash); + if (!PasswordHasher.Verify(password, activeCred.Hash)) + { + return null; + } + + return user; } public async Task InvalidateAsync(Guid userAccountId) diff --git a/src/Core/Service/Service.Core/Services/IAuthService.cs b/src/Core/Service/Service.Core/Services/IAuthService.cs index 490bc6c..b63048d 100644 --- a/src/Core/Service/Service.Core/Services/IAuthService.cs +++ b/src/Core/Service/Service.Core/Services/IAuthService.cs @@ -1,11 +1,10 @@ using DataAccessLayer.Entities; -namespace BusinessLayer.Services +namespace ServiceCore.Services { public interface IAuthService { Task RegisterAsync(UserAccount userAccount, string password); - Task LoginAsync(string usernameOrEmail, string password); - Task InvalidateAsync(Guid userAccountId); + Task LoginAsync(string usernameOrEmail, string password); } -} +} \ No newline at end of file diff --git a/src/Core/Service/Service.Core/Services/IJwtService.cs b/src/Core/Service/Service.Core/Services/IJwtService.cs new file mode 100644 index 0000000..0dfc6e5 --- /dev/null +++ b/src/Core/Service/Service.Core/Services/IJwtService.cs @@ -0,0 +1,6 @@ +namespace ServiceCore.Services; + +public interface IJwtService +{ + string GenerateJwt(Guid userId, string username, DateTime expiry); +} \ No newline at end of file diff --git a/src/Core/Service/Service.Core/Services/IUserService.cs b/src/Core/Service/Service.Core/Services/IUserService.cs index 193a1e3..e1cf55d 100644 --- a/src/Core/Service/Service.Core/Services/IUserService.cs +++ b/src/Core/Service/Service.Core/Services/IUserService.cs @@ -1,6 +1,8 @@ + + using DataAccessLayer.Entities; -namespace BusinessLayer.Services +namespace ServiceCore.Services { public interface IUserService { diff --git a/src/Core/Service/Service.Core/Services/JwtService.cs b/src/Core/Service/Service.Core/Services/JwtService.cs new file mode 100644 index 0000000..36e7bca --- /dev/null +++ b/src/Core/Service/Service.Core/Services/JwtService.cs @@ -0,0 +1,38 @@ +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames; + +namespace ServiceCore.Services; +public class JwtService(IConfiguration config) : IJwtService +{ + private readonly string? _secret = config["Jwt:Secret"]; + + public string GenerateJwt(Guid userId, string username, DateTime expiry) + { + var handler = new JsonWebTokenHandler(); + + var key = Encoding.UTF8.GetBytes(_secret ?? throw new InvalidOperationException("secret not set")); + + // Base claims (always present) + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, userId.ToString()), + new(JwtRegisteredClaimNames.UniqueName, username), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + Expires = expiry, + SigningCredentials = new SigningCredentials( + new SymmetricSecurityKey(key), + SecurityAlgorithms.HmacSha256) + }; + + return handler.CreateToken(tokenDescriptor); + } +} \ No newline at end of file diff --git a/src/Core/Service/Service.Core/Services/PasswordHasher.cs b/src/Core/Service/Service.Core/Services/PasswordHasher.cs index 68c2f94..f691076 100644 --- a/src/Core/Service/Service.Core/Services/PasswordHasher.cs +++ b/src/Core/Service/Service.Core/Services/PasswordHasher.cs @@ -2,7 +2,7 @@ using System.Security.Cryptography; using System.Text; using Konscious.Security.Cryptography; -namespace BusinessLayer.Services +namespace ServiceCore.Services { public static class PasswordHasher { diff --git a/src/Core/Service/Service.Core/Services/UserService.cs b/src/Core/Service/Service.Core/Services/UserService.cs index 1445f41..937351f 100644 --- a/src/Core/Service/Service.Core/Services/UserService.cs +++ b/src/Core/Service/Service.Core/Services/UserService.cs @@ -1,8 +1,7 @@ using DataAccessLayer.Entities; -using DataAccessLayer.Repositories; using DataAccessLayer.Repositories.UserAccount; -namespace BusinessLayer.Services +namespace ServiceCore.Services { public class UserService(IUserAccountRepository repository) : IUserService {