84 lines
3.0 KiB
C#
84 lines
3.0 KiB
C#
using System.Security.Claims;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
using Microsoft.AspNetCore.Identity;
|
|
|
|
namespace Htmx.ApiDemo.Data;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class AppAuthService(
|
|
MongoDbService mongo,
|
|
IPasswordHasher<AppUser> 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<Claim> 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);
|
|
}
|
|
}
|