OpenIddict 7.7 (MongoDB stores, keys kept in Mongo so tokens survive
restarts) serves /oauth/authorize, /oauth/token, /oauth/revoke and the
discovery documents, including /.well-known/oauth-authorization-server:
- authorization code (PKCE optional) and client credentials, Mastodon's
scopes including the granular ones, non-expiring reference tokens,
`created_at` in the token response, and the urn:ietf:wg:oauth:2.0:oob
page that shows the code;
- /oauth/login signs the private login into a fifteen-minute cookie that
only /oauth sees (rate limited, antiforgery-protected); /oauth/authorize
then asks which persona the application acts as. The token's subject
is that persona's id and nothing else; no root id reaches a token, an
authorization or a log line;
- the token exchange refuses a persona whose login is banned or deleted,
and every API request checks the same.
/api/v1/apps registers applications dynamically, /api/v1/apps/
verify_credentials, /api/v1/instance (v1 and v2, "4.2.0 (compatible;
PrivaPub)") and verify_credentials answer in Mastodon's shapes: snake_case
with explicit nulls, Rails-style parameters from query, form or JSON,
{"error": ...} on failure, Link paging. CORS exposes Link.
/api goes to OpenIddict validation and everything else to the existing
JWT; the JWT failure handler no longer sends the exception and stack trace
to the client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
49 lines
1.7 KiB
C#
49 lines
1.7 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 AuthenticationFailed(AuthenticationFailedContext context)
|
|
{
|
|
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 AuthenticationFailed()");
|
|
}
|
|
}
|
|
|
|
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 AuthenticationFailed()");
|
|
}
|
|
}
|
|
}
|
|
} |