Files
jobtrackingapp/JobTrackerApi/Services/SessionContextMiddleware.cs
T
cesnimda 0954876295 fix(security): enforce audited account, network and resource boundaries
Implement B01-B06 with regression coverage and retained validation evidence. Preserve the production ingress preflight; F04 runtime verification remains open.
2026-09-09 18:21:47 +02:00

50 lines
2.3 KiB
C#

using Microsoft.AspNetCore.Authorization;
namespace JobTrackerApi.Services;
// A precondition, never an authority source: all resource owners still come from User.
// Bind retained browser requests to the existing, validated server-side session. No new token
// store or schema is needed; knowing a session id alone cannot authenticate a request.
public sealed class SessionContextMiddleware(RequestDelegate next)
{
public const string HeaderName = "X-Session-Context";
public async Task InvokeAsync(HttpContext context)
{
if (context.User.Identity?.IsAuthenticated != true ||
context.GetEndpoint()?.Metadata.GetMetadata<IAllowAnonymous>() is not null)
{
await next(context);
return;
}
var supplied = context.Request.Headers[HeaderName];
var safeMethod = HttpMethods.IsGet(context.Request.Method) || HttpMethods.IsHead(context.Request.Method) ||
HttpMethods.IsOptions(context.Request.Method) || HttpMethods.IsTrace(context.Request.Method);
// Unbound reads support bootstrap, direct document links and API clients. When a browser
// supplies context on a read, enforce it too so a stale tab cannot adopt another owner's data.
if (safeMethod && supplied.Count == 0)
{
await next(context);
return;
}
var sessionId = context.User.FindFirst("sid")?.Value;
if (supplied.Count != 1 || string.IsNullOrWhiteSpace(supplied[0]) ||
string.IsNullOrWhiteSpace(sessionId) || !string.Equals(supplied[0], sessionId, StringComparison.Ordinal))
{
var missing = supplied.Count == 0 || (supplied.Count == 1 && string.IsNullOrWhiteSpace(supplied[0]));
context.Response.StatusCode = missing ? StatusCodes.Status428PreconditionRequired : StatusCodes.Status409Conflict;
context.Response.Headers.CacheControl = "no-store";
await context.Response.WriteAsJsonAsync(new
{
code = missing ? "session_context_required" : "session_context_mismatch",
message = "Reload this page before continuing. Your session may have changed."
}, context.RequestAborted);
return;
}
await next(context);
}
}