feat(auth): cookie session endpoints + WebApplicationFactory harness

This commit is contained in:
Claude
2026-08-28 11:48:45 +00:00
parent 796ccf50da
commit eb59c87d03
5 changed files with 155 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
namespace VolcanicCore.Api.Contracts;
public sealed record LoginRequest(string Password);
public sealed record MeResponse(bool Authenticated, string? Username);

View File

@@ -0,0 +1,42 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using VolcanicCore.Api.Auth;
using VolcanicCore.Api.Contracts;
using VolcanicCore.Persistence.Repositories;
namespace VolcanicCore.Api.Endpoints;
public static class SessionEndpoints
{
public const string AdminUser = "admin";
public static void MapSessionEndpoints(this IEndpointRouteBuilder app)
{
app.MapPost("/api/session", async (LoginRequest req, HttpContext ctx,
AdminConfigRepository repo, PasswordService pw) =>
{
var hash = repo.Get(AdminBootstrap.PasswordHashKey);
if (hash is null || !pw.Verify(req.Password ?? "", hash))
return Results.Unauthorized();
var claims = new[] { new Claim(ClaimTypes.Name, AdminUser) };
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(identity));
return Results.NoContent();
});
app.MapDelete("/api/session", async (HttpContext ctx) =>
{
await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Results.NoContent();
});
app.MapGet("/api/session/me", (HttpContext ctx) =>
ctx.User.Identity?.IsAuthenticated == true
? Results.Ok(new MeResponse(true, ctx.User.Identity.Name))
: Results.Unauthorized())
.RequireAuthorization();
}
}

View File

@@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using VolcanicCore.Api;
using VolcanicCore.Api.Auth;
using VolcanicCore.Api.Endpoints;
using VolcanicCore.Persistence;
using VolcanicCore.Persistence.Migrations;
using VolcanicCore.Persistence.Repositories;
@@ -37,6 +39,26 @@ builder.Services.AddSingleton<Migrator>(_ => new Migrator(new IMigration[]
}));
builder.Services.AddSingleton<PasswordService>();
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.Cookie.Name = "volcaniccore.sid";
o.Cookie.HttpOnly = true;
o.Cookie.SameSite = SameSiteMode.Lax;
o.ExpireTimeSpan = TimeSpan.FromDays(7);
o.SlidingExpiration = true;
o.Events.OnRedirectToLogin = ctx =>
{
ctx.Response.StatusCode = 401; return Task.CompletedTask;
};
o.Events.OnRedirectToAccessDenied = ctx =>
{
ctx.Response.StatusCode = 403; return Task.CompletedTask;
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
// Apply migrations at boot.
@@ -56,6 +78,10 @@ using (var scope = app.Services.CreateScope())
AdminBootstrap.Seed(repo, pw, settings.AdminPassword);
}
app.UseAuthentication();
app.UseAuthorization();
app.MapSessionEndpoints();
app.MapGet("/api/health", () => Results.Ok(new { ok = true }));
app.Run();