One sign-in for decePubClient: the root JWT exchanged for a persona's token

The first-party client signs in on /clientapi and exchanges that JWT on
/oauth/token (RFC 8693, subject_token_type jwt, avatar_id) for one
persona's Mastodon token. Only the seeded public application `decepub`
holds the grant; RootJwtSubjectToken validates the JWT through RootJwt,
which JwtBearer now shares (signature, lifetime, ban, deletion, session
stamp). The issued token names the avatar, never the root.

Owner decision recorded in ROADMAP; it supersedes "moving decePubClient
onto the Mastodon API is out of scope".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-04 10:03:46 +02:00
1 parent c7e8760ffe
commit 2cd75176a4
10 files changed
+432 -30

No files matched your search

+3 -8
View File
@@ -28,15 +28,10 @@ namespace PrivaPub.Services
? default
: await context.HttpContext.RequestServices.GetRequiredService<DbEntities>().RootUsers.MatchID(rootId)
.ExecuteFirstAsync(context.HttpContext.RequestAborted);
if (root is not { IsBanned: false, DeletedAt: null })
var refusal = RootJwt.Refusal(root, context.Principal);
if (refusal != default)
{
context.Fail("The account can no longer be used.");
return;
}
// a password recovery ends every session made before it (RootSessions gives the root a new stamp)
if ((root.SessionStamp ?? string.Empty) != (context.Principal.FindFirst(AuthTokenManager.SessionStamp)?.Value ?? string.Empty))
{
context.Fail("The account's sessions were ended.");
context.Fail(refusal);
return;
}
if (context.Principal.Identity is not ClaimsIdentity identity)