diff --git a/Femto.Api/Controllers/Auth/AuthController.cs b/Femto.Api/Controllers/Auth/AuthController.cs index 29c26e7..feba2c7 100644 --- a/Femto.Api/Controllers/Auth/AuthController.cs +++ b/Femto.Api/Controllers/Auth/AuthController.cs @@ -1,4 +1,7 @@ -using Femto.Modules.Authentication.Application; +using Femto.Api.Sessions; +using Femto.Modules.Auth.Application; +using Femto.Modules.Auth.Application.Commands.Login; +using Femto.Modules.Auth.Application.Commands.Register; using Microsoft.AspNetCore.Mvc; namespace Femto.Api.Controllers.Auth; @@ -10,23 +13,31 @@ public class AuthController(IAuthenticationModule authModule) : ControllerBase [HttpPost("login")] public async Task> Login([FromBody] LoginRequest request) { - - var userId = await authModule.PostCommand(new LoginCommand(request.Username, request.Password)); + var result = await authModule.PostCommand( + new LoginCommand(request.Username, request.Password) + ); + HttpContext.SetSession(result.Session); - throw new NotImplementedException(); + return new LoginResponse(result.UserId, result.Username); } [HttpPost("signup")] public async Task> Signup([FromBody] SignupRequest request) { - throw new NotImplementedException(); + var result = await authModule.PostCommand( + new RegisterCommand(request.Username, request.Password) + ); + + HttpContext.SetSession(result.Session); + + return new SignupResponse(result.UserId, result.Username); } [HttpPost("delete-session")] public async Task DeleteSession([FromBody] DeleteSessionRequest request) { // TODO - return Ok(new {}); + return Ok(new { }); } -} \ No newline at end of file +} diff --git a/Femto.Api/Controllers/Auth/LoginResponse.cs b/Femto.Api/Controllers/Auth/LoginResponse.cs index 86ee763..ba83e7f 100644 --- a/Femto.Api/Controllers/Auth/LoginResponse.cs +++ b/Femto.Api/Controllers/Auth/LoginResponse.cs @@ -1,3 +1,3 @@ namespace Femto.Api.Controllers.Auth; -public record LoginResponse(Guid UserId, string Username, string SessionToken); \ No newline at end of file +public record LoginResponse(Guid UserId, string Username); \ No newline at end of file diff --git a/Femto.Api/Controllers/Auth/SignupResponse.cs b/Femto.Api/Controllers/Auth/SignupResponse.cs index acfeff6..638d084 100644 --- a/Femto.Api/Controllers/Auth/SignupResponse.cs +++ b/Femto.Api/Controllers/Auth/SignupResponse.cs @@ -1,3 +1,3 @@ namespace Femto.Api.Controllers.Auth; -public record SignupResponse(Guid UserId, string Username, string SessionToken); \ No newline at end of file +public record SignupResponse(Guid UserId, string Username); \ No newline at end of file diff --git a/Femto.Api/Femto.Api.csproj b/Femto.Api/Femto.Api.csproj index 6617cb2..d5012e6 100644 --- a/Femto.Api/Femto.Api.csproj +++ b/Femto.Api/Femto.Api.csproj @@ -18,7 +18,7 @@ - + diff --git a/Femto.Api/Program.cs b/Femto.Api/Program.cs index b85dfa3..31f69d5 100644 --- a/Femto.Api/Program.cs +++ b/Femto.Api/Program.cs @@ -1,6 +1,6 @@ using System.Text.Json; using System.Text.Json.Serialization; -using Femto.Modules.Authentication.Application; +using Femto.Modules.Auth.Application; using Femto.Modules.Blog.Application; using Femto.Modules.Media.Application; diff --git a/Femto.Api/Sessions/HttpContextSessionExtensions.cs b/Femto.Api/Sessions/HttpContextSessionExtensions.cs new file mode 100644 index 0000000..451d401 --- /dev/null +++ b/Femto.Api/Sessions/HttpContextSessionExtensions.cs @@ -0,0 +1,21 @@ +using Femto.Modules.Auth.Application.Dto; + +namespace Femto.Api.Sessions; + +internal static class HttpContextSessionExtensions +{ + public static void SetSession(this HttpContext httpContext, Session session) + { + httpContext.Response.Cookies.Append( + "session", + session.SessionId, + new CookieOptions + { + HttpOnly = true, + Secure = true, + SameSite = SameSiteMode.Strict, + Expires = session.Expires, + } + ); + } +} \ No newline at end of file diff --git a/Femto.Common/Domain/ICommand.cs b/Femto.Common/Domain/ICommand.cs index 39529a7..8c4e4f6 100644 --- a/Femto.Common/Domain/ICommand.cs +++ b/Femto.Common/Domain/ICommand.cs @@ -4,4 +4,8 @@ namespace Femto.Common.Domain; public interface ICommand : IRequest; -public interface ICommand : IRequest; \ No newline at end of file +public interface ICommand : IRequest; + +public interface ICommandHandler : IRequestHandler where TCommand : ICommand; + +public interface ICommandHandler : IRequestHandler where TCommand : ICommand; \ No newline at end of file diff --git a/Femto.Common/Domain/IQuery.cs b/Femto.Common/Domain/IQuery.cs index beca628..3d5c35b 100644 --- a/Femto.Common/Domain/IQuery.cs +++ b/Femto.Common/Domain/IQuery.cs @@ -2,4 +2,6 @@ using MediatR; namespace Femto.Common.Domain; -public interface IQuery : IRequest; \ No newline at end of file +public interface IQuery : IRequest; + +public interface IQueryHandler : IRequestHandler where TQuery : IQuery; \ No newline at end of file diff --git a/Femto.Modules.Authentication/Application/AuthenticationModule.cs b/Femto.Modules.Auth/Application/AuthenticationModule.cs similarity index 86% rename from Femto.Modules.Authentication/Application/AuthenticationModule.cs rename to Femto.Modules.Auth/Application/AuthenticationModule.cs index 136c466..65dd713 100644 --- a/Femto.Modules.Authentication/Application/AuthenticationModule.cs +++ b/Femto.Modules.Auth/Application/AuthenticationModule.cs @@ -1,10 +1,9 @@ using Femto.Common.Domain; -using Femto.Modules.Authentication.Data; using MediatR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Femto.Modules.Authentication.Application; +namespace Femto.Modules.Auth.Application; internal class AuthenticationModule(IHost host) : IAuthenticationModule { diff --git a/Femto.Modules.Authentication/Application/AuthenticationStartup.cs b/Femto.Modules.Auth/Application/AuthenticationStartup.cs similarity index 85% rename from Femto.Modules.Authentication/Application/AuthenticationStartup.cs rename to Femto.Modules.Auth/Application/AuthenticationStartup.cs index fa83d79..ac35662 100644 --- a/Femto.Modules.Authentication/Application/AuthenticationStartup.cs +++ b/Femto.Modules.Auth/Application/AuthenticationStartup.cs @@ -1,12 +1,11 @@ using Femto.Common.Infrastructure; -using Femto.Common.Infrastructure.Outbox; -using Femto.Modules.Authentication.Data; +using Femto.Modules.Auth.Data; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Femto.Modules.Authentication.Application; +namespace Femto.Modules.Auth.Application; public static class AuthenticationStartup { @@ -20,7 +19,7 @@ public static class AuthenticationStartup private static void ConfigureServices(IServiceCollection services, string connectionString) { - services.AddDbContext( + services.AddDbContext( builder => { builder.UseNpgsql(connectionString); @@ -29,7 +28,7 @@ public static class AuthenticationStartup services.AddMediatR(c => c.RegisterServicesFromAssembly(typeof(AuthenticationStartup).Assembly)); - services.AddDbContext(builder => + services.AddDbContext(builder => { builder.UseNpgsql(); builder.UseSnakeCaseNamingConvention(); diff --git a/Femto.Modules.Auth/Application/Commands/Login/LoginCommand.cs b/Femto.Modules.Auth/Application/Commands/Login/LoginCommand.cs new file mode 100644 index 0000000..58c3e5c --- /dev/null +++ b/Femto.Modules.Auth/Application/Commands/Login/LoginCommand.cs @@ -0,0 +1,6 @@ +using Femto.Common.Domain; +using Femto.Modules.Auth.Application.Dto; + +namespace Femto.Modules.Auth.Application.Commands.Login; + +public record LoginCommand(string Username, string Password) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Commands/Login/LoginCommandHandler.cs b/Femto.Modules.Auth/Application/Commands/Login/LoginCommandHandler.cs new file mode 100644 index 0000000..53bf800 --- /dev/null +++ b/Femto.Modules.Auth/Application/Commands/Login/LoginCommandHandler.cs @@ -0,0 +1,33 @@ +using Femto.Common.Domain; +using Femto.Modules.Auth.Application.Dto; +using Femto.Modules.Auth.Application.Services; +using Femto.Modules.Auth.Data; +using Femto.Modules.Auth.Models; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Femto.Modules.Auth.Application.Commands.Login; + +internal class LoginCommandHandler(AuthContext context, SessionGenerator sessionGenerator) + : ICommandHandler +{ + public async Task Handle(LoginCommand request, CancellationToken cancellationToken) + { + var user = await context.Users.SingleOrDefaultAsync( + u => u.Username == request.Username, + cancellationToken + ); + + if (user is null) + throw new DomainException("invalid credentials"); + + if (!user.HasPassword(request.Password)) + throw new DomainException("invalid credentials"); + + var session = sessionGenerator.GenerateSession(); + + await context.AddAsync(session, cancellationToken); + + return new(new Session(session.Id, session.Expires), user.Id, user.Username); + } +} diff --git a/Femto.Modules.Auth/Application/Commands/Register/RegisterCommand.cs b/Femto.Modules.Auth/Application/Commands/Register/RegisterCommand.cs new file mode 100644 index 0000000..e3ecf5f --- /dev/null +++ b/Femto.Modules.Auth/Application/Commands/Register/RegisterCommand.cs @@ -0,0 +1,6 @@ +using Femto.Common.Domain; +using Femto.Modules.Auth.Application.Dto; + +namespace Femto.Modules.Auth.Application.Commands.Register; + +public record RegisterCommand(string Username, string Password) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Commands/Register/RegisterCommandHandler.cs b/Femto.Modules.Auth/Application/Commands/Register/RegisterCommandHandler.cs new file mode 100644 index 0000000..5b8c12a --- /dev/null +++ b/Femto.Modules.Auth/Application/Commands/Register/RegisterCommandHandler.cs @@ -0,0 +1,23 @@ +using Femto.Common.Domain; +using Femto.Modules.Auth.Application.Dto; +using Femto.Modules.Auth.Application.Services; +using Femto.Modules.Auth.Data; +using Femto.Modules.Auth.Models; + +namespace Femto.Modules.Auth.Application.Commands.Register; + +internal class RegisterCommandHandler(AuthContext context, SessionGenerator sessionGenerator) : ICommandHandler +{ + public async Task Handle(RegisterCommand request, CancellationToken cancellationToken) + { + var user = new UserIdentity(request.Username); + + user.SetPassword(request.Password); + + var session = user.StartNewSession(); + + await context.AddAsync(user, cancellationToken); + + return new(new Session(session.Id, session.Expires), user.Id, user.Username); + } +} \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Commands/ValidateSession/ValidateSessionCommand.cs b/Femto.Modules.Auth/Application/Commands/ValidateSession/ValidateSessionCommand.cs new file mode 100644 index 0000000..b1525fb --- /dev/null +++ b/Femto.Modules.Auth/Application/Commands/ValidateSession/ValidateSessionCommand.cs @@ -0,0 +1,6 @@ +using Femto.Common.Domain; +using Femto.Modules.Auth.Application.Dto; + +namespace Femto.Modules.Auth.Application.Commands.ValidateSession; + +public record ValidateSessionCommand(string SessionId) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Commands/ValidateSession/ValidateSessionCommandHandler.cs b/Femto.Modules.Auth/Application/Commands/ValidateSession/ValidateSessionCommandHandler.cs new file mode 100644 index 0000000..9d60da5 --- /dev/null +++ b/Femto.Modules.Auth/Application/Commands/ValidateSession/ValidateSessionCommandHandler.cs @@ -0,0 +1,34 @@ +using Femto.Common.Domain; +using Femto.Modules.Auth.Application.Dto; +using Femto.Modules.Auth.Data; +using Microsoft.EntityFrameworkCore; + +namespace Femto.Modules.Auth.Application.Commands.ValidateSession; + +internal class ValidateSessionCommandHandler(AuthContext context) + : ICommandHandler +{ + public async Task Handle( + ValidateSessionCommand request, + CancellationToken cancellationToken + ) + { + var now = DateTimeOffset.UtcNow; + + var user = await context.Users.SingleOrDefaultAsync( + u => u.Sessions.Any(s => s.Id == request.SessionId && s.Expires > now), + cancellationToken + ); + + if (user is null) + throw new DomainException("invalid session"); + + var session = user.StartNewSession(); + + return new ValidateSessionResult( + new Session(session.Id, session.Expires), + user.Id, + user.Username + ); + } +} diff --git a/Femto.Modules.Auth/Application/Dto/LoginResult.cs b/Femto.Modules.Auth/Application/Dto/LoginResult.cs new file mode 100644 index 0000000..e56fdc1 --- /dev/null +++ b/Femto.Modules.Auth/Application/Dto/LoginResult.cs @@ -0,0 +1,3 @@ +namespace Femto.Modules.Auth.Application.Dto; + +public record LoginResult(Session Session, Guid UserId, string Username); \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Dto/RegisterResult.cs b/Femto.Modules.Auth/Application/Dto/RegisterResult.cs new file mode 100644 index 0000000..1c9b467 --- /dev/null +++ b/Femto.Modules.Auth/Application/Dto/RegisterResult.cs @@ -0,0 +1,3 @@ +namespace Femto.Modules.Auth.Application.Dto; + +public record RegisterResult(Session Session, Guid UserId, string Username); \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Dto/Session.cs b/Femto.Modules.Auth/Application/Dto/Session.cs new file mode 100644 index 0000000..297b4d8 --- /dev/null +++ b/Femto.Modules.Auth/Application/Dto/Session.cs @@ -0,0 +1,5 @@ +using Femto.Modules.Auth.Models; + +namespace Femto.Modules.Auth.Application.Dto; + +public record Session(string SessionId, DateTimeOffset Expires); \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Dto/ValidateSessionResult.cs b/Femto.Modules.Auth/Application/Dto/ValidateSessionResult.cs new file mode 100644 index 0000000..9e22c1e --- /dev/null +++ b/Femto.Modules.Auth/Application/Dto/ValidateSessionResult.cs @@ -0,0 +1,3 @@ +namespace Femto.Modules.Auth.Application.Dto; + +public record ValidateSessionResult(Session Session, Guid UserId, string Username); \ No newline at end of file diff --git a/Femto.Modules.Authentication/Application/IAuthenticationModule.cs b/Femto.Modules.Auth/Application/IAuthenticationModule.cs similarity index 78% rename from Femto.Modules.Authentication/Application/IAuthenticationModule.cs rename to Femto.Modules.Auth/Application/IAuthenticationModule.cs index dcd7b67..a3226c6 100644 --- a/Femto.Modules.Authentication/Application/IAuthenticationModule.cs +++ b/Femto.Modules.Auth/Application/IAuthenticationModule.cs @@ -1,6 +1,6 @@ using Femto.Common.Domain; -namespace Femto.Modules.Authentication.Application; +namespace Femto.Modules.Auth.Application; public interface IAuthenticationModule { diff --git a/Femto.Modules.Auth/Application/Services/SessionGenerator.cs b/Femto.Modules.Auth/Application/Services/SessionGenerator.cs new file mode 100644 index 0000000..b536712 --- /dev/null +++ b/Femto.Modules.Auth/Application/Services/SessionGenerator.cs @@ -0,0 +1,11 @@ +using Femto.Modules.Auth.Models; + +namespace Femto.Modules.Auth.Application.Services; + +public class SessionGenerator +{ + public UserSession GenerateSession() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Femto.Modules.Authentication/Contracts/AuthenticationService.cs b/Femto.Modules.Auth/Contracts/AuthenticationService.cs similarity index 69% rename from Femto.Modules.Authentication/Contracts/AuthenticationService.cs rename to Femto.Modules.Auth/Contracts/AuthenticationService.cs index abd2901..81f48d6 100644 --- a/Femto.Modules.Authentication/Contracts/AuthenticationService.cs +++ b/Femto.Modules.Auth/Contracts/AuthenticationService.cs @@ -1,9 +1,9 @@ -using Femto.Modules.Authentication.Data; -using Femto.Modules.Authentication.Models; +using Femto.Modules.Auth.Data; +using Femto.Modules.Auth.Models; -namespace Femto.Modules.Authentication.Contracts; +namespace Femto.Modules.Auth.Contracts; -internal class AuthenticationService(AuthenticationContext context) : IAuthenticationService +internal class AuthenticationService(AuthContext context) : IAuthenticationService { public async Task Register(string username, string password) { diff --git a/Femto.Modules.Authentication/Contracts/IAuthenticationService.cs b/Femto.Modules.Auth/Contracts/IAuthenticationService.cs similarity index 79% rename from Femto.Modules.Authentication/Contracts/IAuthenticationService.cs rename to Femto.Modules.Auth/Contracts/IAuthenticationService.cs index 78651b8..19aea57 100644 --- a/Femto.Modules.Authentication/Contracts/IAuthenticationService.cs +++ b/Femto.Modules.Auth/Contracts/IAuthenticationService.cs @@ -1,4 +1,4 @@ -namespace Femto.Modules.Authentication.Contracts; +namespace Femto.Modules.Auth.Contracts; public interface IAuthenticationService { diff --git a/Femto.Modules.Authentication/Contracts/UserInfo.cs b/Femto.Modules.Auth/Contracts/UserInfo.cs similarity index 51% rename from Femto.Modules.Authentication/Contracts/UserInfo.cs rename to Femto.Modules.Auth/Contracts/UserInfo.cs index 014ebc1..dee7448 100644 --- a/Femto.Modules.Authentication/Contracts/UserInfo.cs +++ b/Femto.Modules.Auth/Contracts/UserInfo.cs @@ -1,3 +1,3 @@ -namespace Femto.Modules.Authentication.Contracts; +namespace Femto.Modules.Auth.Contracts; public record UserInfo(Guid UserId, string Username); \ No newline at end of file diff --git a/Femto.Modules.Authentication/Data/AuthenticationContext.cs b/Femto.Modules.Auth/Data/AuthContext.cs similarity index 53% rename from Femto.Modules.Authentication/Data/AuthenticationContext.cs rename to Femto.Modules.Auth/Data/AuthContext.cs index 6d1b1e4..e48296a 100644 --- a/Femto.Modules.Authentication/Data/AuthenticationContext.cs +++ b/Femto.Modules.Auth/Data/AuthContext.cs @@ -1,16 +1,18 @@ using Femto.Common.Infrastructure.Outbox; +using Femto.Modules.Auth.Models; using Microsoft.EntityFrameworkCore; -namespace Femto.Modules.Authentication.Data; +namespace Femto.Modules.Auth.Data; -internal class AuthenticationContext : DbContext, IOutboxContext +internal class AuthContext : DbContext, IOutboxContext { + public virtual DbSet Users { get; } public virtual DbSet Outbox { get; } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.HasDefaultSchema("authn"); - builder.ApplyConfigurationsFromAssembly(typeof(AuthenticationContext).Assembly); + builder.ApplyConfigurationsFromAssembly(typeof(AuthContext).Assembly); } } \ No newline at end of file diff --git a/Femto.Modules.Authentication/Data/Configurations/UserIdentityTypeConfiguration.cs b/Femto.Modules.Auth/Data/Configurations/UserIdentityTypeConfiguration.cs similarity index 70% rename from Femto.Modules.Authentication/Data/Configurations/UserIdentityTypeConfiguration.cs rename to Femto.Modules.Auth/Data/Configurations/UserIdentityTypeConfiguration.cs index 43d1650..36c6324 100644 --- a/Femto.Modules.Authentication/Data/Configurations/UserIdentityTypeConfiguration.cs +++ b/Femto.Modules.Auth/Data/Configurations/UserIdentityTypeConfiguration.cs @@ -1,8 +1,8 @@ -using Femto.Modules.Authentication.Models; +using Femto.Modules.Auth.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Femto.Modules.Authentication.Data.Configurations; +namespace Femto.Modules.Auth.Data.Configurations; internal class UserIdentityTypeConfiguration : IEntityTypeConfiguration { @@ -10,5 +10,6 @@ internal class UserIdentityTypeConfiguration : IEntityTypeConfiguration u.Password).WithOwner().HasForeignKey("user_id"); + builder.OwnsMany(u => u.Sessions).WithOwner().HasForeignKey("user_id"); } } diff --git a/Femto.Modules.Authentication/Femto.Modules.Authentication.csproj b/Femto.Modules.Auth/Femto.Modules.Auth.csproj similarity index 100% rename from Femto.Modules.Authentication/Femto.Modules.Authentication.csproj rename to Femto.Modules.Auth/Femto.Modules.Auth.csproj diff --git a/Femto.Modules.Authentication/Models/Events/UserWasCreatedEvent.cs b/Femto.Modules.Auth/Models/Events/UserWasCreatedEvent.cs similarity index 64% rename from Femto.Modules.Authentication/Models/Events/UserWasCreatedEvent.cs rename to Femto.Modules.Auth/Models/Events/UserWasCreatedEvent.cs index a3b44c7..d0f7ef8 100644 --- a/Femto.Modules.Authentication/Models/Events/UserWasCreatedEvent.cs +++ b/Femto.Modules.Auth/Models/Events/UserWasCreatedEvent.cs @@ -1,5 +1,5 @@ using Femto.Common.Domain; -namespace Femto.Modules.Authentication.Models.Events; +namespace Femto.Modules.Auth.Models.Events; internal record UserWasCreatedEvent(UserIdentity User) : DomainEvent; \ No newline at end of file diff --git a/Femto.Modules.Authentication/Models/UserIdentity.cs b/Femto.Modules.Auth/Models/UserIdentity.cs similarity index 56% rename from Femto.Modules.Authentication/Models/UserIdentity.cs rename to Femto.Modules.Auth/Models/UserIdentity.cs index e3e1d23..aed0031 100644 --- a/Femto.Modules.Authentication/Models/UserIdentity.cs +++ b/Femto.Modules.Auth/Models/UserIdentity.cs @@ -1,29 +1,28 @@ using System.Text; +using System.Text.Unicode; using Femto.Common.Domain; -using Femto.Modules.Authentication.Contracts; -using Femto.Modules.Authentication.Models.Events; +using Femto.Modules.Auth.Models.Events; using Geralt; -namespace Femto.Modules.Authentication.Models; +namespace Femto.Modules.Auth.Models; internal class UserIdentity : Entity { public Guid Id { get; private set; } - + public string Username { get; private set; } - + public UserPassword Password { get; private set; } - private UserIdentity() - { - - } + public ICollection Sessions { get; private set; } + + private UserIdentity() { } public UserIdentity(string username) { this.Id = Guid.CreateVersion7(); this.Username = username; - + this.AddDomainEvent(new UserWasCreatedEvent(this)); } @@ -35,17 +34,26 @@ internal class UserIdentity : Entity public void SetPassword(string password) { - var hash = new byte[128]; - try + this.Password = new UserPassword(password); + } + + public bool HasPassword(string requestPassword) + { + if (this.Password is null) { - Argon2id.ComputeHash(hash, Encoding.UTF8.GetBytes(password), 3, 67108864); - } - catch (Exception e) - { - throw new SetPasswordError("Failed to hash password", e); + return false; } - this.Password = new UserPassword(this.Id, hash, new byte[128]); + return this.Password.Check(requestPassword); + } + + public UserSession StartNewSession() + { + var session = UserSession.Create(); + + this.Sessions.Add(session); + + return session; } } diff --git a/Femto.Modules.Auth/Models/UserPassword.cs b/Femto.Modules.Auth/Models/UserPassword.cs new file mode 100644 index 0000000..6d9bd2f --- /dev/null +++ b/Femto.Modules.Auth/Models/UserPassword.cs @@ -0,0 +1,73 @@ +using System.Text; +using Geralt; +using JetBrains.Annotations; + +namespace Femto.Modules.Auth.Models; + +internal class UserPassword +{ + private const int Iterations = 3; + private const int MemorySize = 67108864; + + public Guid Id { get; private set; } + + private byte[] Hash { get; set; } + + private byte[] Salt { get; set; } + + [UsedImplicitly] + private UserPassword() {} + + public UserPassword(string password) + { + this.Id = Guid.NewGuid(); + this.Salt = ComputeSalt(); + this.Hash = ComputePasswordHash(password, Salt); + } + + public bool Check(string password) + { + var matches = Argon2id.VerifyHash( + Hash, + Combine(password, Salt) + ); + + if (!matches) + return false; + + if (Argon2id.NeedsRehash(Hash, Iterations, MemorySize)) + { + this.Salt = ComputeSalt(); + this.Hash = ComputePasswordHash(password, Salt); + } + + return true; + } + + private static byte[] ComputeSalt() => System.Security.Cryptography.RandomNumberGenerator.GetBytes(32); + + private static byte[] ComputePasswordHash(string password, byte[] salt) + { + var hash = new byte[128]; + try + { + Argon2id.ComputeHash(hash, Combine(password, salt), Iterations, MemorySize); + } + catch (Exception e) + { + throw new SetPasswordError("Failed to hash password", e); + } + + return hash; + } + + private static byte[] Combine(string password, byte[] salt) + { + var passwordBytes = Encoding.UTF8.GetBytes(password); + var hashInput = new byte[passwordBytes.Length + salt.Length]; + passwordBytes.CopyTo(hashInput, 0); + salt.CopyTo(hashInput, passwordBytes.Length); + + return hashInput; + } +} \ No newline at end of file diff --git a/Femto.Modules.Auth/Models/UserSession.cs b/Femto.Modules.Auth/Models/UserSession.cs new file mode 100644 index 0000000..1c82e61 --- /dev/null +++ b/Femto.Modules.Auth/Models/UserSession.cs @@ -0,0 +1,19 @@ +namespace Femto.Modules.Auth.Models; + +public class UserSession +{ + private static TimeSpan SessionTimeout = TimeSpan.FromMinutes(30); + public string Id { get; private set; } + public DateTimeOffset Expires { get; private set; } + + private UserSession() {} + + public static UserSession Create() + { + return new() + { + Id = Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(32)), + Expires = DateTimeOffset.Now + SessionTimeout + }; + } +} \ No newline at end of file diff --git a/Femto.Modules.Authentication/Application/Commands/Login/LoginCommand.cs b/Femto.Modules.Authentication/Application/Commands/Login/LoginCommand.cs deleted file mode 100644 index b7e8356..0000000 --- a/Femto.Modules.Authentication/Application/Commands/Login/LoginCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Femto.Common.Domain; - -namespace Femto.Modules.Authentication.Application.Commands; - -public record LoginCommand(string Username, string Password) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Authentication/Application/Commands/Login/LoginCommandHandler.cs b/Femto.Modules.Authentication/Application/Commands/Login/LoginCommandHandler.cs deleted file mode 100644 index 5429b2f..0000000 --- a/Femto.Modules.Authentication/Application/Commands/Login/LoginCommandHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Femto.Modules.Authentication.Data; -using Femto.Modules.Authentication.Models; -using MediatR; - -namespace Femto.Modules.Authentication.Application.Commands; - -internal class LoginCommandHandler(AuthenticationContext context) : IRequestHandler -{ - public async Task Handle(LoginCommand request, CancellationToken cancellationToken) - { - var user = new UserIdentity(request.Username); - - user.SetPassword(request.Password); - - await context.AddAsync(user, cancellationToken); - - return user.Id; - } -} \ No newline at end of file diff --git a/Femto.Modules.Authentication/Models/UserPassword.cs b/Femto.Modules.Authentication/Models/UserPassword.cs deleted file mode 100644 index 0ddb247..0000000 --- a/Femto.Modules.Authentication/Models/UserPassword.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Femto.Modules.Authentication.Models; - -internal class UserPassword -{ - public Guid Id { get; private set; } - - public byte[] Hash { get; private set; } - - public byte[] Salt { get; private set; } - - private UserPassword() {} - - public UserPassword(Guid id, byte[] hash, byte[] salt) - { - Id = id; - Hash = hash; - Salt = salt; - } -} \ No newline at end of file diff --git a/FemtoBackend.sln b/FemtoBackend.sln index 85c939a..52f0715 100644 --- a/FemtoBackend.sln +++ b/FemtoBackend.sln @@ -12,7 +12,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Femto.Common", "Femto.Commo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Femto.Modules.Media", "Femto.Modules.Media\Femto.Modules.Media.csproj", "{AC9FBF11-FF29-4A80-B9EA-AFDF1E3DCA80}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Femto.Modules.Authentication", "Femto.Modules.Authentication\Femto.Modules.Authentication.csproj", "{7E138EF6-E075-4896-93C0-923024F0CA78}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Femto.Modules.Auth", "Femto.Modules.Auth\Femto.Modules.Auth.csproj", "{7E138EF6-E075-4896-93C0-923024F0CA78}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution