dipper 0.5.3

An out-of-the-box modular dependency injection web application framework.
Documentation
//! Identity authentication.

pub mod default;
mod jwt_token;
mod mfa;
mod password;
mod signin;
mod signup;
mod user;

use std::{ops::Deref, sync::Arc, time::Duration as StdDuration};

pub use jwt_token::*;
pub use mfa::*;
pub use password::*;
pub use signin::*;
pub use signup::*;
pub use user::*;

pub use crate::{
    fnd::{identifier::UserIdentifier, secret_key::EnvSecretArgsProvide},
    jwt_auth::JWT_AUTH_DATA_KEY,
};

/// Authentication SDK.
pub struct Authn<U: UserHandle> {
    app_name: Arc<String>,
    user_handle: Arc<U>,
    jwt: Arc<JwtAuthn<U>>,
}

impl<U> Clone for Authn<U>
where
    U: UserHandle,
{
    fn clone(&self) -> Self {
        Self {
            app_name: self.app_name.clone(),
            user_handle: self.user_handle.clone(),
            jwt: self.jwt.clone(),
        }
    }
}

impl<U> Authn<U>
where
    U: UserHandle,
{
    pub fn builder(app_name: impl Into<String>, user_handle: U) -> AuthBuilder<U> {
        AuthBuilder::new(app_name, user_handle)
    }
    pub fn user_handle(&self) -> &U {
        self.user_handle.as_ref()
    }
    pub fn jwt(&self) -> Arc<JwtAuthn<U>> {
        self.jwt.clone()
    }
    pub fn app_name(&self) -> &String {
        &self.app_name
    }
}

impl<U: UserHandle> Deref for Authn<U> {
    type Target = Arc<JwtAuthn<U>>;

    fn deref(&self) -> &Self::Target {
        &self.jwt
    }
}

pub struct AuthBuilder<U> {
    app_name: String,
    user_handle: Arc<U>,
    secret_refresh_interval: Option<StdDuration>,
    jwt_token_key: Option<String>,
    exp_minutes: i64,
}

impl<U> AuthBuilder<U>
where
    U: UserHandle,
{
    pub fn new(app_name: impl Into<String>, user_handle: U) -> Self {
        Self {
            app_name: app_name.into(),
            user_handle: Arc::new(user_handle),
            secret_refresh_interval: None,
            jwt_token_key: None,
            exp_minutes: 14 * 24 * 60, // 14 days
        }
    }
    pub const fn with_secret_refresh_interval(mut self, secret_refresh_interval: StdDuration) -> Self {
        self.secret_refresh_interval = Some(secret_refresh_interval);
        self
    }
    pub fn with_jwt_token_key(mut self, jwt_token_key: String) -> Self {
        self.jwt_token_key = Some(jwt_token_key);
        self
    }
    pub const fn with_exp_minutes(mut self, mut exp_minutes: i64) -> Self {
        if exp_minutes <= 0 {
            exp_minutes = i64::MAX;
        }
        self.exp_minutes = exp_minutes;
        self
    }
    pub async fn build(self) -> anyhow::Result<Authn<U>> {
        let mut jwt = JwtAuthn::new(
            self.user_handle.clone(),
            self.secret_refresh_interval,
            self.jwt_token_key,
        )
        .await?;
        jwt.set_exp_minutes(self.exp_minutes);
        Ok(Authn {
            app_name: Arc::new(self.app_name),
            user_handle: self.user_handle,
            jwt: Arc::new(jwt),
        })
    }
}

#[cfg(test)]
mod tests {
    use std::env;

    use crate::{
        fnd::{authn::*, identifier::Uid, secret_key, secret_key::SecretArgsProvide},
        prelude::*,
    };

    struct TestUserStorage(EnvSecretArgsProvide);
    impl TestUserStorage {
        fn new() -> Self {
            TestUserStorage(EnvSecretArgsProvide::new().unwrap())
        }
    }

    impl SecretArgsProvide for TestUserStorage {
        fn secret_key_args(&self) -> anyhow::Result<secret_key::SecretArgs> {
            self.0.secret_key_args()
        }
    }

    #[async_trait]
    impl UserHandle for TestUserStorage {
        async fn get_password_hash(
            &self,
            _depot: &mut Depot,
            _identifier: &UserIdentifier,
        ) -> Result<UidPasswordHash, ApiError> {
            Ok(UidPasswordHash {
                user_id: Uid::I64(123),
                password_hash:
                    "$argon2id$v=19$m=19456,t=2,p=1$yh6/gTQf6zOmI/TRpuPPFg$VYHht1v1X/4I4j6N4AESMAiiViO0+imS4Lg8dcWyqk4"
                        .to_string(),
            })
        }
        async fn get_totp_info(
            &self,
            _depot: &mut Depot,
            _identifier: &UserIdentifier,
        ) -> Result<UserTotpInfo, ApiError> {
            Ok(UserTotpInfo {
                user_id: Uid::I64(123),
                totp_secret: "BRUO5IP73ZFEWUQSMAGNKZU7QVNYCFBT".to_owned(),
                username: "andeya".to_owned(),
                enable_2fa: true,
            })
        }
        async fn set_password_hash(
            &self,
            _depot: &mut Depot,
            _identifier: &UserIdentifier,
            _password_hash: String,
        ) -> Result<(), ApiError> {
            Ok(())
        }
        async fn set_totp_secret(
            &self,
            _depot: &mut Depot,
            user_id: Uid,
            totp_secret: String,
        ) -> Result<UserTotpInfo, ApiError> {
            Ok(UserTotpInfo {
                user_id,
                username: "andeya".to_owned(),
                totp_secret,
                enable_2fa: true,
            })
        }
        async fn enable_2fa(&self, _depot: &mut Depot, _user_id: Uid, _enable_2fa: bool) -> Result<(), ApiError> {
            Ok(())
        }
    }

    async fn new_authn() -> Authn<TestUserStorage> {
        env::set_var("DIPPER_SECRET_KEY", "123456");
        env::set_var("DIPPER_SECRET_SATL", "x-mod");
        Authn::builder("Dipper".to_owned(), TestUserStorage::new())
            .build()
            .await
            .unwrap()
    }

    #[tokio::test]
    async fn test_totp() {
        let authn = new_authn().await;
        let totp_secret = authn.obtain_pub_totp_secret().await;
        let totp_token = authn.obtain_totp_token(TotpData::email(totp_secret.clone())).unwrap();
        println!("totp_secret={totp_secret}, totp_token={totp_token}");
    }

    #[tokio::test]
    async fn test_captcha() {
        let authn = new_authn().await;

        let captcha = authn.obtain_captcha().await;
        println!("obtain_captcha={:?}", captcha);
        let r = authn
            .verify_captcha(&CaptchaPair {
                captcha: "DMH7".to_owned(),
                captcha_sign: "HuLkAk8CdOktP5tr09ZKPQ".to_owned(),
            })
            .await;
        println!("verify_captcha={r:?}");
    }

    #[test]
    fn signup_args_json_view() {
        let signup_args = SignupArgs::email {
            username: "andeya".to_owned(),
            password: "super_secret_password".to_owned(),
            email: "andeyalee@outlook.com".to_owned(),
            totp_token: "123456".to_owned(),
        };
        println!("{}", serde_json::to_string_pretty(&signup_args).unwrap())
    }

    #[test]
    fn signin_args_json_view() {
        let signin_args = SigninArgs::email {
            email: "andeyalee@outlook.com".to_owned(),
            factor: Factor::totp_token {
                totp_token: "123456".to_owned(),
                captcha: None,
            },
        };
        println!("{}", serde_json::to_string_pretty(&signin_args).unwrap())
    }
}