use chrono::Duration;
use iso8601_timestamp::Timestamp;
use crate::{
config::EmailVerificationConfig,
models::{
totp::Totp, Account, DeletionInfo, EmailVerification, MFAMethod, MFAResponse, MFATicket,
PasswordReset, Session,
},
util::{hash_password, normalise_email},
Authifier, AuthifierEvent, Error, Result, Success,
};
impl Account {
pub async fn save(&self, authifier: &Authifier) -> Success {
authifier.database.save_account(self).await
}
pub async fn new(
authifier: &Authifier,
email: String,
plaintext_password: String,
verify_email: bool,
) -> Result<Account> {
let password = hash_password(plaintext_password)?;
let email_normalised = normalise_email(email.clone());
if let Some(mut account) = authifier
.database
.find_account_by_normalised_email(&email_normalised)
.await?
{
if let EmailVerification::Pending { .. } = &account.verification {
account.start_email_verification(authifier).await?;
} else {
account.start_password_reset(authifier, true).await?;
}
Ok(account)
} else {
let mut account = Account {
id: ulid::Ulid::new().to_string(),
email,
email_normalised,
password,
disabled: false,
verification: EmailVerification::Verified,
password_reset: None,
deletion: None,
lockout: None,
mfa: Default::default(),
};
if verify_email {
account.start_email_verification(authifier).await?;
} else {
account.save(authifier).await?;
}
authifier
.publish_event(AuthifierEvent::CreateAccount {
account: account.clone(),
})
.await;
Ok(account)
}
}
pub async fn create_session(&self, authifier: &Authifier, name: String) -> Result<Session> {
let session = Session {
id: ulid::Ulid::new().to_string(),
token: nanoid!(64),
user_id: self.id.clone(),
name,
last_seen: Timestamp::now_utc().format().to_string(),
origin: None,
subscription: None,
};
authifier.database.save_session(&session).await?;
authifier
.publish_event(AuthifierEvent::CreateSession {
session: session.clone(),
})
.await;
Ok(session)
}
pub async fn start_email_verification(&mut self, authifier: &Authifier) -> Success {
if let EmailVerificationConfig::Enabled {
templates,
expiry,
smtp,
} = &authifier.config.email_verification
{
let token = nanoid!(32);
let url = format!("{}{}", templates.verify.url, token);
smtp.send_email(
self.email.clone(),
&templates.verify,
json!({
"email": self.email.clone(),
"url": url
}),
)?;
self.verification = EmailVerification::Pending {
token,
expiry: Timestamp::UNIX_EPOCH
+ iso8601_timestamp::Duration::milliseconds(
(chrono::Utc::now() + Duration::seconds(expiry.expire_verification))
.timestamp_millis(),
),
};
} else {
self.verification = EmailVerification::Verified;
}
self.save(authifier).await
}
pub async fn start_email_move(&mut self, authifier: &Authifier, new_email: String) -> Success {
if let EmailVerification::Pending { .. } = self.verification {
return Err(Error::UnverifiedAccount);
}
if let EmailVerificationConfig::Enabled {
templates,
expiry,
smtp,
} = &authifier.config.email_verification
{
let token = nanoid!(32);
let url = format!("{}{}", templates.verify.url, token);
smtp.send_email(
new_email.clone(),
&templates.verify,
json!({
"email": self.email.clone(),
"url": url
}),
)?;
self.verification = EmailVerification::Moving {
new_email,
token,
expiry: Timestamp::UNIX_EPOCH
+ iso8601_timestamp::Duration::milliseconds(
(chrono::Utc::now() + Duration::seconds(expiry.expire_verification))
.timestamp_millis(),
),
};
} else {
self.email_normalised = normalise_email(new_email.clone());
self.email = new_email;
}
self.save(authifier).await
}
pub async fn start_password_reset(
&mut self,
authifier: &Authifier,
existing_account: bool,
) -> Success {
if let EmailVerificationConfig::Enabled {
templates,
expiry,
smtp,
} = &authifier.config.email_verification
{
let template = if existing_account {
&templates.reset_existing
} else {
&templates.reset
};
let token = nanoid!(32);
let url = format!("{}{}", template.url, token);
smtp.send_email(
self.email.clone(),
&template,
json!({
"email": self.email.clone(),
"url": url
}),
)?;
self.password_reset = Some(PasswordReset {
token,
expiry: Timestamp::UNIX_EPOCH
+ iso8601_timestamp::Duration::milliseconds(
(chrono::Utc::now() + Duration::seconds(expiry.expire_password_reset))
.timestamp_millis(),
),
});
} else {
return Err(Error::OperationFailed);
}
self.save(authifier).await
}
pub async fn start_account_deletion(&mut self, authifier: &Authifier) -> Success {
if let EmailVerificationConfig::Enabled {
templates,
expiry,
smtp,
} = &authifier.config.email_verification
{
let token = nanoid!(32);
let url = format!("{}{}", templates.deletion.url, token);
smtp.send_email(
self.email.clone(),
&templates.deletion,
json!({
"email": self.email.clone(),
"url": url
}),
)?;
self.deletion = Some(DeletionInfo::WaitingForVerification {
token,
expiry: Timestamp::UNIX_EPOCH
+ iso8601_timestamp::Duration::milliseconds(
(chrono::Utc::now() + Duration::seconds(expiry.expire_password_reset))
.timestamp_millis(),
),
});
self.save(authifier).await
} else {
self.schedule_deletion(authifier).await
}
}
pub fn verify_password(&self, plaintext_password: &str) -> Success {
argon2::verify_encoded(&self.password, plaintext_password.as_bytes())
.map(|v| {
if v {
Ok(())
} else {
Err(Error::InvalidCredentials)
}
})
.map_err(|_| Error::InvalidCredentials)?
}
pub async fn consume_mfa_response(
&mut self,
authifier: &Authifier,
response: MFAResponse,
ticket: Option<MFATicket>,
) -> Success {
let allowed_methods = self.mfa.get_methods();
match response {
MFAResponse::Password { password } => {
if allowed_methods.contains(&MFAMethod::Password) {
self.verify_password(&password)
} else {
Err(Error::DisallowedMFAMethod)
}
}
MFAResponse::Totp { totp_code } => {
if allowed_methods.contains(&MFAMethod::Totp) {
if let Totp::Enabled { .. } = &self.mfa.totp_token {
if let Some(ticket) = ticket {
if let Some(code) = ticket.last_totp_code {
if code == totp_code {
return Ok(());
}
}
}
if self.mfa.totp_token.generate_code()? == totp_code {
Ok(())
} else {
Err(Error::InvalidToken)
}
} else {
unreachable!()
}
} else {
Err(Error::DisallowedMFAMethod)
}
}
MFAResponse::Recovery { recovery_code } => {
if allowed_methods.contains(&MFAMethod::Recovery) {
if let Some(index) = self
.mfa
.recovery_codes
.iter()
.position(|x| x == &recovery_code)
{
self.mfa.recovery_codes.remove(index);
self.save(authifier).await
} else {
Err(Error::InvalidToken)
}
} else {
Err(Error::DisallowedMFAMethod)
}
}
}
}
pub async fn delete_all_sessions(
&self,
authifier: &Authifier,
exclude_session_id: Option<String>,
) -> Success {
authifier
.database
.delete_all_sessions(&self.id, exclude_session_id.clone())
.await?;
authifier
.publish_event(AuthifierEvent::DeleteAllSessions {
user_id: self.id.to_string(),
exclude_session_id,
})
.await;
Ok(())
}
pub async fn disable(&mut self, authifier: &Authifier) -> Success {
self.disabled = true;
self.delete_all_sessions(authifier, None).await?;
self.save(authifier).await
}
pub async fn schedule_deletion(&mut self, authifier: &Authifier) -> Success {
self.deletion = Some(DeletionInfo::Scheduled {
after: Timestamp::UNIX_EPOCH
+ iso8601_timestamp::Duration::milliseconds(
(chrono::Utc::now() + Duration::weeks(1)).timestamp_millis(),
),
});
self.disable(authifier).await
}
}