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";
pub type JwtDataClaims = TokenData<JwtClaims>;
#[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, })
}
pub fn set_exp_minutes(&mut self, exp_minutes: i64) {
self.exp_minutes = exp_minutes
}
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?,
})
}
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
}
#[inline(always)]
pub fn jwt_auth_expiration(&self) -> OffsetDateTime {
OffsetDateTime::now_utc() + self.jwt_auth_maxage()
}
#[inline(always)]
pub const fn jwt_auth_maxage(&self) -> Duration {
Duration::minutes(self.exp_minutes)
}
#[inline(always)]
pub fn jwt_refresh_expiration(&self) -> OffsetDateTime {
OffsetDateTime::now_utc() + self.jwt_refresh_maxage()
}
#[inline(always)]
pub const fn jwt_refresh_maxage(&self) -> Duration {
Duration::days(3650)
}
#[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(),
}
}
#[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);
}
#[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
}
}