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
@@ -1012,6 +1012,7 @@ dependencies = [
|
||||
"hyper",
|
||||
"ipaddress",
|
||||
"itertools 0.14.0",
|
||||
"lettre",
|
||||
"log",
|
||||
"rand 0.10.0",
|
||||
"reqwest",
|
||||
|
||||
@@ -85,6 +85,7 @@ http-body-util.workspace = true
|
||||
hyper.workspace = true
|
||||
ipaddress.workspace = true
|
||||
itertools.workspace = true
|
||||
lettre.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest.workspace = true
|
||||
|
||||
@@ -7,6 +7,7 @@ use conduwuit::{
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use lettre::{Address, message::Mailbox};
|
||||
use ruma::{
|
||||
OwnedRoomId, OwnedUserId, UserId,
|
||||
api::client::{
|
||||
@@ -14,7 +15,7 @@ use ruma::{
|
||||
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
|
||||
deactivate, get_3pids, get_username_availability,
|
||||
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
||||
whoami,
|
||||
request_password_change_token_via_email, whoami,
|
||||
},
|
||||
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 crate::Ruma;
|
||||
@@ -222,6 +223,44 @@ pub(crate) async fn change_password_route(
|
||||
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 `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_all_route)
|
||||
.ruma_route(&client::change_password_route)
|
||||
.ruma_route(&client::password_request_token_route)
|
||||
.ruma_route(&client::deactivate_route)
|
||||
.ruma_route(&client::third_party_route)
|
||||
.ruma_route(&client::request_3pid_management_token_via_email_route)
|
||||
|
||||
@@ -125,7 +125,7 @@ impl Service {
|
||||
|
||||
validation_url
|
||||
.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);
|
||||
|
||||
let message = prepare_body(validation_url.to_string());
|
||||
|
||||
+105
-106
@@ -127,112 +127,6 @@ impl Identity {
|
||||
impl Service {
|
||||
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
|
||||
/// authentication data.
|
||||
pub async fn authenticate(
|
||||
@@ -288,6 +182,111 @@ impl Service {
|
||||
.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.
|
||||
///
|
||||
/// Returns the completed stage's type on success and error information on
|
||||
|
||||
Reference in New Issue
Block a user