use std::marker::PhantomData;
use std::{future::Future, sync::Arc, time::SystemTime};
use actix_session::{Session, SessionExt};
use actix_web::HttpRequest;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::multifactor::factor::{CheckCodeError, Factor, GenerateCodeError};
use crate::session::{SessionUser, SESSION_KEY_USER};
pub const MFA_ID_RANDOM_CODE: &str = "RNDCODE";
const MFA_RANDOM_CODE_KEY: &str = "mfa_random_code";
pub trait CodeSender {
type User;
fn send_code(
&self,
user: &Self::User,
random_code: RandomCode,
) -> impl Future<Output = Result<(), CodeSendError>>;
}
#[derive(Error, Debug)]
#[error("Sending random code failed: {msg}")]
pub struct CodeSendError {
msg: String,
}
impl CodeSendError {
#[allow(unused)]
fn new(msg: &str) -> Self {
Self {
msg: msg.to_owned(),
}
}
}
impl Default for CodeSendError {
fn default() -> Self {
Self {
msg: "Cannot send code".to_owned(),
}
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct RandomCode {
value: String,
valid_until: SystemTime,
}
impl RandomCode {
pub fn new(value: &str, valid_until: SystemTime) -> Self {
Self {
value: value.to_owned(),
valid_until,
}
}
pub fn value(&self) -> &str {
&self.value
}
pub fn valid_until(&self) -> &SystemTime {
&self.valid_until
}
}
pub struct MfaRandomCodeFactor<U, T> {
code_generator: fn() -> RandomCode,
code_sender: Arc<T>,
phantom_data: PhantomData<U>,
}
impl<U, T: CodeSender<User = U>> MfaRandomCodeFactor<U, T> {
pub fn new(code_generator: fn() -> RandomCode, code_sender: Arc<T>) -> Self {
Self {
code_generator,
code_sender,
phantom_data: PhantomData,
}
}
}
impl MfaRandomCodeFactor<(), ()> {
pub fn id() -> String {
MFA_ID_RANDOM_CODE.to_owned()
}
}
impl<U, T> Factor for MfaRandomCodeFactor<U, T>
where
T: CodeSender<User = U> + 'static,
U: SessionUser + 'static,
{
fn generate_code(
&self,
req: &HttpRequest,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), GenerateCodeError>>>> {
let random_code = (self.code_generator)();
let session = req.get_session();
let sender = Arc::clone(&self.code_sender);
let cloned_req = req.clone();
Box::pin(async move {
let user = match cloned_req.get_session().get::<U>(SESSION_KEY_USER) {
Ok(Some(user)) => user,
Err(e) => {
return Err(purge_session_and_error(
&session,
"Could not read user from session",
e,
));
}
_ => {
session.purge();
return Err(GenerateCodeError::new("No user in session"));
}
};
session
.insert(MFA_RANDOM_CODE_KEY, random_code.clone())
.map_err(|e| {
purge_session_and_error(&session, "Could not insert mfa code into session", e)
})?;
sender
.send_code(&user, random_code)
.await
.map_err(|e| purge_session_and_error(&session, "Could not send code to user", e))?;
Ok(())
})
}
fn unique_id(&self) -> String {
MFA_ID_RANDOM_CODE.to_owned()
}
fn check_code(
&self,
code: &str,
req: &HttpRequest,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), CheckCodeError>>>> {
let session = req.get_session();
let owned_code = code.trim().to_owned();
Box::pin(async move {
let random_code = session
.get::<RandomCode>(MFA_RANDOM_CODE_KEY)
.map_err(|_| {
purge_session_and_unknown_code_error(
&session,
"Could not load random code from session",
)
})?;
if let Some(random_code) = random_code {
let now = SystemTime::now();
if &now >= random_code.valid_until() {
return Err(cleanup_and_time_is_up_error(&session));
}
if owned_code != random_code.value() {
return Err(CheckCodeError::InvalidCode);
}
Ok(())
} else {
Err(purge_session_and_unknown_code_error(
&session,
"No random code in session",
))
}
})
}
}
fn purge_session_and_error(
session: &Session,
msg: &str,
e: impl std::error::Error + 'static,
) -> GenerateCodeError {
session.purge();
GenerateCodeError::new_with_cause(msg, e)
}
fn purge_session_and_unknown_code_error(session: &Session, msg: &str) -> CheckCodeError {
session.purge();
CheckCodeError::UnknownError(msg.to_owned())
}
fn cleanup_and_time_is_up_error(session: &Session) -> CheckCodeError {
session.purge();
CheckCodeError::TimeIsUp("Code is no longer valid".to_owned())
}
#[cfg(test)]
mod tests {
use crate::session::factor_impl::random_code_auth::{MfaRandomCodeFactor, MFA_ID_RANDOM_CODE};
#[test]
fn test_static_id() {
assert_eq!(MfaRandomCodeFactor::id(), MFA_ID_RANDOM_CODE);
}
}