summaryrefslogtreecommitdiff
path: root/src/cmd_auth.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd_auth.c')
-rw-r--r--src/cmd_auth.c67
1 files changed, 40 insertions, 27 deletions
diff --git a/src/cmd_auth.c b/src/cmd_auth.c
index 86dab12..502057c 100644
--- a/src/cmd_auth.c
+++ b/src/cmd_auth.c
@@ -18,38 +18,33 @@
/* login rate limiting (in-memory, per key) */
/* ------------------------------------------------------------------ */
-#define RL_MAX_KEYS 16
+/* Keyed per user name ("u:<name>") and per user for password changes
+ ("pw:<id>"): wrong guesses lock the guessed account, never everybody.
+ A full table reuses an expired entry, else the least-failed one, so the
+ limiter never switches itself off. */
+#define RL_MAX_KEYS 1024
#define RL_MAX_FAILS 5
#define RL_WINDOW 900
struct rl_entry {
- char key[64];
+ char key[80];
int fails;
int64_t window_end;
};
static struct rl_entry g_rl[RL_MAX_KEYS];
-static struct rl_entry *rl_get(const char *key, int create)
+static struct rl_entry *rl_find(const char *key)
{
- struct rl_entry *slot = NULL;
- for (size_t i = 0; i < RL_MAX_KEYS; i++) {
+ for (size_t i = 0; i < RL_MAX_KEYS; i++)
if (g_rl[i].key[0] && strcmp(g_rl[i].key, key) == 0)
return &g_rl[i];
- if (create && !g_rl[i].key[0] && !slot)
- slot = &g_rl[i];
- }
- if (create && slot) {
- snprintf(slot->key, sizeof slot->key, "%s", key);
- slot->fails = 0;
- slot->window_end = 0;
- }
- return slot;
+ return NULL;
}
static int rl_blocked(const char *key, int64_t *retry_after)
{
- struct rl_entry *e = rl_get(key, 0);
+ struct rl_entry *e = rl_find(key);
if (!e || e->fails < RL_MAX_FAILS)
return 0;
int64_t now = util_now();
@@ -62,22 +57,38 @@ static int rl_blocked(const char *key, int64_t *retry_after)
static void rl_fail(const char *key)
{
- struct rl_entry *e = rl_get(key, 1);
- if (!e)
- return;
int64_t now = util_now();
- if (e->fails == 0 || e->window_end <= now)
+ struct rl_entry *e = rl_find(key);
+ if (!e) {
+ /* a free or expired slot, else the one with the fewest failures
+ (a blocked entry is replaced last, so flooding the table with
+ names does not lift a block) */
+ e = &g_rl[0];
+ for (size_t i = 0; i < RL_MAX_KEYS; i++) {
+ struct rl_entry *c = &g_rl[i];
+ if (!c->key[0] || c->window_end <= now) {
+ e = c;
+ break;
+ }
+ if (c->fails < e->fails ||
+ (c->fails == e->fails && c->window_end < e->window_end))
+ e = c;
+ }
+ memset(e, 0, sizeof *e);
+ snprintf(e->key, sizeof e->key, "%s", key);
+ }
+ if (e->fails == 0 || e->window_end <= now) {
+ e->fails = 0;
e->window_end = now + RL_WINDOW;
+ }
e->fails++;
}
static void rl_ok(const char *key)
{
- struct rl_entry *e = rl_get(key, 0);
- if (e) {
- e->fails = 0;
- e->window_end = 0;
- }
+ struct rl_entry *e = rl_find(key);
+ if (e)
+ memset(e, 0, sizeof *e);
}
/* ------------------------------------------------------------------ */
@@ -188,7 +199,9 @@ static yyjson_mut_val *h_session_open(struct req *r)
return fail(r, "INVALID_ARGS", "username and password are required");
}
int64_t retry = 0;
- if (rl_blocked("local", &retry)) {
+ char rlkey[80];
+ snprintf(rlkey, sizeof rlkey, "u:%s", username);
+ if (rl_blocked(rlkey, &retry)) {
free(reqjson);
return failf(r, "RATE_LIMITED",
"too many failed logins, retry in %lld seconds",
@@ -202,14 +215,14 @@ static yyjson_mut_val *h_session_open(struct req *r)
auth_verify_password(pwhash, password) == 0;
free(pwhash);
if (!ok) {
- rl_fail("local");
+ rl_fail(rlkey);
audit_append(r->db, 0, uid > 0 ? uid : 0, 0, "auth.fail", reqjson,
"AUTH_FAILED", NULL);
free(display);
free(reqjson);
return fail(r, "AUTH_FAILED", "invalid credentials");
}
- rl_ok("local");
+ rl_ok(rlkey);
int64_t active = 0;
yyjson_mut_val *orgs =
orgs_for_user(r->db, r->rdoc, uid, 0, &active);