use actix_web::HttpRequest;
use crate::{
app::{App, AppTypes},
errors::Error,
hashing,
secret::{PasswordHash, Secret},
sessions,
tokens,
users::UserID,
};
#[derive(Debug)]
pub enum Notification {
UserRegistered {temporary_password: Secret},
PasswordChanged,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum Challenge<A: AppTypes> {
LogIn,
ResetPassword,
VerifyNewUser,
Custom(A::CustomChallenge),
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub enum NoCustomChallenges {}
impl NoCustomChallenges {
pub fn never_happens(self) -> ! {
match self {}
}
}
#[cfg_attr(feature = "diesel", derive(diesel::prelude::QueryableByName))]
pub struct ChallengeData<A: AppTypes> {
#[cfg_attr(feature = "diesel", diesel(embed))]
pub user: A::User,
#[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Text))]
pub challenge: String,
#[cfg_attr(feature = "diesel", diesel(deserialize_as = String), diesel(sql_type = diesel::sql_types::Text))]
pub code_hash: Secret,
#[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::Timestamp))]
pub expires: A::DateTime,
}
pub async fn issue_login_challenge<A: App>(
app: &mut A,
user: &A::User,
) -> Result<(), A::Error> {
issue_challenge(app, user, Challenge::LogIn)
.await
}
pub async fn issue_custom_challenge<A: App>(
app: &mut A,
user: &A::User,
challenge: A::CustomChallenge,
) -> Result<(), A::Error> {
issue_challenge(app, user, Challenge::Custom(challenge))
.await
}
pub(crate) async fn issue_challenge<A: App>(
app: &mut A,
user: &A::User,
challenge: Challenge<A>,
) -> Result<(), A::Error> {
let (code, code_hash) = hashing::generate_challenge_code_and_hash();
let expires_secs = 3600 * app.challenge_expire_after_hours() as u64;
let expires = app.time_now() + std::time::Duration::from_secs(expires_secs);
let challenge_str = to_json(&challenge)?;
let challenge_id = app
.insert_challenge(user, &challenge_str, code_hash, expires)
.await
.map_err(Into::into)?;
let code = tokens::pack(challenge_id, code);
if let Err(e) = app.send_challenge(user, challenge, code)
.await
{
app.delete_challenge_by_id(challenge_id)
.await
.map_err(Into::into)?;
return Err(e.into());
}
Ok(())
}
pub async fn complete_challenge<A: App>(
app: &mut A,
code: Secret,
request: &HttpRequest,
) -> Result<(A::User, Challenge<A>), A::Error> {
let (challenge_id, challenge_code) = tokens::unpack(code)
.ok_or(Error::IncorrectChallengeCode)?;
let data = app
.get_challenge_by_id(challenge_id)
.await
.map_err(Into::into)?
.ok_or(Error::IncorrectChallengeCode)?;
if app.time_now() >= data.expires {
log::debug!("Challenge #{} has expired; deleting", challenge_id);
app.delete_challenge_by_id(challenge_id)
.await
.map_err(Into::into)?;
return Error::IncorrectChallengeCode.as_app_err();
}
let challenge = parse_json::<A>(data.challenge)?;
if !hashing::check_fast_hash(&challenge_code, &data.code_hash) {
log::info!("Invalid code for challenge #{}", challenge_id);
return Error::IncorrectChallengeCode.as_app_err();
}
let user = data.user;
match challenge {
Challenge::ResetPassword => {
log::info!("Successful password reset challenge");
app.update_password(&user, PasswordHash::NONE, true)
.await
.map_err(Into::into)?;
},
Challenge::VerifyNewUser {..} => {
log::info!("Successful email verification challenge");
app.verify_user(&user)
.await
.map_err(Into::into)?;
},
Challenge::LogIn => {
log::info!("Successful email login challenge for user #{}", user.id());
},
Challenge::Custom {..} => {
log::info!("Successful custom challenge");
},
};
sessions::on_successful_challenge(app, &user, request)
.await?;
app.delete_challenge_by_id(challenge_id)
.await
.map_err(Into::into)?;
Ok((user, challenge))
}
fn parse_json<A: App>(value: impl AsRef<str>) -> Result<Challenge<A>, Error> {
serde_json::from_str(value.as_ref())
.map_err(Error::Serde)
}
fn to_json<A: App>(challenge: &Challenge<A>) -> Result<String, Error> {
serde_json::to_string(challenge)
.map_err(Error::Serde)
}