Skip to content

Commit 41f8974

Browse files
committed
Record password login attempts
1 parent cd2a5c1 commit 41f8974

File tree

1 file changed

+21
-2
lines changed

1 file changed

+21
-2
lines changed

crates/handlers/src/views/login.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// SPDX-License-Identifier: AGPL-3.0-only
55
// Please see LICENSE in the repository root for full details.
66

7-
use std::sync::Arc;
7+
use std::sync::{Arc, LazyLock};
88

99
use axum::{
1010
extract::{Form, Query, State},
@@ -30,17 +30,27 @@ use mas_templates::{
3030
AccountInactiveContext, FieldError, FormError, FormState, LoginContext, LoginFormField,
3131
PostAuthContext, PostAuthContextInner, TemplateContext, Templates, ToFormState,
3232
};
33+
use opentelemetry::{Key, KeyValue, metrics::Counter};
3334
use rand::Rng;
3435
use serde::{Deserialize, Serialize};
3536
use zeroize::Zeroizing;
3637

3738
use super::shared::OptionalPostAuthAction;
3839
use crate::{
39-
BoundActivityTracker, Limiter, PreferredLanguage, RequesterFingerprint, SiteConfig,
40+
BoundActivityTracker, Limiter, METER, PreferredLanguage, RequesterFingerprint, SiteConfig,
4041
passwords::PasswordManager,
4142
session::{SessionOrFallback, load_session_or_fallback},
4243
};
4344

45+
static PASSWORD_LOGIN_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
46+
METER
47+
.u64_counter("mas.user.password_login_attempt")
48+
.with_description("Number of password login attempts")
49+
.with_unit("{attempt}")
50+
.build()
51+
});
52+
const RESULT: Key = Key::from_static_str("result");
53+
4454
#[derive(Debug, Deserialize, Serialize)]
4555
pub(crate) struct LoginForm {
4656
username: String,
@@ -156,6 +166,7 @@ pub(crate) async fn post(
156166
}
157167

158168
if !form_state.is_valid() {
169+
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
159170
return render(
160171
locale,
161172
cookie_jar,
@@ -178,6 +189,7 @@ pub(crate) async fn post(
178189
// First, lookup the user
179190
let Some(user) = repo.user().find_by_username(username).await? else {
180191
let form_state = form_state.with_error_on_form(FormError::InvalidCredentials);
192+
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
181193
return render(
182194
locale,
183195
cookie_jar,
@@ -196,6 +208,7 @@ pub(crate) async fn post(
196208
if let Err(e) = limiter.check_password(requester, &user) {
197209
tracing::warn!(error = &e as &dyn std::error::Error);
198210
let form_state = form_state.with_error_on_form(FormError::RateLimitExceeded);
211+
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
199212
return render(
200213
locale,
201214
cookie_jar,
@@ -215,6 +228,7 @@ pub(crate) async fn post(
215228
// There is no password for this user, but we don't want to disclose that. Show
216229
// a generic 'invalid credentials' error instead
217230
let form_state = form_state.with_error_on_form(FormError::InvalidCredentials);
231+
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
218232
return render(
219233
locale,
220234
cookie_jar,
@@ -257,6 +271,7 @@ pub(crate) async fn post(
257271
Ok(None) => user_password,
258272
Err(_) => {
259273
let form_state = form_state.with_error_on_form(FormError::InvalidCredentials);
274+
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
260275
return render(
261276
locale,
262277
cookie_jar,
@@ -275,6 +290,7 @@ pub(crate) async fn post(
275290
// Now that we have checked the user password, we now want to show an error if
276291
// the user is locked or deactivated
277292
if user.deactivated_at.is_some() {
293+
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
278294
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
279295
let ctx = AccountInactiveContext::new(user)
280296
.with_csrf(csrf_token.form_value())
@@ -284,6 +300,7 @@ pub(crate) async fn post(
284300
}
285301

286302
if user.locked_at.is_some() {
303+
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
287304
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
288305
let ctx = AccountInactiveContext::new(user)
289306
.with_csrf(csrf_token.form_value())
@@ -309,6 +326,8 @@ pub(crate) async fn post(
309326

310327
repo.save().await?;
311328

329+
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "success")]);
330+
312331
activity_tracker
313332
.record_browser_session(&clock, &user_session)
314333
.await;

0 commit comments

Comments
 (0)