using System.Security.Claims; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Identity; namespace Htmx.ApiDemo.Data; /// /// AOT-safe authentication service backed by MongoDB. /// No EF Core, no LINQ-to-SQL, no RelationalModel fully NativeAOT safe. /// IPasswordHasher is pure PBKDF2 crypto with no dynamic IL. /// public sealed class AuthService( MongoDbService mongo, IPasswordHasher passwordHasher, IHttpContextAccessor httpContextAccessor) { public async Task<(bool Success, string? Error)> RegisterAsync( string email, string password, string? displayName, CancellationToken ct = default) { var normalized = email.ToUpperInvariant(); if (await mongo.EmailExistsAsync(normalized, ct)) return (false, "That email address is already registered."); var user = new AppUser { Email = email, NormalizedEmail = normalized, DisplayName = string.IsNullOrWhiteSpace(displayName) ? null : displayName.Trim(), CreatedAtUtc = DateTime.UtcNow, }; user.PasswordHash = passwordHasher.HashPassword(user, password); await mongo.InsertAsync(user, ct); await SignInUserAsync(user); return (true, null); } public async Task<(bool Success, string? Error)> LoginAsync( string email, string password, CancellationToken ct = default) { var normalized = email.ToUpperInvariant(); var user = await mongo.FindByNormalizedEmailAsync(normalized, ct); if (user is null) return (false, "Invalid email or password."); var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); if (result == PasswordVerificationResult.Failed) return (false, "Invalid email or password."); await SignInUserAsync(user); return (true, null); } public async Task SignOutAsync() { var ctx = httpContextAccessor.HttpContext ?? throw new InvalidOperationException("HttpContext is not available."); await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } private async Task SignInUserAsync(AppUser user) { var ctx = httpContextAccessor.HttpContext ?? throw new InvalidOperationException("HttpContext is not available."); List claims = [ new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.Email), new Claim(ClaimTypes.Email, user.Email), ]; if (!string.IsNullOrEmpty(user.DisplayName)) claims.Add(new Claim("DisplayName", user.DisplayName)); var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var principal = new ClaimsPrincipal(identity); await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal); } }