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",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "passwordresettoken_userid",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "pduid_pdu",
|
||||
cache_disp: CacheDisp::SharedWith("eventid_outlierpdu"),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ pub mod globals;
|
|||
pub mod key_backups;
|
||||
pub mod media;
|
||||
pub mod moderation;
|
||||
pub mod password_reset;
|
||||
pub mod presence;
|
||||
pub mod pusher;
|
||||
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,
|
||||
federation, firstrun, globals, key_backups,
|
||||
manager::Manager,
|
||||
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending,
|
||||
server_keys,
|
||||
media, moderation, password_reset, presence, pusher, registration_tokens, resolver, rooms,
|
||||
sending, server_keys,
|
||||
service::{self, Args, Map, Service},
|
||||
sync, transactions, uiaa, users,
|
||||
};
|
||||
|
|
@ -27,6 +27,7 @@ pub struct Services {
|
|||
pub globals: Arc<globals::Service>,
|
||||
pub key_backups: Arc<key_backups::Service>,
|
||||
pub media: Arc<media::Service>,
|
||||
pub password_reset: Arc<password_reset::Service>,
|
||||
pub presence: Arc<presence::Service>,
|
||||
pub pusher: Arc<pusher::Service>,
|
||||
pub registration_tokens: Arc<registration_tokens::Service>,
|
||||
|
|
@ -81,6 +82,7 @@ impl Services {
|
|||
globals: build!(globals::Service),
|
||||
key_backups: build!(key_backups::Service),
|
||||
media: build!(media::Service),
|
||||
password_reset: build!(password_reset::Service),
|
||||
presence: build!(presence::Service),
|
||||
pusher: build!(pusher::Service),
|
||||
registration_tokens: build!(registration_tokens::Service),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue