diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index 959c8ba0f..976a08c32 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -211,6 +211,7 @@ pub fn site_config_from_config( password_login_enabled: password_config.enabled(), password_registration_enabled: password_config.enabled() && account_config.password_registration_enabled, + password_registration_email_required: account_config.password_registration_email_required, registration_token_required: account_config.registration_token_required, email_change_allowed: account_config.email_change_allowed, displayname_change_allowed: account_config.displayname_change_allowed, diff --git a/crates/config/src/sections/account.rs b/crates/config/src/sections/account.rs index 47efa0162..2b6538a2b 100644 --- a/crates/config/src/sections/account.rs +++ b/crates/config/src/sections/account.rs @@ -50,6 +50,13 @@ pub struct AccountConfig { #[serde(default = "default_false", skip_serializing_if = "is_default_false")] pub password_registration_enabled: bool, + /// Whether self-service password registrations require a valid email. + /// Defaults to `true`. + /// + /// This has no effect if password registration is disabled. + #[serde(default = "default_true", skip_serializing_if = "is_default_true")] + pub password_registration_email_required: bool, + /// Whether users are allowed to change their passwords. Defaults to `true`. /// /// This has no effect if password login is disabled. @@ -89,6 +96,7 @@ impl Default for AccountConfig { email_change_allowed: default_true(), displayname_change_allowed: default_true(), password_registration_enabled: default_false(), + password_registration_email_required: default_true(), password_change_allowed: default_true(), password_recovery_enabled: default_false(), account_deactivation_allowed: default_true(), diff --git a/crates/data-model/src/site_config.rs b/crates/data-model/src/site_config.rs index ac0d7e6b8..9622203ad 100644 --- a/crates/data-model/src/site_config.rs +++ b/crates/data-model/src/site_config.rs @@ -64,6 +64,9 @@ pub struct SiteConfig { /// Whether password registration is enabled. pub password_registration_enabled: bool, + /// Whether a valid email address is required for password registrations. + pub password_registration_email_required: bool, + /// Whether registration tokens are required for password registrations. pub registration_token_required: bool, diff --git a/crates/handlers/src/admin/v1/site_config.rs b/crates/handlers/src/admin/v1/site_config.rs index b9b05dac7..40a5db51a 100644 --- a/crates/handlers/src/admin/v1/site_config.rs +++ b/crates/handlers/src/admin/v1/site_config.rs @@ -22,6 +22,9 @@ pub struct SiteConfig { /// Whether password registration is enabled. pub password_registration_enabled: bool, + /// Whether a valid email address is required for password registrations. + pub password_registration_email_required: bool, + /// Whether registration tokens are required for password registrations. pub registration_token_required: bool, @@ -59,6 +62,7 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { server_name: "example.com".to_owned(), password_login_enabled: true, password_registration_enabled: true, + password_registration_email_required: true, registration_token_required: true, email_change_allowed: true, displayname_change_allowed: true, @@ -80,6 +84,7 @@ pub async fn handler( server_name: site_config.server_name, password_login_enabled: site_config.password_login_enabled, password_registration_enabled: site_config.password_registration_enabled, + password_registration_email_required: site_config.password_registration_email_required, registration_token_required: site_config.registration_token_required, email_change_allowed: site_config.email_change_allowed, displayname_change_allowed: site_config.displayname_change_allowed, diff --git a/crates/handlers/src/test_utils.rs b/crates/handlers/src/test_utils.rs index a25fda9dc..fa8105763 100644 --- a/crates/handlers/src/test_utils.rs +++ b/crates/handlers/src/test_utils.rs @@ -140,6 +140,7 @@ pub fn test_site_config() -> SiteConfig { email_change_allowed: true, displayname_change_allowed: true, password_change_allowed: true, + password_registration_email_required: true, account_recovery_allowed: true, account_deactivation_allowed: true, captcha: None, diff --git a/crates/handlers/src/views/register/password.rs b/crates/handlers/src/views/register/password.rs index a9a882f68..0fc8b6a63 100644 --- a/crates/handlers/src/views/register/password.rs +++ b/crates/handlers/src/views/register/password.rs @@ -45,6 +45,7 @@ use crate::{ #[derive(Debug, Deserialize, Serialize)] pub(crate) struct RegisterForm { username: String, + #[serde(default)] email: String, password: String, password_confirm: String, @@ -165,9 +166,16 @@ pub(crate) async fn post( .await .is_ok(); + let state = form.to_form_state(); + + // The email form is only shown if the server requires it + let email = site_config + .password_registration_email_required + .then_some(form.email); + // Validate the form let state = { - let mut state = form.to_form_state(); + let mut state = state; if !passed_captcha { state.add_error_on_form(FormError::Captcha); @@ -195,13 +203,15 @@ pub(crate) async fn post( homeserver_denied_username = true; } - // Note that we don't check here if the email is already taken here, as - // we don't want to leak the information about other users. Instead, we will - // show an error message once the user confirmed their email address. - if form.email.is_empty() { - state.add_error_on_field(RegisterFormField::Email, FieldError::Required); - } else if Address::from_str(&form.email).is_err() { - state.add_error_on_field(RegisterFormField::Email, FieldError::Invalid); + if let Some(email) = &email { + // Note that we don't check here if the email is already taken here, as + // we don't want to leak the information about other users. Instead, we will + // show an error message once the user confirmed their email address. + if email.is_empty() { + state.add_error_on_field(RegisterFormField::Email, FieldError::Required); + } else if Address::from_str(email).is_err() { + state.add_error_on_field(RegisterFormField::Email, FieldError::Invalid); + } } if form.password.is_empty() { @@ -240,7 +250,7 @@ pub(crate) async fn post( .evaluate_register(mas_policy::RegisterInput { registration_method: mas_policy::RegistrationMethod::Password, username: &form.username, - email: Some(&form.email), + email: email.as_deref(), requester: mas_policy::Requester { ip_address: activity_tracker.ip(), user_agent: user_agent.clone(), @@ -295,7 +305,9 @@ pub(crate) async fn post( state.add_error_on_form(FormError::RateLimitExceeded); } - if let Err(e) = limiter.check_email_authentication_email(requester, &form.email) { + if let Some(email) = &email + && let Err(e) = limiter.check_email_authentication_email(requester, email) + { tracing::warn!(error = &e as &dyn std::error::Error); state.add_error_on_form(FormError::RateLimitExceeded); } @@ -343,25 +355,28 @@ pub(crate) async fn post( registration }; - // Create a new user email authentication session - let user_email_authentication = repo - .user_email() - .add_authentication_for_registration(&mut rng, &clock, form.email, ®istration) - .await?; + let registration = if let Some(email) = email { + // Create a new user email authentication session + let user_email_authentication = repo + .user_email() + .add_authentication_for_registration(&mut rng, &clock, email, ®istration) + .await?; - // Schedule a job to verify the email - repo.queue_job() - .schedule_job( - &mut rng, - &clock, - SendEmailAuthenticationCodeJob::new(&user_email_authentication, locale.to_string()), - ) - .await?; + // Schedule a job to verify the email + repo.queue_job() + .schedule_job( + &mut rng, + &clock, + SendEmailAuthenticationCodeJob::new(&user_email_authentication, locale.to_string()), + ) + .await?; - let registration = repo - .user_registration() - .set_email_authentication(registration, &user_email_authentication) - .await?; + repo.user_registration() + .set_email_authentication(registration, &user_email_authentication) + .await? + } else { + registration + }; // Hash the password let password = Zeroizing::new(form.password); @@ -713,4 +728,319 @@ mod tests { response.assert_status(StatusCode::OK); assert!(response.body().contains("This username is already taken")); } + + /// Test registration without email when email is not required + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_register_without_email_when_not_required(pool: PgPool) { + setup(); + let state = TestState::from_pool_with_site_config( + pool, + SiteConfig { + password_registration_email_required: false, + ..test_site_config() + }, + ) + .await + .unwrap(); + let cookies = CookieHelper::new(); + + // Render the registration page and get the CSRF token + let request = + Request::get(&*mas_router::PasswordRegister::default().path_and_query()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + // Extract the CSRF token from the response body + let csrf_token = response + .body() + .split("name=\"csrf\" value=\"") + .nth(1) + .unwrap() + .split('\"') + .next() + .unwrap(); + + // Submit the registration form without email + let request = Request::post(&*mas_router::PasswordRegister::default().path_and_query()) + .form(serde_json::json!({ + "csrf": csrf_token, + "username": "alice", + "password": "correcthorsebatterystaple", + "password_confirm": "correcthorsebatterystaple", + "accept_terms": "on", + })); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::SEE_OTHER); + let location = response.headers().get(LOCATION).unwrap(); + + // The handler redirects with the ID as the second to last portion of the path + let id = location + .to_str() + .unwrap() + .rsplit('/') + .nth(1) + .unwrap() + .parse() + .unwrap(); + + // There should be a new registration in the database + let mut repo = state.repository().await.unwrap(); + let registration = repo.user_registration().lookup(id).await.unwrap().unwrap(); + assert_eq!(registration.username, "alice".to_owned()); + assert!(registration.password.is_some()); + // Email authentication should be None when email is not required and not + // provided + assert!(registration.email_authentication_id.is_none()); + } + + /// Test registration with valid email when email is not required + /// (email input is ignored completely when not required) + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_register_with_email_when_not_required(pool: PgPool) { + setup(); + let state = TestState::from_pool_with_site_config( + pool, + SiteConfig { + password_registration_email_required: false, + ..test_site_config() + }, + ) + .await + .unwrap(); + let cookies = CookieHelper::new(); + + // Render the registration page and get the CSRF token + let request = + Request::get(&*mas_router::PasswordRegister::default().path_and_query()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + // Extract the CSRF token from the response body + let csrf_token = response + .body() + .split("name=\"csrf\" value=\"") + .nth(1) + .unwrap() + .split('\"') + .next() + .unwrap(); + + // Submit the registration form with valid email + let request = Request::post(&*mas_router::PasswordRegister::default().path_and_query()) + .form(serde_json::json!({ + "csrf": csrf_token, + "username": "charlie", + "email": "charlie@example.com", + "password": "correcthorsebatterystaple", + "password_confirm": "correcthorsebatterystaple", + "accept_terms": "on", + })); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::SEE_OTHER); + let location = response.headers().get(LOCATION).unwrap(); + + // The handler redirects with the ID as the second to last portion of the path + let id = location + .to_str() + .unwrap() + .rsplit('/') + .nth(1) + .unwrap() + .parse() + .unwrap(); + + // There should be a new registration in the database + let mut repo = state.repository().await.unwrap(); + let registration = repo.user_registration().lookup(id).await.unwrap().unwrap(); + assert_eq!(registration.username, "charlie".to_owned()); + assert!(registration.password.is_some()); + + // Email authentication should be None when email is not required + // (email input is completely ignored in this case) + assert!(registration.email_authentication_id.is_none()); + } + + /// Test registration fails when email is required but not provided + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_register_fails_without_email_when_required(pool: PgPool) { + setup(); + let state = TestState::from_pool_with_site_config( + pool, + SiteConfig { + password_registration_email_required: true, + ..test_site_config() + }, + ) + .await + .unwrap(); + let cookies = CookieHelper::new(); + + // Render the registration page and get the CSRF token + let request = + Request::get(&*mas_router::PasswordRegister::default().path_and_query()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + // Extract the CSRF token from the response body + let csrf_token = response + .body() + .split("name=\"csrf\" value=\"") + .nth(1) + .unwrap() + .split('\"') + .next() + .unwrap(); + + // Submit the registration form without email + let request = Request::post(&*mas_router::PasswordRegister::default().path_and_query()) + .form(serde_json::json!({ + "csrf": csrf_token, + "username": "david", + "password": "correcthorsebatterystaple", + "password_confirm": "correcthorsebatterystaple", + "accept_terms": "on", + })); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + + // Check that the response contains an error about the email field + let body = response.body(); + assert!(body.contains("email") || body.contains("Email")); + + // Ensure no registration was created + let mut repo = state.repository().await.unwrap(); + let user_exists = repo.user().exists("david").await.unwrap(); + assert!(!user_exists); + } + + /// Test registration fails when email is required but empty + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_register_fails_with_empty_email_when_required(pool: PgPool) { + setup(); + let state = TestState::from_pool_with_site_config( + pool, + SiteConfig { + password_registration_email_required: true, + ..test_site_config() + }, + ) + .await + .unwrap(); + let cookies = CookieHelper::new(); + + // Render the registration page and get the CSRF token + let request = + Request::get(&*mas_router::PasswordRegister::default().path_and_query()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + // Extract the CSRF token from the response body + let csrf_token = response + .body() + .split("name=\"csrf\" value=\"") + .nth(1) + .unwrap() + .split('\"') + .next() + .unwrap(); + + // Submit the registration form with empty email + let request = Request::post(&*mas_router::PasswordRegister::default().path_and_query()) + .form(serde_json::json!({ + "csrf": csrf_token, + "username": "eve", + "email": "", + "password": "correcthorsebatterystaple", + "password_confirm": "correcthorsebatterystaple", + "accept_terms": "on", + })); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + + // Check that the response contains an error about the email field + let body = response.body(); + assert!(body.contains("email") || body.contains("Email")); + + // Ensure no registration was created + let mut repo = state.repository().await.unwrap(); + let user_exists = repo.user().exists("eve").await.unwrap(); + assert!(!user_exists); + } + + /// Test registration fails with invalid email when email is required + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_register_fails_with_invalid_email_when_required(pool: PgPool) { + setup(); + let state = TestState::from_pool_with_site_config( + pool, + SiteConfig { + password_registration_email_required: true, + ..test_site_config() + }, + ) + .await + .unwrap(); + let cookies = CookieHelper::new(); + + // Render the registration page and get the CSRF token + let request = + Request::get(&*mas_router::PasswordRegister::default().path_and_query()).empty(); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + // Extract the CSRF token from the response body + let csrf_token = response + .body() + .split("name=\"csrf\" value=\"") + .nth(1) + .unwrap() + .split('\"') + .next() + .unwrap(); + + // Submit the registration form with invalid email + let request = Request::post(&*mas_router::PasswordRegister::default().path_and_query()) + .form(serde_json::json!({ + "csrf": csrf_token, + "username": "grace", + "email": "not-an-email", + "password": "correcthorsebatterystaple", + "password_confirm": "correcthorsebatterystaple", + "accept_terms": "on", + })); + let request = cookies.with_cookies(request); + let response = state.request(request).await; + cookies.save_cookies(&response); + response.assert_status(StatusCode::OK); + response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); + + // Check that the response contains an error about the email field + let body = response.body(); + assert!(body.contains("email") || body.contains("Email")); + + // Ensure no registration was created + let mut repo = state.repository().await.unwrap(); + let user_exists = repo.user().exists("grace").await.unwrap(); + assert!(!user_exists); + } } diff --git a/crates/handlers/src/views/register/steps/finish.rs b/crates/handlers/src/views/register/steps/finish.rs index de7a537b5..e1ed8a3f0 100644 --- a/crates/handlers/src/views/register/steps/finish.rs +++ b/crates/handlers/src/views/register/steps/finish.rs @@ -151,52 +151,62 @@ pub(crate) async fn get( None }; - // For now, we require an email address on the registration, but this might - // change in the future - let email_authentication_id = registration - .email_authentication_id - .context("No email authentication started for this registration") - .map_err(InternalError::from_anyhow)?; - let email_authentication = repo - .user_email() - .lookup_authentication(email_authentication_id) - .await? - .context("Could not load the email authentication") - .map_err(InternalError::from_anyhow)?; - - // Check that the email authentication has been completed - if email_authentication.completed_at.is_none() { - return Ok(( - cookie_jar, - url_builder.redirect(&mas_router::RegisterVerifyEmail::new(id)), - ) - .into_response()); - } - - // Check that the email address isn't already used - // It is important to do that here, as we we're not checking during the - // registration, because we don't want to disclose whether an email is - // already being used or not before we verified it - if repo - .user_email() - .count(UserEmailFilter::new().for_email(&email_authentication.email)) - .await? - > 0 + // If there is an email authentication, we need to check that the email + // address was verified. If there is no email authentication attached, we + // need to make sure the server doesn't require it + let email_authentication = if let Some(email_authentication_id) = + registration.email_authentication_id { - let action = registration - .post_auth_action - .map(serde_json::from_value) - .transpose()?; + let email_authentication = repo + .user_email() + .lookup_authentication(email_authentication_id) + .await? + .context("Could not load the email authentication") + .map_err(InternalError::from_anyhow)?; + + // Check that the email authentication has been completed + if email_authentication.completed_at.is_none() { + return Ok(( + cookie_jar, + url_builder.redirect(&mas_router::RegisterVerifyEmail::new(id)), + ) + .into_response()); + } - let ctx = RegisterStepsEmailInUseContext::new(email_authentication.email, action) - .with_language(lang); + // Check that the email address isn't already used + // It is important to do that here, as we we're not checking during the + // registration, because we don't want to disclose whether an email is + // already being used or not before we verified it + if repo + .user_email() + .count(UserEmailFilter::new().for_email(&email_authentication.email)) + .await? + > 0 + { + let action = registration + .post_auth_action + .map(serde_json::from_value) + .transpose()?; + + let ctx = RegisterStepsEmailInUseContext::new(email_authentication.email, action) + .with_language(lang); - return Ok(( - cookie_jar, - Html(templates.render_register_steps_email_in_use(&ctx)?), - ) - .into_response()); - } + return Ok(( + cookie_jar, + Html(templates.render_register_steps_email_in_use(&ctx)?), + ) + .into_response()); + } + + Some(email_authentication) + } else if site_config.password_registration_email_required { + // This could only happen in theory during a configuration change + return Err(InternalError::from_anyhow(anyhow::anyhow!( + "Server requires an email address to complete the registration, but no email authentication was attached to the user registration" + ))); + } else { + None + }; // Check that the display name is set if registration.display_name.is_none() { @@ -236,9 +246,11 @@ pub(crate) async fn get( .add(&mut rng, &clock, &user, user_agent) .await?; - repo.user_email() - .add(&mut rng, &clock, &user, email_authentication.email) - .await?; + if let Some(email_authentication) = email_authentication { + repo.user_email() + .add(&mut rng, &clock, &user, email_authentication.email) + .await?; + } if let Some(password) = registration.password { let user_password = repo diff --git a/crates/templates/src/context/ext.rs b/crates/templates/src/context/ext.rs index e4ae3886c..679ad91a7 100644 --- a/crates/templates/src/context/ext.rs +++ b/crates/templates/src/context/ext.rs @@ -45,6 +45,7 @@ impl SiteConfigExt for SiteConfig { fn templates_features(&self) -> SiteFeatures { SiteFeatures { password_registration: self.password_registration_enabled, + password_registration_email_required: self.password_registration_email_required, password_login: self.password_login_enabled, account_recovery: self.account_recovery_allowed, login_with_email_allowed: self.login_with_email_allowed, diff --git a/crates/templates/src/context/features.rs b/crates/templates/src/context/features.rs index d514b5c63..07e80f702 100644 --- a/crates/templates/src/context/features.rs +++ b/crates/templates/src/context/features.rs @@ -18,6 +18,9 @@ pub struct SiteFeatures { /// Whether local password-based registration is enabled. pub password_registration: bool, + /// Whether local password-based registration requires an email address. + pub password_registration_email_required: bool, + /// Whether local password-based login is enabled. pub password_login: bool, @@ -32,6 +35,9 @@ impl Object for SiteFeatures { fn get_value(self: &Arc, field: &Value) -> Option { match field.as_str()? { "password_registration" => Some(Value::from(self.password_registration)), + "password_registration_email_required" => { + Some(Value::from(self.password_registration_email_required)) + } "password_login" => Some(Value::from(self.password_login)), "account_recovery" => Some(Value::from(self.account_recovery)), "login_with_email_allowed" => Some(Value::from(self.login_with_email_allowed)), @@ -42,6 +48,7 @@ impl Object for SiteFeatures { fn enumerate(self: &Arc) -> Enumerator { Enumerator::Str(&[ "password_registration", + "password_registration_email_required", "password_login", "account_recovery", "login_with_email_allowed", diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 1c0bef423..bb208eb44 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -509,6 +509,7 @@ mod tests { let features = SiteFeatures { password_login: true, password_registration: true, + password_registration_email_required: true, account_recovery: true, login_with_email_allowed: true, }; diff --git a/docs/api/spec.json b/docs/api/spec.json index 74e42f734..d0e4c7e54 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -35,6 +35,7 @@ "server_name": "example.com", "password_login_enabled": true, "password_registration_enabled": true, + "password_registration_email_required": true, "registration_token_required": true, "email_change_allowed": true, "displayname_change_allowed": true, @@ -3680,6 +3681,7 @@ "minimum_password_complexity", "password_change_allowed", "password_login_enabled", + "password_registration_email_required", "password_registration_enabled", "registration_token_required", "server_name" @@ -3697,6 +3699,10 @@ "description": "Whether password registration is enabled.", "type": "boolean" }, + "password_registration_email_required": { + "description": "Whether a valid email address is required for password registrations.", + "type": "boolean" + }, "registration_token_required": { "description": "Whether registration tokens are required for password registrations.", "type": "boolean" diff --git a/docs/config.schema.json b/docs/config.schema.json index ada9005c7..524f02c93 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -2604,6 +2604,10 @@ "description": "Whether to enable self-service password registration. Defaults to `false` if password authentication is enabled.\n\nThis has no effect if password login is disabled.", "type": "boolean" }, + "password_registration_email_required": { + "description": "Whether self-service password registrations require a valid email. Defaults to `true`.\n\nThis has no effect if password registration is disabled.", + "type": "boolean" + }, "password_change_allowed": { "description": "Whether users are allowed to change their passwords. Defaults to `true`.\n\nThis has no effect if password login is disabled.", "type": "boolean" diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index b292aa332..c5b69e38f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -296,6 +296,12 @@ account: # This has no effect if password login is disabled. password_registration_enabled: false + # Whether self-service registrations require a valid email + # + # Defaults to `true` + # This has no effect if password registration is disabled. + password_registration_email_required: true + # Whether users are allowed to change their passwords # # Defaults to `true`. @@ -759,7 +765,7 @@ upstream_oauth2: localpart: #action: force #template: "{{ user.preferred_username }}" - + # How to handle when localpart already exists. # Possible values are (default: fail): # - `add` : Adds the upstream account link to the existing user, regardless of whether there is an existing link or not. diff --git a/policies/register/register.rego b/policies/register/register.rego index 34428741a..89002a075 100644 --- a/policies/register/register.rego +++ b/policies/register/register.rego @@ -81,13 +81,6 @@ violation contains {"msg": sprintf( common.requester_banned(input.requester, data.requester) } -# Check that we supplied an email for password registration -violation contains {"field": "email", "msg": "email required for password-based registration"} if { - input.registration_method == "password" - - not input.email -} - # Check if the email is valid using the email policy # and add the email field to the violation object violation contains object.union({"field": "email"}, v) if { diff --git a/policies/register/register_test.rego b/policies/register/register_test.rego index 6cb324b77..a01946bbb 100644 --- a/policies/register/register_test.rego +++ b/policies/register/register_test.rego @@ -39,11 +39,8 @@ test_banned_subdomain if { with data.banned_domains as ["staging.element.io"] } -test_email_required if { - not register.allow with input as {"username": "hello", "registration_method": "password"} -} - test_no_email if { + register.allow with input as {"username": "hello", "registration_method": "password"} register.allow with input as {"username": "hello", "registration_method": "upstream-oauth2"} } diff --git a/templates/pages/register/index.html b/templates/pages/register/index.html index 21c14cdc7..5115b7bbb 100644 --- a/templates/pages/register/index.html +++ b/templates/pages/register/index.html @@ -41,7 +41,11 @@

{{ _("mas.register.create_account.heading") }}

{% endfor %} {% if features.password_registration %} - {{ button.button(text=_("mas.register.continue_with_email")) }} + {% if features.password_registration_email_required %} + {{ button.button(text=_("mas.register.continue_with_email")) }} + {% else %} + {{ button.button(text=_("mas.register.continue_with_password")) }} + {% endif %} {% endif %} {% if providers %} diff --git a/templates/pages/register/password.html b/templates/pages/register/password.html index 5178445de..f6f7a924f 100644 --- a/templates/pages/register/password.html +++ b/templates/pages/register/password.html @@ -35,9 +35,11 @@

{{ _("mas.register.create_account.heading") }}

{% endcall %} - {% call(f) field.field(label=_("common.email_address"), name="email", form_state=form) %} - - {% endcall %} + {% if features.password_registration_email_required %} + {% call(f) field.field(label=_("common.email_address"), name="email", form_state=form) %} + + {% endcall %} + {% endif %} {% call(f) field.field(label=_("common.password"), name="password", form_state=form) %} diff --git a/translations/en.json b/translations/en.json index 0d263fb55..8e800b0e1 100644 --- a/translations/en.json +++ b/translations/en.json @@ -10,7 +10,7 @@ }, "continue": "Continue", "@continue": { - "context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:124:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:74:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/registration_token.html:41:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:37:28-48" + "context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:124:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:76:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/registration_token.html:41:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:37:28-48" }, "create_account": "Create Account", "@create_account": { @@ -79,7 +79,7 @@ }, "email_address": "Email address", "@email_address": { - "context": "pages/recovery/start.html:34:33-58, pages/register/password.html:38:33-58, pages/upstream_oauth2/do_register.html:114:37-62" + "context": "pages/recovery/start.html:34:33-58, pages/register/password.html:39:35-60, pages/upstream_oauth2/do_register.html:114:37-62" }, "loading": "Loading…", "@loading": { @@ -91,11 +91,11 @@ }, "password": "Password", "@password": { - "context": "pages/login.html:56:37-57, pages/reauth.html:28:35-55, pages/register/password.html:42:33-53" + "context": "pages/login.html:56:37-57, pages/reauth.html:28:35-55, pages/register/password.html:44:33-53" }, "password_confirm": "Confirm password", "@password_confirm": { - "context": "pages/register/password.html:46:33-61" + "context": "pages/register/password.html:48:33-61" }, "username": "Username", "@username": { @@ -423,7 +423,7 @@ }, "continue_with_provider": "Continue with %(provider)s", "@continue_with_provider": { - "context": "pages/login.html:81:15-67, pages/register/index.html:53:15-67", + "context": "pages/login.html:81:15-67, pages/register/index.html:57:15-67", "description": "Button to log in with an upstream provider" }, "description": "Please sign in to continue:", @@ -613,12 +613,16 @@ "register": { "call_to_login": "Already have an account?", "@call_to_login": { - "context": "pages/register/index.html:59:35-66, pages/register/password.html:77:33-64", + "context": "pages/register/index.html:63:35-66, pages/register/password.html:79:33-64", "description": "Displayed on the registration page to suggest to log in instead" }, "continue_with_email": "Continue with email address", "@continue_with_email": { - "context": "pages/register/index.html:44:30-67" + "context": "pages/register/index.html:45:32-69" + }, + "continue_with_password": "Continue with password", + "@continue_with_password": { + "context": "pages/register/index.html:47:32-72" }, "create_account": { "description": "Choose a username to continue.", @@ -632,7 +636,7 @@ }, "terms_of_service": "I agree to the Terms and Conditions", "@terms_of_service": { - "context": "pages/register/password.html:51:35-95, pages/upstream_oauth2/do_register.html:179:35-95" + "context": "pages/register/password.html:53:35-95, pages/upstream_oauth2/do_register.html:179:35-95" } }, "registration_token": {