This commit is contained in:
john 2025-05-14 23:53:00 +02:00
parent baea64229b
commit 0dc41337da
36 changed files with 324 additions and 95 deletions

View file

@ -0,0 +1,5 @@
using Femto.Common.Domain;
namespace Femto.Modules.Auth.Models.Events;
internal record UserWasCreatedEvent(UserIdentity User) : DomainEvent;

View file

@ -0,0 +1,60 @@
using System.Text;
using System.Text.Unicode;
using Femto.Common.Domain;
using Femto.Modules.Auth.Models.Events;
using Geralt;
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; }
public ICollection<UserSession> Sessions { get; private set; }
private UserIdentity() { }
public UserIdentity(string username)
{
this.Id = Guid.CreateVersion7();
this.Username = username;
this.AddDomainEvent(new UserWasCreatedEvent(this));
}
public UserIdentity WithPassword(string password)
{
this.SetPassword(password);
return this;
}
public void SetPassword(string password)
{
this.Password = new UserPassword(password);
}
public bool HasPassword(string requestPassword)
{
if (this.Password is null)
{
return false;
}
return this.Password.Check(requestPassword);
}
public UserSession StartNewSession()
{
var session = UserSession.Create();
this.Sessions.Add(session);
return session;
}
}
public class SetPasswordError(string message, Exception inner) : DomainException(message, inner);

View file

@ -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;
}
}

View file

@ -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
};
}
}