@@ -205,6 +205,9 @@ pub enum RouteError {
205205 #[ error( "invalid login token" ) ]
206206 InvalidLoginToken ,
207207
208+ #[ error( "user is locked" ) ]
209+ UserLocked ,
210+
208211 #[ error( "failed to provision device" ) ]
209212 ProvisionDeviceFailed ( #[ source] anyhow:: Error ) ,
210213}
@@ -263,6 +266,11 @@ impl IntoResponse for RouteError {
263266 error : "Invalid login token" ,
264267 status : StatusCode :: FORBIDDEN ,
265268 } ,
269+ Self :: UserLocked => MatrixError {
270+ errcode : "M_USER_LOCKED" ,
271+ error : "User account has been locked" ,
272+ status : StatusCode :: UNAUTHORIZED ,
273+ } ,
266274 } ;
267275
268276 ( sentry_event_id, response) . into_response ( )
@@ -506,7 +514,15 @@ async fn token_login(
506514 browser_session. id = %browser_session_id,
507515 "Attempt to exchange login token but browser session is not active"
508516 ) ;
509- return Err ( RouteError :: InvalidLoginToken ) ;
517+ return Err (
518+ if browser_session. finished_at . is_some ( )
519+ || browser_session. user . deactivated_at . is_some ( )
520+ {
521+ RouteError :: InvalidLoginToken
522+ } else {
523+ RouteError :: UserLocked
524+ } ,
525+ ) ;
510526 }
511527
512528 // We're about to create a device, let's explicitly acquire a lock, so that
@@ -565,9 +581,13 @@ async fn user_password_login(
565581 . user ( )
566582 . find_by_username ( username)
567583 . await ?
568- . filter ( mas_data_model :: User :: is_valid )
584+ . filter ( |user| user . deactivated_at . is_none ( ) )
569585 . ok_or ( RouteError :: UserNotFound ) ?;
570586
587+ if user. locked_at . is_some ( ) {
588+ return Err ( RouteError :: UserLocked ) ;
589+ }
590+
571591 // Check the rate limit
572592 limiter. check_password ( requester, & user) ?;
573593
@@ -785,7 +805,12 @@ mod tests {
785805 "### ) ;
786806 }
787807
788- async fn user_with_password ( state : & TestState , username : & str , password : & str ) {
808+ async fn user_with_password (
809+ state : & TestState ,
810+ username : & str ,
811+ password : & str ,
812+ locked : bool ,
813+ ) -> User {
789814 let mut rng = state. rng ( ) ;
790815 let mut repo = state. repository ( ) . await . unwrap ( ) ;
791816
@@ -811,7 +836,14 @@ mod tests {
811836 . await
812837 . unwrap ( ) ;
813838
839+ let user = if locked {
840+ repo. user ( ) . lock ( & state. clock , user) . await . unwrap ( )
841+ } else {
842+ user
843+ } ;
844+
814845 repo. save ( ) . await . unwrap ( ) ;
846+ user
815847 }
816848
817849 /// Test that a user can login with a password using the Matrix
@@ -821,7 +853,7 @@ mod tests {
821853 setup ( ) ;
822854 let state = TestState :: from_pool ( pool) . await . unwrap ( ) ;
823855
824- user_with_password ( & state, "alice" , "password" ) . await ;
856+ let user = user_with_password ( & state, "alice" , "password" , true ) . await ;
825857
826858 // Now let's try to login with the password, without asking for a refresh token.
827859 let request = Request :: post ( "/_matrix/client/v3/login" ) . json ( serde_json:: json!( {
@@ -833,14 +865,30 @@ mod tests {
833865 "password" : "password" ,
834866 } ) ) ;
835867
868+ // First try to login to a locked account
869+ let response = state. request ( request. clone ( ) ) . await ;
870+ response. assert_status ( StatusCode :: UNAUTHORIZED ) ;
871+ let body: serde_json:: Value = response. json ( ) ;
872+ insta:: assert_json_snapshot!( body, @r###"
873+ {
874+ "errcode": "M_USER_LOCKED",
875+ "error": "User account has been locked"
876+ }
877+ "### ) ;
878+
879+ // Now try again after unlocking the account
880+ let mut repo = state. repository ( ) . await . unwrap ( ) ;
881+ let user = repo. user ( ) . unlock ( user) . await . unwrap ( ) ;
882+ repo. save ( ) . await . unwrap ( ) ;
883+
836884 let response = state. request ( request) . await ;
837885 response. assert_status ( StatusCode :: OK ) ;
838886
839887 let body: serde_json:: Value = response. json ( ) ;
840888 insta:: assert_json_snapshot!( body, @r###"
841889 {
842- "access_token": "mct_16tugBE5Ta9LIWoSJaAEHHq2g3fx8S_alcBB4 ",
843- "device_id": "ZGpSvYQqlq ",
890+ "access_token": "mct_cxG6gZXyvelQWW9XqfNbm5KAQovodf_XvJz43 ",
891+ "device_id": "42oTpLoieH ",
844892 "user_id": "@alice:example.com"
845893 }
846894 "### ) ;
@@ -862,10 +910,10 @@ mod tests {
862910 let body: serde_json:: Value = response. json ( ) ;
863911 insta:: assert_json_snapshot!( body, @r###"
864912 {
865- "access_token": "mct_cxG6gZXyvelQWW9XqfNbm5KAQovodf_XvJz43 ",
866- "device_id": "42oTpLoieH ",
913+ "access_token": "mct_PGMLvvMXC4Ds1A3lCWc6Hx4l9DGzqG_lVEIV2 ",
914+ "device_id": "Yp7FM44zJN ",
867915 "user_id": "@alice:example.com",
868- "refresh_token": "mcr_7IvDc44woP66fRQoS9MVcHXO9OeBmR_0jDGr1 ",
916+ "refresh_token": "mcr_LoYqtrtBUBcWlE4RX6o47chBCGkadB_9gzpc1 ",
869917 "expires_in_ms": 300000
870918 }
871919 "### ) ;
@@ -883,8 +931,8 @@ mod tests {
883931 let body: serde_json:: Value = response. json ( ) ;
884932 insta:: assert_json_snapshot!( body, @r###"
885933 {
886- "access_token": "mct_PGMLvvMXC4Ds1A3lCWc6Hx4l9DGzqG_lVEIV2 ",
887- "device_id": "Yp7FM44zJN ",
934+ "access_token": "mct_Xl3bbpfh9yNy9NzuRxyR3b3PLW0rqd_DiXAH2 ",
935+ "device_id": "6cq7FqNSYo ",
888936 "user_id": "@alice:example.com"
889937 }
890938 "### ) ;
@@ -930,6 +978,45 @@ mod tests {
930978 // The response should be the same as the previous one, so that we don't leak if
931979 // it's the user that is invalid or the password.
932980 assert_eq ! ( body, old_body) ;
981+
982+ // Try to login to a deactivated account
983+ let mut repo = state. repository ( ) . await . unwrap ( ) ;
984+ let user = repo. user ( ) . deactivate ( & state. clock , user) . await . unwrap ( ) ;
985+ repo. save ( ) . await . unwrap ( ) ;
986+
987+ let request = Request :: post ( "/_matrix/client/v3/login" ) . json ( serde_json:: json!( {
988+ "type" : "m.login.password" ,
989+ "identifier" : {
990+ "type" : "m.id.user" ,
991+ "user" : "alice" ,
992+ } ,
993+ "password" : "password" ,
994+ } ) ) ;
995+
996+ let response = state. request ( request. clone ( ) ) . await ;
997+ response. assert_status ( StatusCode :: FORBIDDEN ) ;
998+ let body: serde_json:: Value = response. json ( ) ;
999+ insta:: assert_json_snapshot!( body, @r###"
1000+ {
1001+ "errcode": "M_FORBIDDEN",
1002+ "error": "Invalid username/password"
1003+ }
1004+ "### ) ;
1005+
1006+ // Should get the same error if the deactivated user is also locked
1007+ let mut repo = state. repository ( ) . await . unwrap ( ) ;
1008+ let _user = repo. user ( ) . lock ( & state. clock , user) . await . unwrap ( ) ;
1009+ repo. save ( ) . await . unwrap ( ) ;
1010+
1011+ let response = state. request ( request) . await ;
1012+ response. assert_status ( StatusCode :: FORBIDDEN ) ;
1013+ let body: serde_json:: Value = response. json ( ) ;
1014+ insta:: assert_json_snapshot!( body, @r###"
1015+ {
1016+ "errcode": "M_FORBIDDEN",
1017+ "error": "Invalid username/password"
1018+ }
1019+ "### ) ;
9331020 }
9341021
9351022 /// Test that we can send a login request without a Content-Type header
@@ -938,7 +1025,7 @@ mod tests {
9381025 setup ( ) ;
9391026 let state = TestState :: from_pool ( pool) . await . unwrap ( ) ;
9401027
941- user_with_password ( & state, "alice" , "password" ) . await ;
1028+ user_with_password ( & state, "alice" , "password" , false ) . await ;
9421029 // Try without a Content-Type header
9431030 let mut request = Request :: post ( "/_matrix/client/v3/login" ) . json ( serde_json:: json!( {
9441031 "type" : "m.login.password" ,
@@ -970,7 +1057,7 @@ mod tests {
9701057 setup ( ) ;
9711058 let state = TestState :: from_pool ( pool) . await . unwrap ( ) ;
9721059
973- user_with_password ( & state, "alice" , "password" ) . await ;
1060+ let user = user_with_password ( & state, "alice" , "password" , true ) . await ;
9741061
9751062 // Login with a full MXID as identifier
9761063 let request = Request :: post ( "/_matrix/client/v3/login" ) . json ( serde_json:: json!( {
@@ -982,13 +1069,29 @@ mod tests {
9821069 "password" : "password" ,
9831070 } ) ) ;
9841071
1072+ // First try to login to a locked account
1073+ let response = state. request ( request. clone ( ) ) . await ;
1074+ response. assert_status ( StatusCode :: UNAUTHORIZED ) ;
1075+ let body: serde_json:: Value = response. json ( ) ;
1076+ insta:: assert_json_snapshot!( body, @r###"
1077+ {
1078+ "errcode": "M_USER_LOCKED",
1079+ "error": "User account has been locked"
1080+ }
1081+ "### ) ;
1082+
1083+ // Now try again after unlocking the account
1084+ let mut repo = state. repository ( ) . await . unwrap ( ) ;
1085+ let _ = repo. user ( ) . unlock ( user) . await . unwrap ( ) ;
1086+ repo. save ( ) . await . unwrap ( ) ;
1087+
9851088 let response = state. request ( request) . await ;
9861089 response. assert_status ( StatusCode :: OK ) ;
9871090 let body: serde_json:: Value = response. json ( ) ;
9881091 insta:: assert_json_snapshot!( body, @r###"
9891092 {
990- "access_token": "mct_16tugBE5Ta9LIWoSJaAEHHq2g3fx8S_alcBB4 ",
991- "device_id": "ZGpSvYQqlq ",
1093+ "access_token": "mct_cxG6gZXyvelQWW9XqfNbm5KAQovodf_XvJz43 ",
1094+ "device_id": "42oTpLoieH ",
9921095 "user_id": "@alice:example.com"
9931096 }
9941097 "### ) ;
@@ -1132,6 +1235,8 @@ mod tests {
11321235 . add ( & mut state. rng ( ) , & state. clock , "alice" . to_owned ( ) )
11331236 . await
11341237 . unwrap ( ) ;
1238+ // Start with a locked account
1239+ let user = repo. user ( ) . lock ( & state. clock , user) . await . unwrap ( ) ;
11351240 repo. save ( ) . await . unwrap ( ) ;
11361241
11371242 let mxid = state. homeserver_connection . mxid ( & user. username ) ;
@@ -1164,14 +1269,29 @@ mod tests {
11641269 "type" : "m.login.token" ,
11651270 "token" : token,
11661271 } ) ) ;
1272+ let response = state. request ( request. clone ( ) ) . await ;
1273+ response. assert_status ( StatusCode :: UNAUTHORIZED ) ;
1274+ let body: serde_json:: Value = response. json ( ) ;
1275+ insta:: assert_json_snapshot!( body, @r###"
1276+ {
1277+ "errcode": "M_USER_LOCKED",
1278+ "error": "User account has been locked"
1279+ }
1280+ "### ) ;
1281+
1282+ // Now try again after unlocking the account
1283+ let mut repo = state. repository ( ) . await . unwrap ( ) ;
1284+ let user = repo. user ( ) . unlock ( user) . await . unwrap ( ) ;
1285+ repo. save ( ) . await . unwrap ( ) ;
1286+
11671287 let response = state. request ( request) . await ;
11681288 response. assert_status ( StatusCode :: OK ) ;
11691289
11701290 let body: serde_json:: Value = response. json ( ) ;
11711291 insta:: assert_json_snapshot!( body, @r#"
11721292 {
1173- "access_token": "mct_bnkWh1tPmm1MZOpygPaXwygX8PfxEY_hE6do1 ",
1174- "device_id": "O3Ju1MUh3Z ",
1293+ "access_token": "mct_bUTa4XIh92RARTPTjqQrCZLAkq2ild_0VsYE6 ",
1294+ "device_id": "uihy4bk51g ",
11751295 "user_id": "@alice:example.com"
11761296 }
11771297 "# ) ;
@@ -1212,6 +1332,41 @@ mod tests {
12121332 "error": "Login token expired"
12131333 }
12141334 "### ) ;
1335+
1336+ // Try to login to a deactivated account
1337+ let token = get_login_token ( & state, & user) . await ;
1338+
1339+ let mut repo = state. repository ( ) . await . unwrap ( ) ;
1340+ let user = repo. user ( ) . deactivate ( & state. clock , user) . await . unwrap ( ) ;
1341+ repo. save ( ) . await . unwrap ( ) ;
1342+ let request = Request :: post ( "/_matrix/client/v3/login" ) . json ( serde_json:: json!( {
1343+ "type" : "m.login.token" ,
1344+ "token" : token,
1345+ } ) ) ;
1346+ let response = state. request ( request. clone ( ) ) . await ;
1347+ response. assert_status ( StatusCode :: FORBIDDEN ) ;
1348+ let body: serde_json:: Value = response. json ( ) ;
1349+ insta:: assert_json_snapshot!( body, @r###"
1350+ {
1351+ "errcode": "M_FORBIDDEN",
1352+ "error": "Invalid login token"
1353+ }
1354+ "### ) ;
1355+
1356+ // Should get the same error if the deactivated user is also locked
1357+ let mut repo = state. repository ( ) . await . unwrap ( ) ;
1358+ let _user = repo. user ( ) . lock ( & state. clock , user) . await . unwrap ( ) ;
1359+ repo. save ( ) . await . unwrap ( ) ;
1360+
1361+ let response = state. request ( request) . await ;
1362+ response. assert_status ( StatusCode :: FORBIDDEN ) ;
1363+ let body: serde_json:: Value = response. json ( ) ;
1364+ insta:: assert_json_snapshot!( body, @r###"
1365+ {
1366+ "errcode": "M_FORBIDDEN",
1367+ "error": "Invalid login token"
1368+ }
1369+ "### ) ;
12151370 }
12161371
12171372 /// Get a login token for a user.
0 commit comments