Skip to content

Commit b8480b1

Browse files
authored
Support M_USER_LOCKED error for compat sessions (#4789)
2 parents b83c747 + 6183cae commit b8480b1

File tree

1 file changed

+172
-17
lines changed

1 file changed

+172
-17
lines changed

crates/handlers/src/compat/login.rs

Lines changed: 172 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)