feat: slow down repeated failed logins

The login endpoint answered guesses as fast as they arrived, against a
single static password.

Wrong attempts now back off — 1s, 2s, 4s, up to 30s — and a correct one
clears the count. It is a single counter rather than a table keyed by
address: there is only one account, so an attacker gains nothing by
rotating source IPs, and there is no per-client map to grow. The cap is
there so that someone hammering the login delays the owner by half a
minute rather than locking them out.

Wrong credentials now answer 401 rather than 400. The server also warns
at startup if DIARY_PASSWORD is still the value from .env.example.
This commit is contained in:
nvrl committed 2026-09-05 09:47:27 +02:00
1 parent f0135d34ac
commit 41d2d7ef77
7 files changed
+119 -3

No files matched your search

+9 -1
View File
@@ -23,9 +23,17 @@ pub async fn login(
State(state): State<AppState>,
Json(body): Json<LoginRequest>,
) -> AppResult<impl IntoResponse> {
state.login_throttle.check().map_err(|wait| {
tracing::warn!("login refused, {wait}s of backoff remaining");
AppError::TooManyRequests(wait)
})?;
if !auth::credentials_match(&state.config, &body.username, &body.password) {
return Err(AppError::BadRequest("invalid credentials".into()));
state.login_throttle.record_failure();
return Err(AppError::Unauthorized);
}
state.login_throttle.record_success();
let token = auth::issue_token(&state.config);
Ok((
StatusCode::OK,