aboutsummaryrefslogtreecommitdiff
path: root/auth.c
diff options
context:
space:
mode:
authorXavier Del Campo Romero <xavi92@disroot.org>2025-09-22 17:32:44 +0200
committerXavier Del Campo Romero <xavi92@disroot.org>2026-02-13 09:57:39 +0100
commit78bf2fe4a5bf37514f6dfd203ef969da0bf40c2e (patch)
tree33f9440b8ee0fa7a3b3ad033616d722d2101bb4d /auth.c
parent107a2e43d54f9a42fb85b00b83cb0d9abb194680 (diff)
Setup project skeletonHEADmaster
Diffstat (limited to 'auth.c')
-rw-r--r--auth.c327
1 files changed, 327 insertions, 0 deletions
diff --git a/auth.c b/auth.c
new file mode 100644
index 0000000..f8fd6bd
--- /dev/null
+++ b/auth.c
@@ -0,0 +1,327 @@
+/*
+ * nanobbs, a tiny forums software.
+ * Copyright (C) 2025-2026 Xavier Del Campo Romero
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#define _POSIX_C_SOURCE 200809L
+
+#include "auth.h"
+#include "db.h"
+#include "defs.h"
+#include "endpoints.h"
+#include "jwt.h"
+#include "op.h"
+#include <dynstr.h>
+#include <libweb/http.h>
+#include <sodium.h>
+#include <sqlite3.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+struct validate
+{
+ bool valid;
+ unsigned long id;
+ enum auth_role role;
+ sqlite3 *db;
+ auth_fn fn;
+ void *user;
+};
+
+static void free_validate(void *const p)
+{
+ int error;
+ struct validate *const v = p;
+
+ if (!v)
+ return;
+ else if ((error = sqlite3_close(v->db)) != SQLITE_OK)
+ fprintf(stderr, "%s: sqlite3_close: %s\n", __func__,
+ sqlite3_errstr(error));
+
+ free(v);
+}
+
+static int check_role(const char *const s, enum auth_role *const out)
+{
+ static const struct r
+ {
+ const char *s;
+ enum auth_role role;
+ } roles[] =
+ {
+ {"admin", AUTH_ROLE_ADMIN},
+ {"mod", AUTH_ROLE_MOD},
+ {"user", AUTH_ROLE_USER},
+ {"banned", AUTH_ROLE_BANNED}
+ };
+
+ for (size_t i = 0; i < sizeof roles / sizeof *roles; i++)
+ {
+ const struct r *const r = &roles[i];
+
+ if (!strcmp(s, r->s))
+ {
+ *out = r->role;
+ return 0;
+ }
+ }
+
+ fprintf(stderr, "%s: invalid role: %s\n", __func__, s);
+ return -1;
+}
+
+static int end(const struct http_payload *const p,
+ struct http_response *const r, void *const user, void *const args)
+{
+ int ret = -1;
+ struct validate *const v = args;
+
+ if (!v->valid)
+ ret = v->fn(p, r, v->user, v->db, NULL);
+ else
+ {
+ const struct http_cookie *const c = &p->cookie;
+ const char *const username = c->field;
+ const struct auth_user u =
+ {
+ .username = username,
+ .role = v->role,
+ .id = v->id
+ };
+
+ ret = v->fn(p, r, v->user, v->db, &u);
+ }
+
+ free(v);
+ return ret;
+}
+
+static int signkey(const struct http_cookie *const c, const char *const key,
+ struct validate *const v)
+{
+ int ret = -1;
+ cJSON *j = NULL;
+ unsigned char dkey[crypto_auth_hmacsha256_KEYBYTES];
+ const size_t len = strlen(key), explen = sizeof dkey * 2;
+ const cJSON *u;
+ const char *user;
+
+ if (len != explen)
+ {
+ fprintf(stderr, "%s: key size mismatch, got %zu, expected %zu\n",
+ __func__, len, explen);
+ goto end;
+ }
+ else if (sodium_hex2bin(dkey, sizeof dkey, key, len, NULL, NULL, NULL))
+ {
+ fprintf(stderr, "%s: sodium_hex2bin failed\n", __func__);
+ goto end;
+ }
+ else if ((ret = jwt_decode(c->value, dkey, sizeof dkey, &j)))
+ {
+ if (ret < 0)
+ fprintf(stderr, "%s: jwt_check failed\n", __func__);
+
+ goto end;
+ }
+ else if (!(u = cJSON_GetObjectItem(j, "name"))
+ || !(user = cJSON_GetStringValue(u))
+ || strcmp(user, c->field))
+ {
+ ret = 1;
+ goto end;
+ }
+
+ ret = 0;
+
+end:
+ cJSON_Delete(j);
+ return ret;
+}
+
+static int row(sqlite3_stmt *const stmt, const struct http_payload *const p,
+ struct http_response *const r, void *const user, void *const args)
+{
+ int ret = -1, error;
+ struct validate *const v = args;
+ sqlite3 *const db = v->db;
+ char *const key = db_str(db, stmt, "signkey"), *name = NULL;
+ enum auth_role role;
+ unsigned id;
+
+ if (!key)
+ {
+ fprintf(stderr, "%s: db_str %s failed\n", __func__, "signkey");
+ goto end;
+ }
+ else if ((ret = db_uint(db, stmt, "id", &id)))
+ {
+ fprintf(stderr, "%s: db_uint %s failed\n", __func__, "id");
+ goto end;
+ }
+ else if (!(name = db_str(db, stmt, "name")))
+ {
+ fprintf(stderr, "%s: db_str %s failed\n", __func__, "name");
+ goto end;
+ }
+ else if (check_role(name, &role))
+ {
+ fprintf(stderr, "%s: role failed\n", __func__);
+ goto end;
+ }
+ else if ((error = signkey(&p->cookie, key, v)))
+ {
+ if (error < 0)
+ {
+ fprintf(stderr, "%s: signkey failed\n", __func__);
+ goto end;
+ }
+ }
+ else if (!error)
+ {
+ v->valid = true;
+ v->id = id;
+ v->role = role;
+ }
+
+ ret = 0;
+
+end:
+ free(name);
+ free(key);
+ return ret;
+}
+
+static int check_username(const char *const name)
+{
+ return strspn(name, USER_SYMS) != strlen(name);
+}
+
+static int setup(const struct http_payload *const p,
+ struct http_response *const r, void *const user, void *const args)
+{
+ int ret = -1;
+ const struct http_cookie *const c = &p->cookie;
+ const char *const username = c->field;
+ struct validate *const v = args;
+ struct dynstr d;
+
+ dynstr_init(&d);
+
+ if (dynstr_append(&d, "SELECT users.id, users.signkey, roles.name "
+ "FROM users JOIN roles ON users.roleid = roles.id "
+ "WHERE users.name = '%s';", username))
+ {
+ fprintf(stderr, "%s: dynstr_append failed\n", __func__);
+ goto end;
+ }
+
+ const struct op_cfg op =
+ {
+ .error = free_validate,
+ .end = end,
+ .row = row,
+ .args = v
+ };
+
+ if (!op_run(v->db, d.str, &op, r))
+ {
+ fprintf(stderr, "%s: op_run failed\n", __func__);
+ goto end;
+ }
+
+ ret = 0;
+
+end:
+
+ if (ret)
+ free_validate(v);
+
+ dynstr_free(&d);
+ return ret;
+}
+
+static int open_db(const struct http_payload *const p,
+ struct http_response *const r, void *const user, void *const args)
+{
+ struct validate *const v = args;
+ const struct cfg *const cfg = user;
+ const int error = db_open(cfg->dir, &v->db);
+
+ if (error != SQLITE_OK)
+ {
+ if (error != SQLITE_BUSY)
+ {
+ fprintf(stderr, "%s: db_open: %s\n", __func__,
+ sqlite3_errstr(error));
+ goto failure;
+ }
+ }
+ else
+ *r = (const struct http_response)
+ {
+ .step.payload = setup,
+ .args = v
+ };
+
+ return 0;
+
+failure:
+ free_validate(v);
+ return -1;
+}
+
+int auth_validate(const struct http_payload *const p,
+ struct http_response *const r, void *const user, const auth_fn fn)
+{
+ int ret = -1;
+ struct validate *v = NULL;
+ const struct http_cookie *const c = &p->cookie;
+ const char *const username = c->field;
+
+ if (!username || !c->value || check_username(username))
+ {
+ ret = 1;
+ goto failure;
+ }
+ else if (!(v = malloc(sizeof *v)))
+ {
+ fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno));
+ goto failure;
+ }
+
+ *r = (const struct http_response)
+ {
+ .step.payload = open_db,
+ .free = free_validate,
+ .args = v
+ };
+
+ *v = (const struct validate)
+ {
+ .fn = fn,
+ .user = user
+ };
+
+ return 0;
+
+failure:
+ free(v);
+ return ret;
+}