authfix 0.1.1

A simple and extensible authentication layer for Actix Web, designed for fast integration and easy configuration.
Documentation
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};

/// ID to reference random code mfa
pub const MFA_ID_RANDOM_CODE: &str = "RNDCODE";

const MFA_RANDOM_CODE_KEY: &str = "mfa_random_code";

/// Interface for sending the code to the user
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(),
        }
    }
}

/// The code and its validity generated by [MfaRandomCodeFactor]
#[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
    }
}

/// Random code implementation of [Factor]
///
/// It generates a [RandomCode] via a generator function and sends the code via [CodeSender] to the user.
/// *Currently it only works with the [SessionAuthProvider](crate::session::session_auth::SessionAuthProvider).*
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() {
                    // ToDo: here we need to cound the attempts and reject finally with cleanup
                    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);
    }
}