Skip to main content

tuwunel_api/client/account/
change_password.rs

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