Normalize the username server-side when registering

`POST /register` trims and lowercases the username before either the password
or the upstream provider path looks at it, since Matrix localparts are
lowercase.
This commit is contained in:
Quentin Gliech
2026-10-13 14:02:32 +02:00
parent 740e9551d8
commit bdecd466bb
2 changed files with 56 additions and 4 deletions
+7 -4
View File
@@ -177,13 +177,17 @@ pub(crate) async fn post(
) -> Result<Response, InternalError> {
let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
let Ok(form) = cookie_jar.verify_form(&clock, form) else {
let Ok(mut form) = cookie_jar.verify_form(&clock, form) else {
// An invalid token most likely comes from a page left open past the CSRF token lifetime
tracing::debug!("Invalid CSRF token on the registration form, redirecting to a fresh one");
let destination = Register::from(query.post_auth_action);
return Ok((cookie_jar, url_builder.redirect(&destination)).into_response());
};
// Matrix localparts are lowercase, so a stray capital or surrounding space
// isn't worth an error
form.username = form.username.trim().to_lowercase();
// Carry the username along so we can prefill it if the user gets to pick
// one when they come back
if let Some(provider_id) = &form.provider {
@@ -201,9 +205,8 @@ pub(crate) async fn post(
};
// The username is carried in a cookie, so its size has to be bounded
let username = form.username.trim();
let username = if username.len() <= MAX_USERNAME_LENGTH {
username
let username = if form.username.len() <= MAX_USERNAME_LENGTH {
form.username.as_str()
} else {
""
};
@@ -364,6 +364,55 @@ mod tests {
},
};
/// The username is trimmed and lowercased before anything checks it
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_register_normalizes_username(pool: PgPool) {
setup();
let state = TestState::from_pool(pool).await.unwrap();
let cookies = CookieHelper::new();
let request = Request::get(&*mas_router::Register::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);
let csrf_token = response
.body()
.split("name=\"csrf\" value=\"")
.nth(1)
.unwrap()
.split('\"')
.next()
.unwrap();
let request = Request::post(&*mas_router::Register::default().path_and_query()).form(
serde_json::json!({
"csrf": csrf_token,
"username": " John ",
"email": "john@example.com",
"password": "correcthorsebatterystaple",
"password_confirm": "correcthorsebatterystaple",
"accept_terms": "on",
}),
);
let request = cookies.with_cookies(request);
let response = state.request(request).await;
response.assert_status(StatusCode::SEE_OTHER);
let location = response.headers().get(LOCATION).unwrap();
let id = location
.to_str()
.unwrap()
.rsplit('/')
.nth(1)
.unwrap()
.parse()
.unwrap();
let mut repo = state.repository().await.unwrap();
let registration = repo.user_registration().lookup(id).await.unwrap().unwrap();
assert_eq!(registration.username, "john");
}
/// Test the registration happy path
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_register(pool: PgPool) {