R3 two-factor sign-in: TOTP with recovery codes, asked for only after a correct password
Build / Build (push) Successful in 36s
Build / Build (push) Successful in 36s
Completion program R3 (docs/ROADMAP.md, 2026-10-17). - Services/TwoFactor.cs: TOTP (RFC 6238, 30 s, 6 digits, SHA-1, one step of slack, Otp.NET 1.4.1) and ten recovery codes (SHA-256 kept, pulled when used). RootUser.Totp holds the secret, the last step taken (moved only by a conditional update, so a code works once even for two sign-ins at the same moment) and the codes' hashes. - /clientapi/user/totp/setup (the password) answers the secret, its otpauth:// URI and a QR code as an SVG data URI (QRCoder 1.8.0); /totp/enable takes the app's first code, shows the recovery codes once and ends every other session (this device gets a new one); /totp/disable takes the password and a code. GET /clientapi/user/account says whether it is on and how many recovery codes are left. - Sign-in (/clientapi/user/login, the invitation login, /oauth/login) asks for LoginForm.OneTimeCode with a 428, and only once the password was right; the OAuth page has the field. Recovery by email leaves two-factor on. - PrivaPub admin reset-totp <root>: the way back for a login that lost its authenticator and its codes. - scenarios/accounts.sh checks it all, with codes computed in Python; the OAuth page with its antiforgery token. Verified: dotnet build PrivaPub.sln -c Release; the pasture (gts, mailpit): accounts 45/45. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
1 parent
cff75e3a47
commit
1f3bb27e17
13 files changed
+372
-5
No files matched your search
@@ -21,6 +21,7 @@ namespace PrivaPub.Infrastructure.Cli
|
||||
PrivaPub admin create-root <login> [--admin] the password is read from standard input
|
||||
PrivaPub admin move-persona <persona> <root> gives a persona to another login; its Mastodon tokens end
|
||||
PrivaPub admin delete-root <root> deletes a login and every persona it still has
|
||||
PrivaPub admin reset-totp <root> turns a login's two-factor sign-in off
|
||||
PrivaPub admin media audit [--fix] media files against what holds them; --fix (as www-data) trashes
|
||||
what nothing holds and gives today's pictures their rows
|
||||
PrivaPub admin backup [--kind manual|pre-deploy] [--db-only]
|
||||
@@ -52,6 +53,8 @@ namespace PrivaPub.Infrastructure.Cli
|
||||
return await MovePersona(services, persona, root, output);
|
||||
case ["delete-root", var root]:
|
||||
return await DeleteRoot(services, root, output);
|
||||
case ["reset-totp", var root]:
|
||||
return await ResetTotp(root, output);
|
||||
case ["media", "audit", .. var flags] when flags.All(f => f == "--fix"):
|
||||
return await AuditMedia(services, flags.Contains("--fix"), output);
|
||||
case ["backup", "verify", var id]:
|
||||
@@ -267,6 +270,22 @@ namespace PrivaPub.Infrastructure.Cli
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The way back for a login that lost its authenticator and its recovery codes: its two-factor sign-in goes, its
|
||||
// password stays.
|
||||
static async Task<int> ResetTotp(string login, TextWriter output)
|
||||
{
|
||||
login = login.ToLowerInvariant();
|
||||
var updated = await DB.Default.Update<RootUser>().Match(u => u.UserName == login && u.DeletedAt == null)
|
||||
.Modify(u => u.Totp, null).ExecuteAsync();
|
||||
if (updated.MatchedCount == 0)
|
||||
{
|
||||
Console.Error.WriteLine($"no root user '{login}'");
|
||||
return 1;
|
||||
}
|
||||
output.WriteLine($"{login}: two-factor sign-in is off");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A login deleted from the box as the client deletes its own (RootRemoval): every persona it still has goes with it,
|
||||
// announced deleted, and its sessions end.
|
||||
static async Task<int> DeleteRoot(IServiceProvider services, string login, TextWriter output)
|
||||
|
||||
Reference in new issue
Block a user