Skip to main content

tuwunel_api_client/account/
change_password.rs

1use axum::extract::State;
2use ruma::{
3	OwnedUserId,
4	api::client::{
5		account::change_password,
6		uiaa::{AuthData, EmailIdentity, ThirdpartyIdCredentials},
7	},
8	thirdparty::Medium,
9};
10use tuwunel_core::{Err, Error, Result, info};
11
12use crate::{ClientIp, Ruma, router::auth_uiaa};
13
14/// # `POST /_matrix/client/r0/account/password`
15///
16/// Changes the password of this account.
17///
18/// - Authenticated changes require UIAA to verify the current user
19/// - Logged-out resets consume a validated email proof and derive the target
20///   from its reverse binding
21/// - Changes the password of the authenticated or proof-bound user
22/// - The password hash is calculated using argon2 with 32 character salt, the
23///   plain password is
24/// not saved
25///
26/// If `logout_devices` is true, authenticated changes apply the following
27/// actions to each device except the sender device. Logged-out resets apply
28/// them to every device:
29/// - Invalidates access token
30/// - Deletes device metadata (device id, device display name, last seen ip,
31///   last seen ts)
32/// - Forgets to-device events
33/// - Triggers device list updates
34#[tracing::instrument(skip_all, fields(%client), name = "change_password")]
35pub(crate) async fn change_password_route(
36	State(services): State<crate::State>,
37	ClientIp(client): ClientIp,
38	body: Ruma<change_password::v3::Request>,
39) -> Result<change_password::v3::Response> {
40	let sender_user = match (body.sender_user.as_ref(), body.auth.as_ref()) {
41		| (None, Some(AuthData::EmailIdentity(EmailIdentity { thirdparty_id_creds, .. }))) =>
42			redeem_password_reset(services, thirdparty_id_creds).await?,
43		| (None, _) => return Err!(Request(MissingToken("Missing access token."))),
44		| (Some(_), _) => auth_uiaa(&services, &body).await?,
45	};
46
47	services
48		.users
49		.set_password_if_active(&sender_user, Some(&body.new_password))
50		.await?;
51
52	if body.logout_devices {
53		// A logged-out reset has no current device to preserve.
54		services
55			.users
56			.remove_devices(&sender_user, body.sender_device.as_deref())
57			.await;
58	}
59
60	info!("User {sender_user} changed their password.");
61
62	services
63		.admin
64		.notify(&format!("User {sender_user} changed their password."))
65		.await;
66
67	Ok(change_password::v3::Response {})
68}
69
70#[tracing::instrument(level = "debug", skip_all)]
71async fn redeem_password_reset(
72	services: crate::State,
73	creds: &ThirdpartyIdCredentials,
74) -> Result<OwnedUserId> {
75	let association = match services
76		.threepid
77		.redeem_validated(creds.sid.as_str(), creds.client_secret.as_str())
78		.await
79	{
80		| Ok(association) => association,
81		| Err(error) if error.is_not_found() || matches!(&error, Error::Request(..)) => {
82			return Err!(Request(Forbidden("Invalid email identity proof.")));
83		},
84		| Err(error) => return Err(error),
85	};
86
87	if association.medium != Medium::Email {
88		return Err!(Request(Forbidden("Invalid email identity proof.")));
89	}
90
91	let Some(user_id) = services
92		.threepid
93		.user_id_for_email(&association.address)
94		.await?
95	else {
96		return Err!(Request(Forbidden("Invalid email identity proof.")));
97	};
98
99	// Deactivation keeps the email binding, and setting a password would
100	// reactivate the account.
101	services.users.deactivated_check(&user_id).await?;
102
103	Ok(user_id)
104}