diff --git a/Cargo.lock b/Cargo.lock index 18f6e791a..efc964207 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3104,7 +3104,7 @@ dependencies = [ [[package]] name = "mas-axum-utils" -version = "1.4.0" +version = "1.4.1" dependencies = [ "anyhow", "axum", @@ -3138,7 +3138,7 @@ dependencies = [ [[package]] name = "mas-cli" -version = "1.4.0" +version = "1.4.1" dependencies = [ "anyhow", "axum", @@ -3211,7 +3211,7 @@ dependencies = [ [[package]] name = "mas-config" -version = "1.4.0" +version = "1.4.1" dependencies = [ "anyhow", "camino", @@ -3243,7 +3243,7 @@ dependencies = [ [[package]] name = "mas-context" -version = "1.4.0" +version = "1.4.1" dependencies = [ "console", "opentelemetry", @@ -3259,7 +3259,7 @@ dependencies = [ [[package]] name = "mas-data-model" -version = "1.4.0" +version = "1.4.1" dependencies = [ "base64ct", "chrono", @@ -3282,7 +3282,7 @@ dependencies = [ [[package]] name = "mas-email" -version = "1.4.0" +version = "1.4.1" dependencies = [ "async-trait", "lettre", @@ -3293,7 +3293,7 @@ dependencies = [ [[package]] name = "mas-handlers" -version = "1.4.0" +version = "1.4.1" dependencies = [ "aide", "anyhow", @@ -3373,7 +3373,7 @@ dependencies = [ [[package]] name = "mas-http" -version = "1.4.0" +version = "1.4.1" dependencies = [ "futures-util", "headers", @@ -3394,7 +3394,7 @@ dependencies = [ [[package]] name = "mas-i18n" -version = "1.4.0" +version = "1.4.1" dependencies = [ "camino", "icu_calendar", @@ -3416,7 +3416,7 @@ dependencies = [ [[package]] name = "mas-i18n-scan" -version = "1.4.0" +version = "1.4.1" dependencies = [ "camino", "clap", @@ -3430,7 +3430,7 @@ dependencies = [ [[package]] name = "mas-iana" -version = "1.4.0" +version = "1.4.1" dependencies = [ "schemars 0.8.22", "serde", @@ -3438,7 +3438,7 @@ dependencies = [ [[package]] name = "mas-iana-codegen" -version = "1.4.0" +version = "1.4.1" dependencies = [ "anyhow", "async-trait", @@ -3454,7 +3454,7 @@ dependencies = [ [[package]] name = "mas-jose" -version = "1.4.0" +version = "1.4.1" dependencies = [ "base64ct", "chrono", @@ -3484,7 +3484,7 @@ dependencies = [ [[package]] name = "mas-keystore" -version = "1.4.0" +version = "1.4.1" dependencies = [ "aead", "base64ct", @@ -3512,7 +3512,7 @@ dependencies = [ [[package]] name = "mas-listener" -version = "1.4.0" +version = "1.4.1" dependencies = [ "anyhow", "bytes", @@ -3537,7 +3537,7 @@ dependencies = [ [[package]] name = "mas-matrix" -version = "1.4.0" +version = "1.4.1" dependencies = [ "anyhow", "async-trait", @@ -3547,7 +3547,7 @@ dependencies = [ [[package]] name = "mas-matrix-synapse" -version = "1.4.0" +version = "1.4.1" dependencies = [ "anyhow", "async-trait", @@ -3564,7 +3564,7 @@ dependencies = [ [[package]] name = "mas-oidc-client" -version = "1.4.0" +version = "1.4.1" dependencies = [ "assert_matches", "async-trait", @@ -3600,7 +3600,7 @@ dependencies = [ [[package]] name = "mas-policy" -version = "1.4.0" +version = "1.4.1" dependencies = [ "anyhow", "arc-swap", @@ -3617,7 +3617,7 @@ dependencies = [ [[package]] name = "mas-router" -version = "1.4.0" +version = "1.4.1" dependencies = [ "axum", "serde", @@ -3628,7 +3628,7 @@ dependencies = [ [[package]] name = "mas-spa" -version = "1.4.0" +version = "1.4.1" dependencies = [ "camino", "serde", @@ -3637,7 +3637,7 @@ dependencies = [ [[package]] name = "mas-storage" -version = "1.4.0" +version = "1.4.1" dependencies = [ "async-trait", "chrono", @@ -3659,7 +3659,7 @@ dependencies = [ [[package]] name = "mas-storage-pg" -version = "1.4.0" +version = "1.4.1" dependencies = [ "async-trait", "chrono", @@ -3686,7 +3686,7 @@ dependencies = [ [[package]] name = "mas-tasks" -version = "1.4.0" +version = "1.4.1" dependencies = [ "anyhow", "async-trait", @@ -3718,7 +3718,7 @@ dependencies = [ [[package]] name = "mas-templates" -version = "1.4.0" +version = "1.4.1" dependencies = [ "anyhow", "arc-swap", @@ -3748,7 +3748,7 @@ dependencies = [ [[package]] name = "mas-tower" -version = "1.4.0" +version = "1.4.1" dependencies = [ "http", "opentelemetry", @@ -4018,7 +4018,7 @@ dependencies = [ [[package]] name = "oauth2-types" -version = "1.4.0" +version = "1.4.1" dependencies = [ "assert_matches", "base64ct", @@ -6100,7 +6100,7 @@ dependencies = [ [[package]] name = "syn2mas" -version = "1.4.0" +version = "1.4.1" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index dbfc35c4a..0fa145f53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = ["crates/*"] resolver = "2" # Updated in the CI with a `sed` command -package.version = "1.4.0" +package.version = "1.4.1" package.license = "AGPL-3.0-only OR LicenseRef-Element-Commercial" package.authors = ["Element Backend Team"] package.edition = "2024" @@ -34,35 +34,35 @@ broken_intra_doc_links = "deny" [workspace.dependencies] # Workspace crates -mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.4.0" } -mas-cli = { path = "./crates/cli/", version = "=1.4.0" } -mas-config = { path = "./crates/config/", version = "=1.4.0" } -mas-context = { path = "./crates/context/", version = "=1.4.0" } -mas-data-model = { path = "./crates/data-model/", version = "=1.4.0" } -mas-email = { path = "./crates/email/", version = "=1.4.0" } -mas-graphql = { path = "./crates/graphql/", version = "=1.4.0" } -mas-handlers = { path = "./crates/handlers/", version = "=1.4.0" } -mas-http = { path = "./crates/http/", version = "=1.4.0" } -mas-i18n = { path = "./crates/i18n/", version = "=1.4.0" } -mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.4.0" } -mas-iana = { path = "./crates/iana/", version = "=1.4.0" } -mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.4.0" } -mas-jose = { path = "./crates/jose/", version = "=1.4.0" } -mas-keystore = { path = "./crates/keystore/", version = "=1.4.0" } -mas-listener = { path = "./crates/listener/", version = "=1.4.0" } -mas-matrix = { path = "./crates/matrix/", version = "=1.4.0" } -mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.4.0" } -mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.4.0" } -mas-policy = { path = "./crates/policy/", version = "=1.4.0" } -mas-router = { path = "./crates/router/", version = "=1.4.0" } -mas-spa = { path = "./crates/spa/", version = "=1.4.0" } -mas-storage = { path = "./crates/storage/", version = "=1.4.0" } -mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.4.0" } -mas-tasks = { path = "./crates/tasks/", version = "=1.4.0" } -mas-templates = { path = "./crates/templates/", version = "=1.4.0" } -mas-tower = { path = "./crates/tower/", version = "=1.4.0" } -oauth2-types = { path = "./crates/oauth2-types/", version = "=1.4.0" } -syn2mas = { path = "./crates/syn2mas", version = "=1.4.0" } +mas-axum-utils = { path = "./crates/axum-utils/", version = "=1.4.1" } +mas-cli = { path = "./crates/cli/", version = "=1.4.1" } +mas-config = { path = "./crates/config/", version = "=1.4.1" } +mas-context = { path = "./crates/context/", version = "=1.4.1" } +mas-data-model = { path = "./crates/data-model/", version = "=1.4.1" } +mas-email = { path = "./crates/email/", version = "=1.4.1" } +mas-graphql = { path = "./crates/graphql/", version = "=1.4.1" } +mas-handlers = { path = "./crates/handlers/", version = "=1.4.1" } +mas-http = { path = "./crates/http/", version = "=1.4.1" } +mas-i18n = { path = "./crates/i18n/", version = "=1.4.1" } +mas-i18n-scan = { path = "./crates/i18n-scan/", version = "=1.4.1" } +mas-iana = { path = "./crates/iana/", version = "=1.4.1" } +mas-iana-codegen = { path = "./crates/iana-codegen/", version = "=1.4.1" } +mas-jose = { path = "./crates/jose/", version = "=1.4.1" } +mas-keystore = { path = "./crates/keystore/", version = "=1.4.1" } +mas-listener = { path = "./crates/listener/", version = "=1.4.1" } +mas-matrix = { path = "./crates/matrix/", version = "=1.4.1" } +mas-matrix-synapse = { path = "./crates/matrix-synapse/", version = "=1.4.1" } +mas-oidc-client = { path = "./crates/oidc-client/", version = "=1.4.1" } +mas-policy = { path = "./crates/policy/", version = "=1.4.1" } +mas-router = { path = "./crates/router/", version = "=1.4.1" } +mas-spa = { path = "./crates/spa/", version = "=1.4.1" } +mas-storage = { path = "./crates/storage/", version = "=1.4.1" } +mas-storage-pg = { path = "./crates/storage-pg/", version = "=1.4.1" } +mas-tasks = { path = "./crates/tasks/", version = "=1.4.1" } +mas-templates = { path = "./crates/templates/", version = "=1.4.1" } +mas-tower = { path = "./crates/tower/", version = "=1.4.1" } +oauth2-types = { path = "./crates/oauth2-types/", version = "=1.4.1" } +syn2mas = { path = "./crates/syn2mas", version = "=1.4.1" } # OpenAPI schema generation and validation [workspace.dependencies.aide] diff --git a/crates/handlers/src/graphql/mutations/mod.rs b/crates/handlers/src/graphql/mutations/mod.rs index af6caab62..a84bf9210 100644 --- a/crates/handlers/src/graphql/mutations/mod.rs +++ b/crates/handlers/src/graphql/mutations/mod.rs @@ -84,7 +84,7 @@ async fn verify_password_if_needed( password, user_password.hashed_password, ) - .await; + .await?; - Ok(res.is_ok()) + Ok(res.is_success()) } diff --git a/crates/handlers/src/graphql/mutations/user.rs b/crates/handlers/src/graphql/mutations/user.rs index f9f5696e7..355c7d0ac 100644 --- a/crates/handlers/src/graphql/mutations/user.rs +++ b/crates/handlers/src/graphql/mutations/user.rs @@ -737,13 +737,14 @@ impl UserMutations { )); }; - if let Err(_err) = password_manager + if !password_manager .verify( active_password.version, Zeroizing::new(current_password_attempt), active_password.hashed_password, ) - .await + .await? + .is_success() { return Ok(SetPasswordPayload { status: SetPasswordStatus::WrongPassword, diff --git a/crates/handlers/src/graphql/tests.rs b/crates/handlers/src/graphql/tests.rs index 328b6f152..888d477d0 100644 --- a/crates/handlers/src/graphql/tests.rs +++ b/crates/handlers/src/graphql/tests.rs @@ -6,6 +6,7 @@ use axum::http::Request; use hyper::StatusCode; +use mas_axum_utils::SessionInfoExt; use mas_data_model::{AccessToken, Client, TokenType, User}; use mas_matrix::{HomeserverConnection, ProvisionRequest}; use mas_router::SimpleRoute; @@ -19,11 +20,9 @@ use oauth2_types::{ scope::{OPENID, Scope, ScopeToken}, }; use sqlx::PgPool; +use zeroize::Zeroizing; -use crate::{ - test_utils, - test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}, -}; +use crate::test_utils::{self, CookieHelper, RequestBuilderExt, ResponseExt, TestState, setup}; async fn create_test_client(state: &TestState) -> Client { let mut repo = state.repository().await.unwrap(); @@ -781,3 +780,301 @@ async fn test_add_user(pool: PgPool) { }) ); } + +/// Test the setPassword mutation where the current password provided is +/// wrong. +#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] +async fn test_set_password_rejected_wrong_password(pool: PgPool) { + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + + let mut rng = state.rng(); + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + let password = Zeroizing::new("current.password.123".to_owned()); + let (version, hashed_password) = state + .password_manager + .hash(&mut rng, password) + .await + .unwrap(); + + repo.user_password() + .add( + &mut rng, + &state.clock, + &user, + version, + hashed_password, + None, + ) + .await + .unwrap(); + let browser_session = repo + .browser_session() + .add(&mut rng, &state.clock, &user, None) + .await + .unwrap(); + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let cookie_jar = cookie_jar.set_session(&browser_session); + + let user_id = user.id; + + let request = Request::post("/graphql").json(serde_json::json!({ + "query": format!(r#" + mutation {{ + setPassword(input: {{ + userId: "user:{user_id}", + currentPassword: "wrong.password.123", + newPassword: "new.password.123" + }}) {{ + status + }} + }} + "#), + })); + + let cookies = CookieHelper::new(); + cookies.import(cookie_jar); + let request = cookies.with_cookies(request); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: GraphQLResponse = response.json(); + assert!(response.errors.is_empty(), "{:?}", response.errors); + assert_eq!( + response.data["setPassword"]["status"].as_str(), + Some("WRONG_PASSWORD"), + "{:?}", + response.data + ); +} + +/// Test the startEmailAuthentication mutation where the current password +/// provided is invalid. +#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] +async fn test_start_email_authentication_rejected_wrong_password(pool: PgPool) { + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + + let mut rng = state.rng(); + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + let password = Zeroizing::new("current.password.123".to_owned()); + let (version, hashed_password) = state + .password_manager + .hash(&mut rng, password) + .await + .unwrap(); + + repo.user_password() + .add( + &mut rng, + &state.clock, + &user, + version, + hashed_password, + None, + ) + .await + .unwrap(); + let browser_session = repo + .browser_session() + .add(&mut rng, &state.clock, &user, None) + .await + .unwrap(); + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let cookie_jar = cookie_jar.set_session(&browser_session); + + let request = Request::post("/graphql").json(serde_json::json!({ + "query": r#" + mutation { + startEmailAuthentication(input: { + email: "alice@example.org", + password: "wrong.password.123" + }) { + status + } + } + "#, + })); + + let cookies = CookieHelper::new(); + cookies.import(cookie_jar); + let request = cookies.with_cookies(request); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: GraphQLResponse = response.json(); + assert!(response.errors.is_empty(), "{:?}", response.errors); + assert_eq!( + response.data["startEmailAuthentication"]["status"].as_str(), + Some("INCORRECT_PASSWORD"), + "{:?}", + response.data + ); +} + +/// Test the removeEmail mutation where the current password +/// provided is invalid. +#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] +async fn test_remove_email_rejected_wrong_password(pool: PgPool) { + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + + let mut rng = state.rng(); + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + let password = Zeroizing::new("current.password.123".to_owned()); + let (version, hashed_password) = state + .password_manager + .hash(&mut rng, password) + .await + .unwrap(); + + repo.user_password() + .add( + &mut rng, + &state.clock, + &user, + version, + hashed_password, + None, + ) + .await + .unwrap(); + let user_email_id = repo + .user_email() + .add( + &mut rng, + &state.clock, + &user, + "alice@example.org".to_owned(), + ) + .await + .unwrap() + .id; + let browser_session = repo + .browser_session() + .add(&mut rng, &state.clock, &user, None) + .await + .unwrap(); + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let cookie_jar = cookie_jar.set_session(&browser_session); + + let request = Request::post("/graphql").json(serde_json::json!({ + "query": format!(r#" + mutation {{ + removeEmail(input: {{ + userEmailId: "user_email:{user_email_id}", + password: "wrong.password.123" + }}) {{ + status + }} + }} + "#), + })); + + let cookies = CookieHelper::new(); + cookies.import(cookie_jar); + let request = cookies.with_cookies(request); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: GraphQLResponse = response.json(); + assert!(response.errors.is_empty(), "{:?}", response.errors); + assert_eq!( + response.data["removeEmail"]["status"].as_str(), + Some("INCORRECT_PASSWORD"), + "{:?}", + response.data + ); +} + +/// Test the deactivateUser mutation where the current password +/// provided is invalid. +#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] +async fn test_deactivate_user_rejected_wrong_password(pool: PgPool) { + setup(); + let state = TestState::from_pool(pool).await.unwrap(); + + let mut rng = state.rng(); + let mut repo = state.repository().await.unwrap(); + let user = repo + .user() + .add(&mut rng, &state.clock, "alice".to_owned()) + .await + .unwrap(); + let password = Zeroizing::new("current.password.123".to_owned()); + let (version, hashed_password) = state + .password_manager + .hash(&mut rng, password) + .await + .unwrap(); + + repo.user_password() + .add( + &mut rng, + &state.clock, + &user, + version, + hashed_password, + None, + ) + .await + .unwrap(); + let browser_session = repo + .browser_session() + .add(&mut rng, &state.clock, &user, None) + .await + .unwrap(); + repo.save().await.unwrap(); + + let cookie_jar = state.cookie_jar(); + let cookie_jar = cookie_jar.set_session(&browser_session); + + let request = Request::post("/graphql").json(serde_json::json!({ + "query": r#" + mutation { + deactivateUser(input: { + hsErase: true, + password: "wrong.password.123" + }) { + status + } + } + "#, + })); + + let cookies = CookieHelper::new(); + cookies.import(cookie_jar); + let request = cookies.with_cookies(request); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let response: GraphQLResponse = response.json(); + assert!(response.errors.is_empty(), "{:?}", response.errors); + assert_eq!( + response.data["deactivateUser"]["status"].as_str(), + Some("INCORRECT_PASSWORD"), + "{:?}", + response.data + ); +} diff --git a/crates/handlers/src/passwords.rs b/crates/handlers/src/passwords.rs index 6f32f77f9..6071cf730 100644 --- a/crates/handlers/src/passwords.rs +++ b/crates/handlers/src/passwords.rs @@ -49,6 +49,11 @@ impl PasswordVerificationResult { Self::Failure => PasswordVerificationResult::Failure, } } + + #[must_use] + pub fn is_success(&self) -> bool { + matches!(self, Self::Success(_)) + } } impl From for PasswordVerificationResult<()> {