Files
SocialPub/PrivaPub/Web/Pages/OAuth/Authorize.cshtml
T
thepraandClaude Opus 5.5 4d9be37c1c Mastodon clients can sign in: OAuth with a persona per token
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
2026-10-01 11:52:31 +02:00

30 lines
1.1 KiB
Plaintext

@page "/oauth/authorize"
@model PrivaPub.Web.Pages.OAuth.AuthorizeModel
@{
ViewData["Title"] = "Authorize";
}
<header><h1>Authorize @Model.ApplicationName</h1></header>
<p>It asks for: <strong>@string.Join(", ", Model.RequestedScopes)</strong>.</p>
@if (Model.Avatars.Count == 0)
{
<p role="alert">This login has no persona yet. Create one first.</p>
}
else
{
<form method="post">
@foreach (var parameter in Model.Parameters)
{
<input type="hidden" name="@parameter.Key" value="@parameter.Value">
}
<fieldset>
<legend>Act as</legend>
@foreach (var avatar in Model.Avatars)
{
<p><label><input type="radio" name="avatarId" value="@avatar.Id" required @(avatar == Model.Avatars[0] ? "checked" : "")> @avatar.Name <span class="meta">@@@avatar.Handle</span></label></p>
}
</fieldset>
<p class="meta">Each persona is a separate account to the application. Choosing one does not reveal the others.</p>
<p><button type="submit" name="decision" value="allow">Allow</button> <button type="submit" name="decision" value="deny">Deny</button></p>
</form>
}