mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat: Add support for password resets via email
This commit is contained in:
Generated
+1
@@ -1003,6 +1003,7 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"ipaddress",
|
"ipaddress",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
|
"lettre",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ http-body-util.workspace = true
|
|||||||
hyper.workspace = true
|
hyper.workspace = true
|
||||||
ipaddress.workspace = true
|
ipaddress.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
|
lettre.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use conduwuit::{
|
|||||||
};
|
};
|
||||||
use conduwuit_service::Services;
|
use conduwuit_service::Services;
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
|
use lettre::{Address, message::Mailbox};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedRoomId, OwnedUserId, UserId,
|
OwnedRoomId, OwnedUserId, UserId,
|
||||||
api::client::{
|
api::client::{
|
||||||
@@ -14,7 +15,7 @@ use ruma::{
|
|||||||
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
|
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
|
||||||
deactivate, get_3pids, get_username_availability,
|
deactivate, get_3pids, get_username_availability,
|
||||||
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
||||||
whoami,
|
request_password_change_token_via_email, whoami,
|
||||||
},
|
},
|
||||||
uiaa::{AuthFlow, AuthType},
|
uiaa::{AuthFlow, AuthType},
|
||||||
},
|
},
|
||||||
@@ -26,7 +27,7 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use service::uiaa::Identity;
|
use service::{mailer::messages, uiaa::Identity};
|
||||||
|
|
||||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
@@ -222,6 +223,44 @@ pub(crate) async fn change_password_route(
|
|||||||
Ok(change_password::v3::Response {})
|
Ok(change_password::v3::Response {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/v3/account/password/email/requestToken`
|
||||||
|
///
|
||||||
|
/// Requests a validation email for the purpose of resetting a user's password.
|
||||||
|
pub(crate) async fn password_request_token_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<request_password_change_token_via_email::v3::Request>,
|
||||||
|
) -> Result<request_password_change_token_via_email::v3::Response> {
|
||||||
|
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||||
|
return Err!(Request(InvalidParam("Invalid email address")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(localpart) = services.threepid.get_localpart_for_email(&email).await else {
|
||||||
|
return Err!(Request(ThreepidNotFound(
|
||||||
|
"No account is associated with this email address"
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id =
|
||||||
|
OwnedUserId::parse(format!("@{localpart}:{}", services.globals.server_name())).unwrap();
|
||||||
|
let display_name = services.users.displayname(&user_id).await.ok();
|
||||||
|
|
||||||
|
let session = services
|
||||||
|
.threepid
|
||||||
|
.send_validation_email(
|
||||||
|
Mailbox::new(display_name.clone(), email),
|
||||||
|
|verification_link| messages::PasswordReset {
|
||||||
|
display_name: display_name.as_deref(),
|
||||||
|
user_id: &user_id,
|
||||||
|
verification_link,
|
||||||
|
},
|
||||||
|
&body.client_secret,
|
||||||
|
body.send_attempt.try_into().unwrap(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(request_password_change_token_via_email::v3::Response::new(session))
|
||||||
|
}
|
||||||
|
|
||||||
/// # `GET /_matrix/client/v3/account/whoami`
|
/// # `GET /_matrix/client/v3/account/whoami`
|
||||||
///
|
///
|
||||||
/// Get `user_id` of the sender user.
|
/// Get `user_id` of the sender user.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
|||||||
.ruma_route(&client::logout_route)
|
.ruma_route(&client::logout_route)
|
||||||
.ruma_route(&client::logout_all_route)
|
.ruma_route(&client::logout_all_route)
|
||||||
.ruma_route(&client::change_password_route)
|
.ruma_route(&client::change_password_route)
|
||||||
|
.ruma_route(&client::password_request_token_route)
|
||||||
.ruma_route(&client::deactivate_route)
|
.ruma_route(&client::deactivate_route)
|
||||||
.ruma_route(&client::third_party_route)
|
.ruma_route(&client::third_party_route)
|
||||||
.ruma_route(&client::request_3pid_management_token_via_email_route)
|
.ruma_route(&client::request_3pid_management_token_via_email_route)
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ impl Service {
|
|||||||
|
|
||||||
validation_url
|
validation_url
|
||||||
.query_pairs_mut()
|
.query_pairs_mut()
|
||||||
.append_pair("session_id", session.session_id.as_ref())
|
.append_pair("session", session.session_id.as_ref())
|
||||||
.append_pair("token", &token.token);
|
.append_pair("token", &token.token);
|
||||||
|
|
||||||
let message = prepare_body(validation_url.to_string());
|
let message = prepare_body(validation_url.to_string());
|
||||||
|
|||||||
+105
-106
@@ -127,112 +127,6 @@ impl Identity {
|
|||||||
impl Service {
|
impl Service {
|
||||||
const SESSION_ID_LENGTH: usize = 32;
|
const SESSION_ID_LENGTH: usize = 32;
|
||||||
|
|
||||||
/// Create a new UIAA session with a random session ID.
|
|
||||||
///
|
|
||||||
/// If information about the user's identity is already known, it may be
|
|
||||||
/// supplied with the `identity` parameter. Authentication will fail if
|
|
||||||
/// flows provide different values for known identity information.
|
|
||||||
///
|
|
||||||
/// Returns the info of the newly created session.
|
|
||||||
async fn create_session(
|
|
||||||
&self,
|
|
||||||
flows: Vec<AuthFlow>,
|
|
||||||
params: Box<RawValue>,
|
|
||||||
identity: Option<Identity>,
|
|
||||||
) -> UiaaInfo {
|
|
||||||
let mut uiaa_sessions = self.uiaa_sessions.lock().await;
|
|
||||||
|
|
||||||
let session_id = utils::random_string(Self::SESSION_ID_LENGTH);
|
|
||||||
let mut info = UiaaInfo::new(flows, params);
|
|
||||||
info.session = Some(session_id.clone());
|
|
||||||
|
|
||||||
uiaa_sessions.insert(session_id, UiaaSession {
|
|
||||||
info: info.clone(),
|
|
||||||
identity: identity.unwrap_or_default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
info
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Proceed with UIAA authentication given a client's authorization data.
|
|
||||||
async fn continue_session(&self, auth: &AuthData, session: &str) -> Result<UiaaStatus> {
|
|
||||||
// Hold this lock for the entire function to make sure that, if try_auth()
|
|
||||||
// is called concurrently with the same session, only one call will succeed
|
|
||||||
let mut uiaa_sessions = self.uiaa_sessions.lock().await;
|
|
||||||
|
|
||||||
let Entry::Occupied(mut session) = uiaa_sessions.entry(session.to_owned()) else {
|
|
||||||
return Err!(Request(InvalidParam("Invalid session")));
|
|
||||||
};
|
|
||||||
|
|
||||||
if let &AuthData::FallbackAcknowledgement(_) = auth {
|
|
||||||
// The client is checking if authentication has succeeded out-of-band. This is
|
|
||||||
// possible if the client is using "fallback auth" (see spec section
|
|
||||||
// 4.9.1.4), which we don't support (and probably never will, because it's a
|
|
||||||
// disgusting hack).
|
|
||||||
|
|
||||||
// Return early to tell the client that no, authentication did not succeed while
|
|
||||||
// it wasn't looking.
|
|
||||||
return Ok(UiaaStatus::Retry(session.get().info.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let completed = 'completed: {
|
|
||||||
let UiaaSession { info, identity } = session.get_mut();
|
|
||||||
|
|
||||||
let completed_stages: HashSet<_> = info
|
|
||||||
.completed
|
|
||||||
.iter()
|
|
||||||
.map(AuthType::as_str)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// If the provided stage has already been completed, return early
|
|
||||||
if completed_stages
|
|
||||||
.contains(auth.auth_type().expect("auth type should be set").as_str())
|
|
||||||
{
|
|
||||||
return Ok(UiaaStatus::Retry(session.get().info.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.check_stage(auth, identity.clone()).await {
|
|
||||||
| Ok((completed_stage, updated_identity)) => {
|
|
||||||
info.completed.push(completed_stage);
|
|
||||||
*identity = updated_identity;
|
|
||||||
},
|
|
||||||
| Err(error) => {
|
|
||||||
info.auth_error = Some(error);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check all flows to see if any of them succeeded
|
|
||||||
|
|
||||||
for flow in &info.flows {
|
|
||||||
let flow_stages = flow
|
|
||||||
.stages
|
|
||||||
.iter()
|
|
||||||
.map(AuthType::as_str)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if completed_stages.is_superset(&flow_stages) {
|
|
||||||
// All stages in this flow are completed
|
|
||||||
break 'completed true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No flows had all their stages completed
|
|
||||||
break 'completed false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if completed {
|
|
||||||
// This session is complete, remove it and return success
|
|
||||||
let (_, UiaaSession { identity, .. }) = session.remove_entry();
|
|
||||||
|
|
||||||
Ok(UiaaStatus::Success(identity))
|
|
||||||
} else {
|
|
||||||
// The client needs to try again, return the updated session
|
|
||||||
Ok(UiaaStatus::Retry(session.get().info.clone()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform the full UIAA authentication sequence for a route given its
|
/// Perform the full UIAA authentication sequence for a route given its
|
||||||
/// authentication data.
|
/// authentication data.
|
||||||
pub async fn authenticate(
|
pub async fn authenticate(
|
||||||
@@ -288,6 +182,111 @@ impl Service {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new UIAA session with a random session ID.
|
||||||
|
///
|
||||||
|
/// If information about the user's identity is already known, it may be
|
||||||
|
/// supplied with the `identity` parameter. Authentication will fail if
|
||||||
|
/// flows provide different values for known identity information.
|
||||||
|
///
|
||||||
|
/// Returns the info of the newly created session.
|
||||||
|
async fn create_session(
|
||||||
|
&self,
|
||||||
|
flows: Vec<AuthFlow>,
|
||||||
|
params: Box<RawValue>,
|
||||||
|
identity: Option<Identity>,
|
||||||
|
) -> UiaaInfo {
|
||||||
|
let mut uiaa_sessions = self.uiaa_sessions.lock().await;
|
||||||
|
|
||||||
|
let session_id = utils::random_string(Self::SESSION_ID_LENGTH);
|
||||||
|
let mut info = UiaaInfo::new(flows, params);
|
||||||
|
info.session = Some(session_id.clone());
|
||||||
|
|
||||||
|
uiaa_sessions.insert(session_id, UiaaSession {
|
||||||
|
info: info.clone(),
|
||||||
|
identity: identity.unwrap_or_default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proceed with UIAA authentication given a client's authorization data.
|
||||||
|
async fn continue_session(&self, auth: &AuthData, session: &str) -> Result<UiaaStatus> {
|
||||||
|
// Hold this lock for the entire function to make sure that, if try_auth()
|
||||||
|
// is called concurrently with the same session, only one call will succeed
|
||||||
|
let mut uiaa_sessions = self.uiaa_sessions.lock().await;
|
||||||
|
|
||||||
|
let Entry::Occupied(mut session) = uiaa_sessions.entry(session.to_owned()) else {
|
||||||
|
return Err!(Request(InvalidParam("Invalid session")));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let &AuthData::FallbackAcknowledgement(_) = auth {
|
||||||
|
// The client is checking if authentication has succeeded out-of-band. This is
|
||||||
|
// possible if the client is using "fallback auth" (see spec section
|
||||||
|
// 4.9.1.4), which we don't support (and probably never will, because it's a
|
||||||
|
// disgusting hack).
|
||||||
|
|
||||||
|
// Return early to tell the client that no, authentication did not succeed while
|
||||||
|
// it wasn't looking.
|
||||||
|
return Ok(UiaaStatus::Retry(session.get().info.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let completed = 'completed: {
|
||||||
|
let UiaaSession { info, identity } = session.get_mut();
|
||||||
|
|
||||||
|
let mut completed_stages: HashSet<_> = info
|
||||||
|
.completed
|
||||||
|
.iter()
|
||||||
|
.map(AuthType::as_str)
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// If the provided stage hasn't already been completed, check it for completion
|
||||||
|
if !completed_stages
|
||||||
|
.contains(auth.auth_type().expect("auth type should be set").as_str())
|
||||||
|
{
|
||||||
|
match self.check_stage(auth, identity.clone()).await {
|
||||||
|
| Ok((completed_stage, updated_identity)) => {
|
||||||
|
info.auth_error = None;
|
||||||
|
completed_stages.insert(completed_stage.to_string());
|
||||||
|
info.completed.push(completed_stage);
|
||||||
|
*identity = updated_identity;
|
||||||
|
},
|
||||||
|
| Err(error) => {
|
||||||
|
info.auth_error = Some(error);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all flows to see if any of them succeeded
|
||||||
|
for flow in &info.flows {
|
||||||
|
let flow_stages = flow
|
||||||
|
.stages
|
||||||
|
.iter()
|
||||||
|
.map(AuthType::as_str)
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if completed_stages.is_superset(&flow_stages) {
|
||||||
|
// All stages in this flow are completed
|
||||||
|
break 'completed true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No flows had all their stages completed
|
||||||
|
break 'completed false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if completed {
|
||||||
|
// This session is complete, remove it and return success
|
||||||
|
let (_, UiaaSession { identity, .. }) = session.remove_entry();
|
||||||
|
|
||||||
|
Ok(UiaaStatus::Success(identity))
|
||||||
|
} else {
|
||||||
|
// The client needs to try again, return the updated session
|
||||||
|
Ok(UiaaStatus::Retry(session.get().info.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if the provided authentication data is valid.
|
/// Check if the provided authentication data is valid.
|
||||||
///
|
///
|
||||||
/// Returns the completed stage's type on success and error information on
|
/// Returns the completed stage's type on success and error information on
|
||||||
|
|||||||
Reference in New Issue
Block a user