mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-05-25 12:04:15 +00:00
Add a configuration option to make email optional for password registration
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
let ctx = RegisterStepsEmailInUseContext::new(email_authentication.email, action)
|
||||
.with_language(lang);
|
||||
// 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());
|
||||
}
|
||||
|
||||
return Ok((
|
||||
cookie_jar,
|
||||
Html(templates.render_register_steps_email_in_use(&ctx)?),
|
||||
)
|
||||
.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
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Self>, field: &Value) -> Option<Value> {
|
||||
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<Self>) -> Enumerator {
|
||||
Enumerator::Str(&[
|
||||
"password_registration",
|
||||
"password_registration_email_required",
|
||||
"password_login",
|
||||
"account_recovery",
|
||||
"login_with_email_allowed",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
{% 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 %}
|
||||
|
||||
@@ -35,9 +35,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
<input {{ field.attributes(f) }} class="cpd-text-control" type="text" autocomplete="username" autocorrect="off" autocapitalize="none" required />
|
||||
{% endcall %}
|
||||
|
||||
{% call(f) field.field(label=_("common.email_address"), name="email", form_state=form) %}
|
||||
<input {{ field.attributes(f) }} class="cpd-text-control" type="email" autocomplete="email" required />
|
||||
{% endcall %}
|
||||
{% if features.password_registration_email_required %}
|
||||
{% call(f) field.field(label=_("common.email_address"), name="email", form_state=form) %}
|
||||
<input {{ field.attributes(f) }} class="cpd-text-control" type="email" autocomplete="email" required />
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
||||
{% call(f) field.field(label=_("common.password"), name="password", form_state=form) %}
|
||||
<input {{ field.attributes(f) }} class="cpd-text-control" type="password" autocomplete="new-password" required />
|
||||
|
||||
+12
-8
@@ -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 <a href=\"%s\" data-kind=\"primary\" class=\"cpd-link\">Terms and Conditions</a>",
|
||||
"@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": {
|
||||
|
||||
Reference in New Issue
Block a user