feat(auth): cookie session endpoints + WebApplicationFactory harness
This commit is contained in:
4
src/VolcanicCore.Api/Contracts/SessionDtos.cs
Normal file
4
src/VolcanicCore.Api/Contracts/SessionDtos.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace VolcanicCore.Api.Contracts;
|
||||
|
||||
public sealed record LoginRequest(string Password);
|
||||
public sealed record MeResponse(bool Authenticated, string? Username);
|
||||
42
src/VolcanicCore.Api/Endpoints/SessionEndpoints.cs
Normal file
42
src/VolcanicCore.Api/Endpoints/SessionEndpoints.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user