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:
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;