feat: Add a new service for handling password resets
This commit is contained in:
parent
3d50af0943
commit
267feb3c09
5 changed files with 179 additions and 2 deletions
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
69
src/service/password_reset/data.rs
Normal file
69
src/service/password_reset/data.rs
Normal 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); }
|
||||||
|
}
|
||||||
101
src/service/password_reset/mod.rs
Normal file
101
src/service/password_reset/mod.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue