dipper 0.5.3

An out-of-the-box modular dependency injection web application framework.
Documentation
use std::{collections::HashMap, sync::Arc, time::Duration as StdDuration};

use fnd::{
    identifier::Uid,
    secret_key::{SecretArgsProvide, SecretKeyCache},
};
use jsonwebtoken::{self, EncodingKey, TokenData};
use salvo::jwt_auth::{ConstDecoder, CookieFinder, FormFinder, HeaderFinder, QueryFinder};
use serde::{Deserialize, Serialize};
use time::{Duration, OffsetDateTime};

use crate::{http::cookie::Cookie, prelude::*};

pub const DEFAULT_JWT_TOKEN_KEY: &str = "jwt_token";

/// Authorization data object.
pub type JwtDataClaims = TokenData<JwtClaims>;

/// Core data object of authorization.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::exhaustive_structs)]
pub struct JwtClaims {
    pub uid: Uid,
    pub exp: i64,
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub ext: HashMap<String, String>,
}

impl JwtClaims {
    pub fn new(uid: Uid, exp: i64) -> Self {
        Self {
            uid,
            exp,
            ext: Default::default(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[allow(clippy::exhaustive_structs)]
pub struct JwtTokenPair {
    pub auth_token: String,
    pub refresh_token: String,
}

pub struct JwtAuthn<P> {
    secret_key_cache: SecretKeyCache<Arc<P>>,
    jwt_token_key: String,
    exp_minutes: i64,
    auth_handler: JwtAuth<JwtClaims, ConstDecoder>,
}

#[dipper]
impl<P> JwtAuthn<P>
where
    P: SecretArgsProvide,
{
    pub async fn new(
        secret_args_provide: Arc<P>,
        secret_refresh_interval: Option<StdDuration>,
        jwt_token_key: Option<String>,
    ) -> anyhow::Result<Self> {
        let secret_key_cache = SecretKeyCache::new(secret_args_provide, secret_refresh_interval)?;
        let jwt_token_key = jwt_token_key.unwrap_or(DEFAULT_JWT_TOKEN_KEY.to_owned());
        Ok(Self {
            auth_handler: JwtAuth::new(ConstDecoder::from_secret(
                secret_key_cache.obtain_secret_key().await.as_bytes(),
            ))
            .finders(vec![
                Box::new(HeaderFinder::new()),
                Box::new(QueryFinder::new(jwt_token_key.clone())),
                Box::new(CookieFinder::new(jwt_token_key.clone())),
                Box::new(FormFinder::new(jwt_token_key.clone())),
            ]),
            secret_key_cache,
            jwt_token_key,
            exp_minutes: 14 * 24 * 60, // 14 days
        })
    }

    pub fn set_exp_minutes(&mut self, exp_minutes: i64) {
        self.exp_minutes = exp_minutes
    }

    /// JWT token key for header and cookie.
    pub const fn jwt_token_key(&self) -> &String {
        &self.jwt_token_key
    }

    pub async fn new_jwt_token_pair(
        &self,
        user_id: Uid,
        ext: Option<HashMap<String, String>>,
    ) -> Result<JwtTokenPair, ApiError> {
        Ok(JwtTokenPair {
            auth_token: self
                .new_jwt_token(user_id.clone(), self.jwt_auth_expiration(), ext.clone())
                .await?,
            refresh_token: self.new_jwt_token(user_id, self.jwt_refresh_expiration(), ext).await?,
        })
    }

    /// Inject jwt authorization token into response and return it.
    pub async fn new_jwt_token(
        &self,
        user_id: Uid,
        expiration: OffsetDateTime,
        ext: Option<HashMap<String, String>>,
    ) -> Result<String, ApiError> {
        let claim = self.new_jwt_claims(user_id, expiration, ext);
        self.encode_jwt_claims(&claim).await
    }

    /// Generate an JWT Auth Token expiration time.
    #[inline(always)]
    pub fn jwt_auth_expiration(&self) -> OffsetDateTime {
        OffsetDateTime::now_utc() + self.jwt_auth_maxage()
    }

    /// JWT Auth Token validity period duration.
    #[inline(always)]
    pub const fn jwt_auth_maxage(&self) -> Duration {
        Duration::minutes(self.exp_minutes)
    }

    /// Generate an JWT Refresh Token expiration time.
    #[inline(always)]
    pub fn jwt_refresh_expiration(&self) -> OffsetDateTime {
        OffsetDateTime::now_utc() + self.jwt_refresh_maxage()
    }

    /// JWT Refresh Token validity period duration.
    #[inline(always)]
    pub const fn jwt_refresh_maxage(&self) -> Duration {
        Duration::days(3650)
    }

    /// Create a JWT token
    #[inline(always)]
    pub fn new_jwt_claims(&self, user_id: Uid, exp: OffsetDateTime, ext: Option<HashMap<String, String>>) -> JwtClaims {
        JwtClaims {
            uid: user_id,
            exp: exp.unix_timestamp(),
            ext: ext.unwrap_or_default(),
        }
    }

    /// Encode JWT token
    #[inline(always)]
    pub async fn encode_jwt_claims(&self, claim: &JwtClaims) -> Result<String, ApiError> {
        jsonwebtoken::encode(
            &jsonwebtoken::Header::default(),
            claim,
            &EncodingKey::from_secret(self.obtain_jwt_secret_key().await.as_bytes()),
        )
        .map_err(|err| api_err!(ET_SYS_ERR, &ERR_PATH_AUTHN).with_source(err.into_error(), true))
    }

    #[inline(always)]
    pub fn set_jwt_auth_cookie(&self, token: String, res: &mut Response) {
        let cookie = Cookie::build((self.jwt_token_key.clone(), token))
            .path("/")
            .http_only(true)
            .max_age(Duration::minutes(self.exp_minutes))
            .build();
        res.add_cookie(cookie);
    }

    // /// A hoop that only detects JWT tokens.
    // #[dipper(handler)]
    // pub async fn jwt_token_hoop(&self, req: &mut Request, depot: &mut Depot, res:
    // &mut Response, ctrl: &mut FlowCtrl) {     let auth_handler =
    // self.deref().0.auth_handler.as_ref().unwrap();     auth_handler.
    // handle(req, depot, res, ctrl).await }

    /// Hoop for verifying jwt token.
    #[dipper(handler)]
    pub async fn jwt_verifying_hoop(
        self: Arc<Self>,
        req: &mut Request,
        depot: &mut Depot,
        res: &mut Response,
        ctrl: &mut FlowCtrl,
    ) {
        self.auth_handler.handle(req, depot, res, ctrl).await;
        if !ctrl.has_next() {
            return;
        }
        if let Some(data) = depot.jwt_auth_data::<JwtClaims>() {
            if data.claims.exp < OffsetDateTime::now_utc().unix_timestamp() {
                res.render(StatusError::unauthorized());
                ctrl.skip_rest();
            }
        } else {
            unreachable!()
        };
    }

    #[inline(always)]
    pub fn obtain_jwt_claims<'a>(&self, depot: &'a Depot) -> Option<&'a TokenData<JwtClaims>> {
        depot.jwt_auth_data::<JwtClaims>()
    }

    #[inline(always)]
    pub async fn obtain_jwt_secret_key(&self) -> String {
        self.secret_key_cache.obtain_secret_key().await
    }
}