Files
SocialPub/PrivaPub/Services/JwtEvents.cs
T
thepraandClaude Opus 5.5 125a49a1a0
Build / Build (push) Successful in 2m24s
Deploy / privapub.thepra.dev (push) Successful in 2m39s
T4: nothing answers 500
NeverFiveHundredTests walks every endpoint the server maps and fills each route parameter
with junk ('x', zeroed and random ObjectIds, a dot-dot segment, 5000 characters). It calls
each one anonymously, as a persona, and with a junk token, and asserts that nothing
answers 5xx or throws, and that every /api error carries a Mastodon error body.

It found two bugs:
- A junk 'Authorization: Bearer' on any non-/api route, anonymous ones included
  (/build.json, /peasants/*, inboxes), broke the response. JwtEvents.AuthenticationFailed
  wrote a body during authentication and the endpoint then wrote its own. The 401 body now
  comes from the Challenge event, which runs only when an endpoint needs a user.
- /api answered 401, 404 and 429 with no body. UseMastodonErrorBodies gives any /api error
  that leaves without a body Mastodon's {"error": ...}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
2026-10-03 10:46:20 +02:00

52 lines
1.8 KiB
C#

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Localization;
using PrivaPub.ClientModels;
using PrivaPub.Resources;
using System.Text;
using System.Text.Json;
namespace PrivaPub.Services
{
public class JwtEvents : JwtBearerEvents
{
ILogger<JwtEvents> _logger { get; set; }
const string contentType = "application/json";
public override async Task Challenge(JwtBearerChallengeContext context)
{
context.HandleResponse();
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.Headers.WWWAuthenticate = context.AuthenticateFailure == default ? "Bearer" : "Bearer error=\"invalid_token\"";
try
{
var localizer = context.HttpContext.RequestServices.GetRequiredService<IStringLocalizer<GenericRes>>();
var webResult = new WebResult().Invalidate(localizer["Unauthorized."], StatusCodes.Status401Unauthorized);
context.Response.ContentType = contentType;
await context.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(webResult)));
}
catch (Exception ex)
{
_logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<JwtEvents>>();
_logger.LogError(ex, "Error at Challenge()");
}
}
public override async Task Forbidden(ForbiddenContext context)
{
try
{
var localizer = context.HttpContext.RequestServices.GetRequiredService<IStringLocalizer<GenericRes>>();
var webResult = new WebResult().Invalidate(localizer["Forbidden."], StatusCodes.Status403Forbidden);
context.Response.ContentType = contentType;
await context.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(webResult)));
}
catch (Exception ex)
{
_logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<JwtEvents>>();
_logger.LogError(ex, "Error at Forbidden()");
}
}
}
}