Add two-factor login (TOTP) with recovery codes
Users can set up an authenticator app from their account page; login then asks for a code after the password, with one-time recovery codes as a fallback. Sensitive actions (tokens, user changes, 2FA resets) need a code from the last 5 minutes. totp.require (none/admins/all) forces setup, and admins can reset a user's 2FA from the UI, the jarl CLI or jarld on the server. The dashboard and jarl status flag users missing 2FA and a shared "admin" account still in use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b22ffee1bb
commit
8f9cdd0a17
35 files changed
+1965
-43
No files matched your search
@@ -23,8 +23,27 @@ export class APIError extends Error {
|
||||
let onUnauthorized = () => {};
|
||||
export function setUnauthorizedHandler(fn) { onUnauthorized = fn; }
|
||||
|
||||
// api calls the REST API. opts: {body, stage (bool), query}.
|
||||
// With two-factor login, some actions (API tokens, users) need a code
|
||||
// from the last few minutes. The server then answers totp_step_up (or
|
||||
// totp_invalid for a wrong code); askCode asks for one, and the request is
|
||||
// sent again with it. It returns null when the user cancels.
|
||||
let askCode = null;
|
||||
export function setCodePrompt(fn) { askCode = fn; }
|
||||
const stepUpCodes = new Set(["totp_step_up", "totp_invalid"]);
|
||||
|
||||
// api calls the REST API. opts: {body, stage (bool), query, code}.
|
||||
export async function api(method, path, opts = {}) {
|
||||
try {
|
||||
return await call(method, path, opts);
|
||||
} catch (e) {
|
||||
if (!askCode || e.status !== 403 || !stepUpCodes.has(e.code) || path.startsWith("/me/totp/disable")) throw e;
|
||||
const code = await askCode(e.code === "totp_invalid" ? e.message : "");
|
||||
if (!code) throw e;
|
||||
return api(method, path, { ...opts, code });
|
||||
}
|
||||
}
|
||||
|
||||
async function call(method, path, opts) {
|
||||
const headers = { "Accept": "application/json" };
|
||||
let body;
|
||||
if (opts.body !== undefined) {
|
||||
@@ -40,6 +59,7 @@ export async function api(method, path, opts = {}) {
|
||||
if (s) url += (url.includes("?") ? "&" : "?") + s;
|
||||
}
|
||||
if (opts.stage) headers["X-Jarl-Changeset"] = String(opts.stage);
|
||||
if (opts.code) headers["X-Jarl-TOTP"] = opts.code;
|
||||
const resp = await fetch(url, { method, headers, body, credentials: "same-origin" });
|
||||
if (resp.status === 204) return null;
|
||||
const ct = resp.headers.get("Content-Type") || "";
|
||||
|
||||
Reference in new issue
Block a user