feat: Add a new service for handling password resets

This commit is contained in:
Ginger 2026-03-03 10:36:25 -05:00
parent 3d50af0943
commit 267feb3c09
No known key found for this signature in database
5 changed files with 179 additions and 2 deletions

View file

@ -112,6 +112,10 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "onetimekeyid_onetimekeys", name: "onetimekeyid_onetimekeys",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL
}, },
Descriptor {
name: "passwordresettoken_userid",
..descriptor::RANDOM_SMALL
},
Descriptor { Descriptor {
name: "pduid_pdu", name: "pduid_pdu",
cache_disp: CacheDisp::SharedWith("eventid_outlierpdu"), cache_disp: CacheDisp::SharedWith("eventid_outlierpdu"),

View file

@ -23,6 +23,7 @@ pub mod globals;
pub mod key_backups; pub mod key_backups;
pub mod media; pub mod media;
pub mod moderation; pub mod moderation;
pub mod password_reset;
pub mod presence; pub mod presence;
pub mod pusher; pub mod pusher;
pub mod registration_tokens; pub mod registration_tokens;

View file

@ -0,0 +1,69 @@
use std::{
sync::Arc,
time::{Duration, SystemTime},
};
use conduwuit::utils::{ReadyExt, stream::TryExpect};
use database::{Database, Deserialized, Json, Map};
use ruma::{OwnedUserId, UserId};
use serde::{Deserialize, Serialize};
pub(super) struct Data {
passwordresettoken_info: Arc<Map>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResetTokenInfo {
pub user: OwnedUserId,
pub issued_at: SystemTime,
}
impl ResetTokenInfo {
const MAX_TOKEN_AGE: Duration = Duration::from_secs(60 * 60);
// one hour
pub fn is_valid(&self) -> bool {
let now = SystemTime::now();
now.duration_since(self.issued_at)
.is_ok_and(|duration| duration < Self::MAX_TOKEN_AGE)
}
}
impl Data {
pub(super) fn new(db: &Arc<Database>) -> Self {
Self {
passwordresettoken_info: db["passwordresettoken_info"].clone(),
}
}
/// Associate a reset token with its info in the database.
pub(super) fn save_token(&self, token: &str, info: &ResetTokenInfo) {
self.passwordresettoken_info.raw_put(token, Json(info));
}
/// Lookup the info for a reset token.
pub(super) async fn lookup_token_info(&self, token: &str) -> Option<ResetTokenInfo> {
self.passwordresettoken_info
.get(token)
.await
.deserialized()
.ok()
}
/// Find a user's existing reset token, if any.
pub(super) async fn find_token_for_user(
&self,
user: &UserId,
) -> Option<(String, ResetTokenInfo)> {
self.passwordresettoken_info
.stream::<'_, String, ResetTokenInfo>()
.expect_ok()
.ready_find(|(_, info)| info.user == user)
.await
}
/// Remove a reset token.
pub(super) fn remove_token(&self, token: &str) { self.passwordresettoken_info.del(token); }
}

View file

@ -0,0 +1,101 @@
mod data;
use std::{sync::Arc, time::SystemTime};
use conduwuit::{Err, Result, info, utils};
use data::{Data, ResetTokenInfo};
use ruma::OwnedUserId;
use crate::{Dep, globals, users};
const RESET_TOKEN_LENGTH: usize = 32;
pub struct Service {
db: Data,
services: Services,
}
struct Services {
users: Dep<users::Service>,
globals: Dep<globals::Service>,
}
#[derive(Debug)]
pub struct ValidResetToken {
pub token: String,
pub info: ResetTokenInfo,
}
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
db: Data::new(args.db),
services: Services {
users: args.depend::<users::Service>("users"),
globals: args.depend::<globals::Service>("globals"),
},
}))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
/// Generate a random string suitable to be used as a password reset token.
#[must_use]
pub fn generate_token_string() -> String { utils::random_string(RESET_TOKEN_LENGTH) }
/// Issue a password reset token for `user`, who must be a local user with
/// the `password` origin.
pub async fn issue_token(&self, user: OwnedUserId) -> Result<ValidResetToken> {
if !self.services.globals.user_is_local(&user) {
return Err!("Cannot issue a password reset token for remote user {user}");
}
if self.services.users.origin(&user).await? != "password" {
return Err!("Cannot issue a password reset token for non-internal user {user}");
}
if let Some((existing_token, _)) = self.db.find_token_for_user(&user).await {
self.db.remove_token(&existing_token);
}
let token = Self::generate_token_string();
let info = ResetTokenInfo { user, issued_at: SystemTime::now() };
self.db.save_token(&token, &info);
info!(?info.user, "Issued a password reset token");
Ok(ValidResetToken { token, info })
}
/// Check if `token` represents a valid, non-expired password reset token.
pub async fn check_token(&self, token: &str) -> Option<ValidResetToken> {
self.db.lookup_token_info(token).await.and_then(|info| {
if info.is_valid() {
Some(ValidResetToken { token: token.to_owned(), info })
} else {
self.db.remove_token(token);
None
}
})
}
/// Consume the supplied valid token, using it to change its user's password
/// to `new_password`.
pub async fn consume_token(
&self,
ValidResetToken { token, info }: ValidResetToken,
new_password: &str,
) -> Result<()> {
if info.is_valid() {
self.db.remove_token(&token);
self.services
.users
.set_password(&info.user, Some(new_password))
.await?;
}
Ok(())
}
}

View file

@ -11,8 +11,8 @@ use crate::{
account_data, admin, announcements, antispam, appservice, client, config, emergency, account_data, admin, announcements, antispam, appservice, client, config, emergency,
federation, firstrun, globals, key_backups, federation, firstrun, globals, key_backups,
manager::Manager, manager::Manager,
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending, media, moderation, password_reset, presence, pusher, registration_tokens, resolver, rooms,
server_keys, sending, server_keys,
service::{self, Args, Map, Service}, service::{self, Args, Map, Service},
sync, transactions, uiaa, users, sync, transactions, uiaa, users,
}; };
@ -27,6 +27,7 @@ pub struct Services {
pub globals: Arc<globals::Service>, pub globals: Arc<globals::Service>,
pub key_backups: Arc<key_backups::Service>, pub key_backups: Arc<key_backups::Service>,
pub media: Arc<media::Service>, pub media: Arc<media::Service>,
pub password_reset: Arc<password_reset::Service>,
pub presence: Arc<presence::Service>, pub presence: Arc<presence::Service>,
pub pusher: Arc<pusher::Service>, pub pusher: Arc<pusher::Service>,
pub registration_tokens: Arc<registration_tokens::Service>, pub registration_tokens: Arc<registration_tokens::Service>,
@ -81,6 +82,7 @@ impl Services {
globals: build!(globals::Service), globals: build!(globals::Service),
key_backups: build!(key_backups::Service), key_backups: build!(key_backups::Service),
media: build!(media::Service), media: build!(media::Service),
password_reset: build!(password_reset::Service),
presence: build!(presence::Service), presence: build!(presence::Service),
pusher: build!(pusher::Service), pusher: build!(pusher::Service),
registration_tokens: build!(registration_tokens::Service), registration_tokens: build!(registration_tokens::Service),