using System.Diagnostics.CodeAnalysis; using System.Web; using Authinator.Backend.Utils; using Isopoh.Cryptography.Argon2; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Authinator.Backend.Database.Tables; public class User { public int Id { get; set; } public int Iteration { get; set; } public string Reference { get; set; } = null!; public string? Username { get; set; } public string? Password { get; set; } public string? Email { get; set; } public List Groups { get; set; } = new(); public bool IsSignupComplete => !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password) && !string.IsNullOrEmpty(Email); public string GetAuthToken() => $"auth:{Id}:{Iteration}:{GetAuthTokenExpiry()}:".HmacWithMessage(ConfigCache.HmacSecret); private static long GetAuthTokenExpiry() => DateTimeOffset.UtcNow.Add(ConfigCache.AuthTokenLifetime).ToUnixTimeSeconds(); public string GetSignupToken() => $"register:{Id}:".HmacWithMessage(ConfigCache.HmacSecret); public string GetPwResetToken() => $"reset:{Id}:{Iteration}:{GetPwResetTokenExpiry()}:".HmacWithMessage(ConfigCache.HmacSecret); private static long GetPwResetTokenExpiry() => DateTimeOffset.UtcNow.Add(ConfigCache.PwResetTokenLifetime).ToUnixTimeSeconds(); [SuppressMessage("ReSharper.DPA", "DPA0001: Memory allocation issues")] public bool ValidatePassword(string password) => Argon2.Verify(Password ?? string.Empty, password); [SuppressMessage("ReSharper.DPA", "DPA0001: Memory allocation issues")] public void SetPassword(string password) => Password = Argon2.Hash(password); public class Configuration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder user) { user.ToTable("Users"); user.HasKey(p => p.Id); user.Property(b => b.Iteration).HasColumnName("Iteration").HasDefaultValue(0); user.Property(b => b.Reference).HasColumnName("Reference").IsRequired(); user.Property(b => b.Username).HasColumnName("Username"); user.Property(b => b.Password).HasColumnName("Password"); user.Property(b => b.Email).HasColumnName("Email"); user.HasMany(p => p.Groups).WithMany(); } } private bool Equals(User other) => Id == other.Id; public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; return obj.GetType() == GetType() && Equals((User)obj); } public override int GetHashCode() => Id; public static bool operator ==(User? left, User? right) => Equals(left, right); public static bool operator !=(User? left, User? right) => !Equals(left, right); } public static class UserUtils { public static User? ValidateAuthToken(this DbSet users, string token) { if (string.IsNullOrWhiteSpace(token) || !token.StartsWith("auth:") || token.Split(":").Length != 5) return null; var index = token.LastIndexOf(":", StringComparison.Ordinal) + 1; var hmac = token[index..].FixUrlEncodedBase64(); var message = token[..index]; if (message.Hmac(ConfigCache.HmacSecret) != hmac) return null; var expiry = long.Parse(message.Split(":")[3]); if (DateTimeOffset.FromUnixTimeSeconds(expiry) < DateTimeOffset.UtcNow) return null; var userId = int.Parse(message.Split(":")[1]); var userIteration = int.Parse(message.Split(":")[2]); return users.Include(p => p.Groups).FirstOrDefault(p => p.Id == userId && p.Iteration == userIteration); } public static bool ValidateResetToken(this User user, string token) { if (string.IsNullOrWhiteSpace(token) || !token.StartsWith("reset:") || token.Split(":").Length != 5) return false; var index = token.LastIndexOf(":", StringComparison.Ordinal) + 1; var hmac = token[index..].FixUrlEncodedBase64(); var message = token[..index]; if (message.Hmac(ConfigCache.HmacSecret) != hmac) return false; var expiry = long.Parse(message.Split(":")[3]); if (DateTimeOffset.FromUnixTimeSeconds(expiry) < DateTimeOffset.UtcNow) return false; var userId = int.Parse(message.Split(":")[1]); var userIteration = int.Parse(message.Split(":")[2]); return user.Id == userId && user.Iteration == userIteration; } }