timekeeper

[Abandoned unfinished] CGI web application in C for time tracking. (My first, just a learning project)
Log | Files | Refs | README

commit 19f817d2a2f3daebc2b1c4770c23645b293e9b65
parent 2c819509cfacbb2c83a84eeb1418d555fe324ea5
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date:   Sun, 24 Mar 2024 18:18:59 -0700

Add authentication tokens separate from user hash

These tokens are generated using bcrypt(3) with the user hash as
the key and a new salt and expire at a set time. The benifit of
this method over just using the user hash is that there is a unique
token for each session, leaked tokens don't forever compromise an
account, and tokens can be revoked.

Diffstat:
Mconfig.h | 1+
Mpage.c | 2+-
Mpages/login.c | 2+-
Mstmt.c | 21+++++++++++++++++++++
Mstmt.h | 4++++
Muser.c | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Muser.h | 1+
7 files changed, 79 insertions(+), 2 deletions(-)

diff --git a/config.h b/config.h @@ -1 +1,2 @@ #define DataDir "/var/timekeeper" +#define TokenTTL (60 * 60 * 8) /* 8 hours */ diff --git a/page.c b/page.c @@ -24,7 +24,7 @@ loadpagerequest(struct kfcgi *fcgi, struct pagedata *pd) if (!pd->req.cookiemap[KeyHash]) return KCGI_OK; - status = getuser(&pd->user, pd, "hash", pd->req.cookiemap[KeyHash]->parsed.s); + status = getuser(&pd->user, pd, "token", pd->req.cookiemap[KeyHash]->parsed.s); if (status == LoginValid || status == LoginNotFound) return KCGI_OK; diff --git a/pages/login.c b/pages/login.c @@ -59,7 +59,7 @@ pagelogin(struct pagedata *pd) userstatus = loginuser(&user, pd, fuser, fpass); if (userstatus == LoginValid) { status = khttp_head(&pd->req, kresps[KRESP_SET_COOKIE], "%s=%s; Path=/", - pd->keys[KeyHash].name, user->hash); + pd->keys[KeyHash].name, user->auth); freeuser(user); if (status != KCGI_OK) return status; diff --git a/stmt.c b/stmt.c @@ -29,6 +29,19 @@ struct sqlbox_pstmt pstmts[] = { " SELECT userid, max(entry)\n" " FROM times GROUP BY userid" }, + [StmtInit4] = { .stmt = + "CREATE TABLE IF NOT EXISTS authtokens (\n" + " userid TEXT REFERENCES auth (hash),\n" + " token TEXT NOT NULL PRIMARY KEY,\n" + " expires INTEGER NOT NULL\n" + ")" + }, + [StmtInit5] = { .stmt = + "CREATE TRIGGER IF NOT EXISTS prune_authtokens\n" + " AFTER INSERT ON authtokens FOR EACH ROW BEGIN\n" + " DELETE FROM authtokens WHERE expires <= unixepoch();\n" + "END" + }, [StmtInitLast] = { .stmt = "CREATE TRIGGER IF NOT EXISTS userdelete\n" " AFTER DELETE ON auth FOR EACH ROW BEGIN\n" @@ -40,7 +53,15 @@ struct sqlbox_pstmt pstmts[] = { }, [StmtGetUserByName] = { .stmt = "SELECT * FROM auth WHERE name IS ?" }, [StmtGetUserByHash] = { .stmt = "SELECT * FROM auth WHERE hash IS ?" }, + [StmtGetUserByToken] = { .stmt = + "SELECT * FROM auth WHERE hash IS\n" + " (SELECT (userid) FROM authtokens WHERE token IS ? AND\n" + " expires > unixepoch())" + }, [StmtDeleteUser] = { .stmt = "DELETE FROM auth WHERE hash IS ?" }, + [StmtAddAuthToken] = { .stmt = + "INSERT into authtokens VALUES (?, ?, ?)" + }, /* * Currently, the user can set the end time with a diff --git a/stmt.h b/stmt.h @@ -3,11 +3,15 @@ enum StmtID { StmtInit1, StmtInit2, StmtInit3, + StmtInit4, + StmtInit5, StmtInitLast, StmtAddUser, StmtGetUserByHash, StmtGetUserByName, + StmtGetUserByToken, StmtDeleteUser, + StmtAddAuthToken, StmtStartTime, StmtStartBreakTime, StmtEndBreakTime, diff --git a/user.c b/user.c @@ -5,8 +5,10 @@ #include <sys/types.h> #include <stdarg.h> #include <stdint.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> +#include <time.h> #include <kcgi.h> #include <kcgihtml.h> @@ -20,6 +22,8 @@ #include "user.h" #include "util.h" +#include "config.h" + char *user_errors[] = { [LoginValid] = "Login valid", [LoginNotFound] = "User not found", @@ -65,6 +69,7 @@ freeuser(struct user *user) return; free(user->hash); free(user->name); + free(user->auth); } enum user_error @@ -82,6 +87,8 @@ getuser(struct user **ruser, struct pagedata *pd, char *field, char *value) gets = StmtGetUserByName; else if (strcmp(field, "hash") == 0) gets = StmtGetUserByHash; + else if (strcmp(field, "token") == 0) + gets = StmtGetUserByToken; else gets = -1; @@ -123,6 +130,43 @@ getuser(struct user **ruser, struct pagedata *pd, char *field, char *value) return LoginValid; } +enum sqlbox_code +addtoken(struct pagedata *pd, char *hash, char *token, time_t expires) +{ + struct sqlbox_parm params[] = { + { .sparm = hash, .type = SQLBOX_PARM_STRING }, + { .sparm = token, .type = SQLBOX_PARM_STRING }, + { .iparm = expires, .type = SQLBOX_PARM_INT } + }; + + return sqlbox_exec(pd->db, pd->dbid, StmtAddAuthToken, + Len(params), params, 0); +} + +char * +newtoken(struct pagedata *pd, char *hash) +{ + time_t expires; + char *salt, *token; + + salt = bcrypt_gensalt(12); + if (!salt) { + kutil_warn(NULL, NULL, "Unable to generate salt for token"); + return NULL; + } + + token = bcrypt(hash, salt); + if (!token || !(token = strdup(token))) + return NULL; + + expires = time(NULL) + TokenTTL; + if (addtoken(pd, hash, token, expires) != SQLBOX_CODE_OK) { + free(token); + return NULL; + } + return token; +} + enum user_error loginuser(struct user **ruser, struct pagedata *pd, char *name, char *key) { @@ -144,6 +188,12 @@ loginuser(struct user **ruser, struct pagedata *pd, char *name, char *key) return LoginInvalid; } + user->auth = newtoken(pd, user->hash); + if (!user->auth) { + freeuser(user); + return LoginSystem; + } + if (ruser) *ruser = user; else diff --git a/user.h b/user.h @@ -15,6 +15,7 @@ enum user_error { struct user { char *name; char *hash; + char *auth; }; struct pagedata;