webui: persist login sessions so they survive restarts
Sessions lived in an in-memory array, so every daemon restart wiped them and forced a re-login. Move them into the webui DB: - New sessions table storing a SHA-256 of the bearer token (never the raw token, so a DB read can't be replayed), the user, role, and an absolute expiry. - create/lookup/touch/delete + per-user delete + prune in webui_store. - Login persists the session; auth checks validate against the DB with a throttled sliding expiry (re-extended at most hourly to avoid a write per request); logout and admin reset/delete drop the rows. Expired rows are reaped lazily on lookup and pruned at startup. - TTL is configurable via NAUT_SESSION_TTL (default 7 days) and drives the cookie Max-Age. Removes the in-memory session array + auth_lock. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
096535292d
commit
067b62c23a
3 changed files with 190 additions and 70 deletions
|
|
@ -79,6 +79,13 @@ webui_store *webui_store_open(const char *path) {
|
|||
" pw_iters INTEGER NOT NULL,"
|
||||
" role TEXT NOT NULL DEFAULT 'user',"
|
||||
" created_at INTEGER NOT NULL);"
|
||||
"CREATE TABLE IF NOT EXISTS sessions ("
|
||||
" token_hash TEXT PRIMARY KEY,"
|
||||
" username TEXT NOT NULL,"
|
||||
" role TEXT NOT NULL DEFAULT 'user',"
|
||||
" expires INTEGER NOT NULL);"
|
||||
"CREATE INDEX IF NOT EXISTS sessions_user ON sessions(username);"
|
||||
"CREATE INDEX IF NOT EXISTS sessions_expires ON sessions(expires);"
|
||||
"CREATE TABLE IF NOT EXISTS categories ("
|
||||
" name TEXT PRIMARY KEY,"
|
||||
" save_path TEXT NOT NULL DEFAULT '');"
|
||||
|
|
@ -323,6 +330,131 @@ bool webui_store_list_users(webui_store *s, json_t *out) {
|
|||
return ok;
|
||||
}
|
||||
|
||||
/* --- sessions ------------------------------------------------------------- */
|
||||
|
||||
/* SHA-256 of a bearer token, hex-encoded. We persist only this, never the raw
|
||||
* token, so a DB leak can't be replayed as a live cookie. */
|
||||
static void sha256_hex(const char *token, char out[65]) {
|
||||
unsigned char d[32];
|
||||
unsigned int dl = 0;
|
||||
EVP_Digest(token, strlen(token), d, &dl, EVP_sha256(), NULL);
|
||||
to_hex(d, 32, out);
|
||||
}
|
||||
|
||||
bool webui_store_session_create(webui_store *s, const char *token,
|
||||
const char *user, const char *role,
|
||||
long expires) {
|
||||
if (!s || !token || !*token || !user || !*user) return false;
|
||||
char th[65];
|
||||
sha256_hex(token, th);
|
||||
pthread_mutex_lock(&s->lock);
|
||||
sqlite3_stmt *st = NULL;
|
||||
bool ok = false;
|
||||
if (sqlite3_prepare_v2(s->db,
|
||||
"INSERT OR REPLACE INTO sessions (token_hash,username,role,expires)"
|
||||
" VALUES (?,?,?,?);", -1, &st, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(st, 1, th, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(st, 2, user, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(st, 3, role && *role ? role : "user", -1, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 4, (sqlite3_int64)expires);
|
||||
ok = sqlite3_step(st) == SQLITE_DONE;
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
pthread_mutex_unlock(&s->lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool webui_store_session_lookup(webui_store *s, const char *token,
|
||||
char *user, size_t user_sz,
|
||||
char *role, size_t role_sz, long *expires_out) {
|
||||
if (!s || !token || !*token) return false;
|
||||
char th[65];
|
||||
sha256_hex(token, th);
|
||||
pthread_mutex_lock(&s->lock);
|
||||
sqlite3_stmt *st = NULL;
|
||||
bool ok = false;
|
||||
if (sqlite3_prepare_v2(s->db,
|
||||
"SELECT username, role, expires FROM sessions WHERE token_hash=?;",
|
||||
-1, &st, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(st, 1, th, -1, SQLITE_STATIC);
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
const char *u = (const char *)sqlite3_column_text(st, 0);
|
||||
const char *r = (const char *)sqlite3_column_text(st, 1);
|
||||
if (user) snprintf(user, user_sz, "%s", u ? u : "");
|
||||
if (role) snprintf(role, role_sz, "%s", r ? r : "user");
|
||||
if (expires_out) *expires_out = (long)sqlite3_column_int64(st, 2);
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
pthread_mutex_unlock(&s->lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool webui_store_session_touch(webui_store *s, const char *token, long expires) {
|
||||
if (!s || !token) return false;
|
||||
char th[65];
|
||||
sha256_hex(token, th);
|
||||
pthread_mutex_lock(&s->lock);
|
||||
sqlite3_stmt *st = NULL;
|
||||
bool ok = false;
|
||||
if (sqlite3_prepare_v2(s->db,
|
||||
"UPDATE sessions SET expires=? WHERE token_hash=?;",
|
||||
-1, &st, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)expires);
|
||||
sqlite3_bind_text(st, 2, th, -1, SQLITE_STATIC);
|
||||
ok = sqlite3_step(st) == SQLITE_DONE;
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
pthread_mutex_unlock(&s->lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool webui_store_session_delete(webui_store *s, const char *token) {
|
||||
if (!s || !token) return false;
|
||||
char th[65];
|
||||
sha256_hex(token, th);
|
||||
pthread_mutex_lock(&s->lock);
|
||||
sqlite3_stmt *st = NULL;
|
||||
bool ok = false;
|
||||
if (sqlite3_prepare_v2(s->db, "DELETE FROM sessions WHERE token_hash=?;",
|
||||
-1, &st, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(st, 1, th, -1, SQLITE_STATIC);
|
||||
ok = sqlite3_step(st) == SQLITE_DONE;
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
pthread_mutex_unlock(&s->lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool webui_store_sessions_delete_user(webui_store *s, const char *user) {
|
||||
if (!s || !user) return false;
|
||||
pthread_mutex_lock(&s->lock);
|
||||
sqlite3_stmt *st = NULL;
|
||||
bool ok = false;
|
||||
if (sqlite3_prepare_v2(s->db, "DELETE FROM sessions WHERE username=?;",
|
||||
-1, &st, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(st, 1, user, -1, SQLITE_STATIC);
|
||||
ok = sqlite3_step(st) == SQLITE_DONE;
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
pthread_mutex_unlock(&s->lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void webui_store_sessions_prune(webui_store *s, long now) {
|
||||
if (!s) return;
|
||||
pthread_mutex_lock(&s->lock);
|
||||
sqlite3_stmt *st = NULL;
|
||||
if (sqlite3_prepare_v2(s->db, "DELETE FROM sessions WHERE expires<=?;",
|
||||
-1, &st, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)now);
|
||||
sqlite3_step(st);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
pthread_mutex_unlock(&s->lock);
|
||||
}
|
||||
|
||||
/* --- taxonomy ------------------------------------------------------------- */
|
||||
|
||||
/* Replace one table's contents from a json array, inside a transaction. The
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue