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
99use 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 } ;
3334use rand:: Rng ;
3435use serde:: { Deserialize , Serialize } ;
3536use zeroize:: Zeroizing ;
3637
3738use super :: shared:: OptionalPostAuthAction ;
3839use 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 ) ]
4555pub ( 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