Protect Tabularium with persistent authenticator-code login
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 5m28s
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 5m28s
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { chmodSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { authId, matchingStep, type AuthConfig } from "./auth";
|
||||
|
||||
function database() {
|
||||
const configFile = process.env.AUTH_CONFIG_FILE;
|
||||
if (!configFile) throw new Error("Authentication is not configured");
|
||||
const file = join(dirname(configFile), "login-state.sqlite");
|
||||
const db = new DatabaseSync(file);
|
||||
chmodSync(file, 0o600);
|
||||
db.exec(`PRAGMA busy_timeout=5000;
|
||||
CREATE TABLE IF NOT EXISTS login_state (
|
||||
id TEXT PRIMARY KEY, window_start INTEGER NOT NULL,
|
||||
attempts INTEGER NOT NULL DEFAULT 0, last_step INTEGER NOT NULL DEFAULT -1
|
||||
);`);
|
||||
return db;
|
||||
}
|
||||
|
||||
// SQLite's write transaction serializes attempts across workers and restarts.
|
||||
export function consumeCode(
|
||||
config: AuthConfig,
|
||||
code: string,
|
||||
now = Date.now(),
|
||||
): "ok" | "invalid" | "limited" {
|
||||
const db = database();
|
||||
try {
|
||||
db.exec("BEGIN IMMEDIATE");
|
||||
const id = authId(config);
|
||||
db.prepare(
|
||||
"INSERT OR IGNORE INTO login_state(id,window_start) VALUES (?,?)",
|
||||
).run(id, now);
|
||||
const state = db
|
||||
.prepare("SELECT * FROM login_state WHERE id=?")
|
||||
.get(id) as {
|
||||
window_start: number;
|
||||
attempts: number;
|
||||
last_step: number;
|
||||
};
|
||||
const expired = now - state.window_start >= 300000;
|
||||
const attempts = expired ? 0 : state.attempts;
|
||||
let result: "ok" | "invalid" | "limited" = "limited";
|
||||
if (attempts < 20) {
|
||||
db.prepare(
|
||||
"UPDATE login_state SET attempts=?,window_start=? WHERE id=?",
|
||||
).run(attempts + 1, expired ? now : state.window_start, id);
|
||||
const step = matchingStep(config.secret, code, now);
|
||||
result = step !== null && step > state.last_step ? "ok" : "invalid";
|
||||
if (result === "ok")
|
||||
db.prepare("UPDATE login_state SET last_step=? WHERE id=?").run(
|
||||
step!,
|
||||
id,
|
||||
);
|
||||
}
|
||||
db.exec("COMMIT");
|
||||
return result;
|
||||
} catch (error) {
|
||||
try {
|
||||
db.exec("ROLLBACK");
|
||||
} catch {
|
||||
/* Transaction may not have started. */
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeStore() {
|
||||
const db = database();
|
||||
db.close();
|
||||
}
|
||||
Reference in New Issue
Block a user