From a1e755061e0ca1a422be202fd2e3bbfad776888f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Wed, 21 Dec 2022 20:44:54 +0100 Subject: [PATCH] Tokens --- crates/model/src/lib.rs | 2 +- crates/token_manager/Cargo.toml | 1 + crates/token_manager/src/actions.rs | 519 ++++++++++++++++++++++++++ crates/token_manager/src/db/tokens.rs | 12 +- crates/token_manager/src/main.rs | 482 +----------------------- 5 files changed, 538 insertions(+), 478 deletions(-) create mode 100644 crates/token_manager/src/actions.rs diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index 6604f55..3c131b5 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -1077,7 +1077,7 @@ impl ShoppingCartItem { pub struct TokenId(RecordId); #[cfg_attr(feature = "db", derive(sqlx::FromRow))] -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Token { pub id: TokenId, pub customer_id: uuid::Uuid, diff --git a/crates/token_manager/Cargo.toml b/crates/token_manager/Cargo.toml index 6177d87..427107b 100644 --- a/crates/token_manager/Cargo.toml +++ b/crates/token_manager/Cargo.toml @@ -32,6 +32,7 @@ thiserror = { version = "1" } tokio = { version = "1", features = ['full'] } tracing = { version = "0" } uuid = { version = "1", features = ['v4'] } +gumdrop = { version = "0" } [dev-dependencies] fake = { version = "2" } diff --git a/crates/token_manager/src/actions.rs b/crates/token_manager/src/actions.rs new file mode 100644 index 0000000..8cbe3ed --- /dev/null +++ b/crates/token_manager/src/actions.rs @@ -0,0 +1,519 @@ +use std::collections::BTreeMap; +use std::str::FromStr; + +use chrono::{NaiveDateTime, TimeZone, Utc}; +use config::SharedAppConfig; +use db_utils::PgT; +use hmac::digest::KeyInit; +use hmac::Hmac; +use model::{AccessTokenString, AccountId, Audience, Role, Token}; +use sha2::Sha256; + +use crate::db::Database; + +#[derive(Debug, Copy, Clone, serde::Serialize, thiserror::Error)] +#[serde(rename_all = "kebab-case", tag = "token")] +pub enum Error { + #[error("Database connection failure")] + DatabaseError, + #[error("Unable to save new token")] + Save, + #[error("Unable to save new token. Can't connect to database")] + SaveInternal, + #[error("Unable to validate token")] + Validate, + #[error("Unable to validate token. Can't connect to database")] + ValidateInternal, + #[error("Token does not exists or some fields are incorrect")] + Invalid, +} + +pub type Result = std::result::Result; + +/// Creates single token, it's mostly used by [CreatePair] +/// +/// # Examples +/// +/// ``` +/// use actix::Addr; +/// use model::{AccountId, Role}; +/// use token_manager::*; +/// async fn create_pair(token_manager: Addr) { +/// match token_manager.send(CreateToken { customer_id: uuid::Uuid::new_v4(), role: Role::Admin, subject: AccountId::from(1), audience: None, exp: None }).await { +/// Ok(Ok(pair)) => {} +/// Ok(Err(manager_error)) => {} +/// Err(actor_error) => {} +/// } +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct CreateToken { + pub customer_id: uuid::Uuid, + pub role: Role, + pub subject: AccountId, + pub audience: Option, + pub exp: Option, +} + +impl CreateToken { + pub async fn run( + self, + db: Database, + config: SharedAppConfig, + ) -> Result<(Token, AccessTokenString)> { + let mut t = db.pool.begin().await.map_err(|e| { + tracing::error!("{e}"); + Error::DatabaseError + })?; + + let auth = match self.run_t(&mut t, config).await { + Ok(auth) => auth, + Err(e) => { + t.rollback().await.ok(); + return Err(e); + } + }; + if t.commit().await.is_err() { + return Err(Error::DatabaseError); + } + Ok(auth) + } + + async fn run_t( + self, + t: &mut PgT<'_>, + config: SharedAppConfig, + ) -> Result<(Token, AccessTokenString)> { + let CreateToken { + customer_id, + role, + subject, + audience, + exp, + } = self; + + let audience = audience.unwrap_or_default(); + + let token: Token = match exp { + None => crate::db::CreateToken { + customer_id, + role, + subject, + audience, + } + .run(&mut *t) + .await + .map_err(|e| { + tracing::warn!("{e}"); + Error::Save + })?, + Some(exp) => crate::db::CreateExtendedToken { + customer_id, + role, + subject, + audience, + expiration_time: exp, + } + .run(&mut *t) + .await + .map_err(|e| { + tracing::warn!("{e}"); + Error::Save + })?, + }; + + let token_string = { + use jwt::SignWithKey; + + let secret = config.lock().web().jwt_secret(); + let key: Hmac = build_key(secret)?; + let mut claims = BTreeMap::new(); + + // cti (customer id): Customer uuid identifier used by payment service + claims.insert("cti", format!("{}", token.customer_id)); + // arl (account role): account role + claims.insert("arl", String::from(token.role.as_str())); + // iss (issuer): Issuer of the JWT + claims.insert("iss", token.issuer.to_string()); + // sub (subject): Subject of the JWT (the user) + claims.insert("sub", format!("{}", token.subject)); + // aud (audience): Recipient for which the JWT is intended + claims.insert("aud", String::from(token.audience.as_str())); + // exp (expiration time): Time after which the JWT expires + claims.insert( + "exp", + format!( + "{}", + Utc.from_utc_datetime(&token.expiration_time).format("%+") + ), + ); + // nbt (not before time): Time before which the JWT must not be accepted + // for processing + claims.insert( + "nbt", + format!( + "{}", + Utc.from_utc_datetime(&token.not_before_time).format("%+") + ), + ); + // iat (issued at time): Time at which the JWT was issued; can be used + // to determine age of the JWT, + claims.insert( + "iat", + format!( + "{}", + Utc.from_utc_datetime(&token.issued_at_time).format("%+") + ), + ); + // jti (JWT ID): Unique identifier; can be used to prevent the JWT from + // being replayed (allows a token to be used only once) + claims.insert("jti", format!("{}", token.jwt_id)); + + let s = match claims.sign_with_key(&key) { + Ok(s) => s, + Err(e) => { + tracing::error!("{e:?}"); + return Err(Error::SaveInternal); + } + }; + AccessTokenString::new(s) + }; + Ok((token, token_string)) + } +} + +#[derive(Debug, Clone)] +pub struct AuthPair { + pub access_token: Token, + pub access_token_string: AccessTokenString, + pub _refresh_token: Token, + pub refresh_token_string: model::RefreshTokenString, +} + +/// Creates access token and refresh token +/// +/// # Examples +/// +/// ``` +/// use actix::Addr; +/// use model::{AccountId, Role}; +/// use token_manager::CreatePair; +/// async fn create_pair(token_manager: Addr) { +/// match token_manager.send(CreatePair { customer_id: uuid::Uuid::new_v4(), account_id: AccountId::from(0), role: Role::Admin }).await { +/// Ok(Ok(pair)) => {} +/// Ok(Err(manager_error)) => {} +/// Err(actor_error) => {} +/// } +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct CreatePair { + pub customer_id: uuid::Uuid, + pub role: Role, + pub account_id: AccountId, +} + +impl CreatePair { + pub async fn run(self, db: Database, config: SharedAppConfig) -> Result { + let mut t = db.pool.begin().await.map_err(|e| { + tracing::error!("{e}"); + Error::DatabaseError + })?; + + let auth = match self.run_t(&mut t, config).await { + Ok(auth) => auth, + Err(e) => { + t.rollback().await.ok(); + return Err(e); + } + }; + if t.commit().await.is_err() { + return Err(Error::DatabaseError); + } + Ok(auth) + } + + async fn run_t(self, t: &mut PgT<'_>, config: SharedAppConfig) -> Result { + let (access_token, refresh_token) = ( + CreateToken { + customer_id: self.customer_id, + role: self.role, + subject: self.account_id, + audience: Some(model::Audience::Web), + exp: None, + } + .run_t(&mut *t, config.clone()) + .await, + CreateToken { + customer_id: self.customer_id, + role: self.role, + subject: self.account_id, + audience: Some(model::Audience::Web), + exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc()), + } + .run_t(&mut *t, config.clone()) + .await, + ); + + let (access_token, access_token_string): (Token, AccessTokenString) = access_token?; + let (refresh_token, refresh_token_string): (Token, AccessTokenString) = refresh_token?; + + Ok(AuthPair { + access_token, + access_token_string, + _refresh_token: refresh_token, + refresh_token_string: refresh_token_string.into(), + }) + } +} + +/// Checks if token is still valid +/// +/// # Examples +/// +/// ``` +/// use actix::Addr; +/// use model::{AccessTokenString, AccountId, Role}; +/// use token_manager::{CreatePair, Validate}; +/// async fn create_pair(token_manager: Addr, token: AccessTokenString) { +/// match token_manager.send(Validate { token }).await { +/// Ok(Ok(pair)) => {} +/// Ok(Err(manager_error)) => {} +/// Err(actor_error) => {} +/// } +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct Validate { + pub token: AccessTokenString, +} + +impl Validate { + pub async fn run(self, db: Database, config: SharedAppConfig) -> Result { + let mut t = db.pool.begin().await.map_err(|e| { + tracing::error!("{e}"); + Error::DatabaseError + })?; + + let token = match self.run_t(&mut t, config).await { + Ok(token) => token, + Err(e) => { + t.rollback().await.ok(); + return Err(e); + } + }; + if t.commit().await.is_err() { + return Err(Error::DatabaseError); + } + Ok(token) + } + + async fn run_t(self, t: &mut PgT<'_>, config: SharedAppConfig) -> Result { + use jwt::VerifyWithKey; + + tracing::info!("Validating token {:?}", self.token); + + let secret = config.lock().web().jwt_secret(); + let key: Hmac = build_key(secret)?; + let claims: BTreeMap = self.token.verify_with_key(&key).map_err(|e| { + tracing::warn!("{e}"); + Error::Validate + })?; + let jti = claims.get("jti").ok_or_else(|| Error::Validate)?; + + let token: Token = crate::db::TokenByJti { + jti: uuid::Uuid::from_str(jti).map_err(|e| { + tracing::warn!("{e}"); + Error::Validate + })?, + } + .run(&mut *t) + .await + .map_err(|e| { + tracing::error!("{e}"); + Error::Validate + })?; + + if token.expiration_time < Utc::now().naive_utc() { + return Err(Error::Validate); + } + + validate_pair(&claims, "cti", token.customer_id, validate_uuid)?; + validate_pair(&claims, "arl", token.role, eq)?; + validate_pair(&claims, "iss", &token.issuer, eq)?; + validate_pair(&claims, "sub", token.subject, validate_num)?; + validate_pair(&claims, "aud", token.audience, eq)?; + validate_pair(&claims, "exp", &token.expiration_time, validate_time)?; + validate_pair(&claims, "nbt", &token.not_before_time, validate_time)?; + validate_pair(&claims, "iat", &token.issued_at_time, validate_time)?; + + tracing::info!("JWT token valid"); + Ok(token) + } +} + +pub struct Refresh {} + +impl Refresh { + pub async fn run(self, _db: Database, _config: SharedAppConfig) -> Result { + todo!() + } +} + +fn build_key(secret: String) -> Result> { + match Hmac::new_from_slice(secret.as_bytes()) { + Ok(key) => Ok(key), + Err(e) => { + tracing::error!("{e:?}"); + dbg!(e); + Err(Error::ValidateInternal) + } + } +} + +#[inline(always)] +fn validate_pair( + claims: &BTreeMap, + key: &str, + v: V, + cmp: F, +) -> std::result::Result<(), Error> +where + F: for<'s> FnOnce(V, &'s str) -> bool, + V: PartialEq, +{ + claims + .get(key) + .map(|s| cmp(v, s.as_str())) + .unwrap_or_default() + .then_some(()) + .ok_or(Error::Invalid) +} + +#[inline(always)] +fn eq(value: V, text: &str) -> bool +where + V: for<'s> PartialEq<&'s str>, +{ + value == text +} + +#[inline(always)] +fn validate_time(left: &NaiveDateTime, right: &str) -> bool { + chrono::DateTime::parse_from_str(right, "%+") + .map(|t| t.naive_utc() == *left) + .unwrap_or_default() +} + +#[inline(always)] +fn validate_num(left: i32, right: &str) -> bool { + right.parse::().map(|n| left == n).unwrap_or_default() +} + +#[inline(always)] +fn validate_uuid(left: uuid::Uuid, right: &str) -> bool { + uuid::Uuid::from_str(right) + .map(|u| u == left) + .unwrap_or_default() +} + +// #[cfg(test)] +// mod tests { +// use actix::Actor; +// use config::UpdateConfig; +// use database_manager::Database; +// use model::*; +// +// use super::*; +// +// pub struct NoOpts; +// +// impl UpdateConfig for NoOpts {} +// +// #[actix::test] +// async fn create_token() { +// testx::db!(config, db); +// let db = db.start(); +// +// let (token, _text) = super::create_token( +// CreateToken { +// customer_id: Default::default(), +// role: Role::Admin, +// subject: AccountId::from(1), +// audience: None, +// exp: None, +// }, +// db.clone(), +// config, +// ) +// .await +// .unwrap(); +// +// db.send(database_manager::DeleteToken { token_id: token.id }) +// .await +// .ok(); +// } +// +// #[actix::test] +// async fn create_pair() { +// testx::db!(config, db); +// let db = db.start(); +// +// let AuthPair { +// access_token, +// access_token_string: _, +// refresh_token_string: _, +// _refresh_token, +// } = super::create_pair( +// CreatePair { +// customer_id: Default::default(), +// role: Role::Admin, +// account_id: AccountId::from(0), +// }, +// db.clone(), +// config, +// ) +// .await +// .unwrap(); +// +// db.send(database_manager::DeleteToken { +// token_id: access_token.id, +// }) +// .await +// .ok(); +// +// db.send(database_manager::DeleteToken { +// token_id: _refresh_token.id, +// }) +// .await +// .ok(); +// } +// +// #[actix::test] +// async fn validate() { +// testx::db!(config, db); +// let db = db.start(); +// +// let (token, text) = super::create_token( +// CreateToken { +// customer_id: Default::default(), +// role: Role::Admin, +// subject: AccountId::from(1), +// audience: None, +// exp: None, +// }, +// db.clone(), +// config.clone(), +// ) +// .await +// .unwrap(); +// +// super::validate(Validate { token: text }, db.clone(), config.clone()) +// .await +// .unwrap(); +// +// db.send(database_manager::DeleteToken { token_id: token.id }) +// .await +// .ok(); +// } +// } diff --git a/crates/token_manager/src/db/tokens.rs b/crates/token_manager/src/db/tokens.rs index 7c544a6..0352058 100644 --- a/crates/token_manager/src/db/tokens.rs +++ b/crates/token_manager/src/db/tokens.rs @@ -1,8 +1,6 @@ -use actix::Message; +use db_utils::PgT; use model::{AccountId, Audience, Token, TokenId}; -use crate::{db_async_handler, Result}; - #[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)] pub enum Error { #[error("Failed to save new token")] @@ -37,7 +35,7 @@ pub struct TokenByJti { } impl TokenByJti { - pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result { + pub async fn run(self, t: &mut PgT<'_>) -> Result { sqlx::query_as(r#" SELECT id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id FROM tokens @@ -62,7 +60,7 @@ pub struct CreateToken { } impl CreateToken { - pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result { + pub async fn run(self, t: &mut PgT<'_>) -> Result { let CreateToken { customer_id, role, @@ -97,7 +95,7 @@ pub struct CreateExtendedToken { } impl CreateExtendedToken { - pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result { + pub async fn run(self, t: &mut PgT<'_>) -> Result { let CreateExtendedToken { customer_id, role, @@ -130,7 +128,7 @@ pub struct DeleteToken { } impl DeleteToken { - pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result> { + pub async fn run(self, t: &mut PgT<'_>) -> Result> { sqlx::query_as(r#" DELETE FROM tokens WHERE id = $1 diff --git a/crates/token_manager/src/main.rs b/crates/token_manager/src/main.rs index c2b573e..5e379e9 100644 --- a/crates/token_manager/src/main.rs +++ b/crates/token_manager/src/main.rs @@ -70,21 +70,9 @@ //! } //! ``` +mod actions; mod db; -use std::collections::BTreeMap; -use std::str::FromStr; - -use channels::payments::CreatePayment; -use chrono::prelude::*; -use config::SharedAppConfig; -use hmac::digest::KeyInit; -use hmac::Hmac; -use model::{AccessTokenString, AccountId, Audience, Role, Token}; -use sha2::Sha256; - -use crate::db::Database; - /*struct Jwt { /// cti (customer id): Customer uuid identifier used by payment service pub cti: uuid::Uuid, @@ -109,462 +97,16 @@ use crate::db::Database; pub jti: uuid::Uuid, }*/ -#[derive(Debug, Copy, Clone, serde::Serialize, thiserror::Error)] -#[serde(rename_all = "kebab-case", tag = "token")] -pub enum Error { - #[error("Unable to save new token")] - Save, - #[error("Unable to save new token. Can't connect to database")] - SaveInternal, - #[error("Unable to validate token")] - Validate, - #[error("Unable to validate token. Can't connect to database")] - ValidateInternal, - #[error("Token does not exists or some fields are incorrect")] - Invalid, -} - -pub type Result = std::result::Result; - -pub struct TokenManager { - db: Database, - config: SharedAppConfig, -} - -impl TokenManager { - pub fn new(config: SharedAppConfig, db: Database) -> Self { - Self { db, config } - } -} - -/// Creates single token, it's mostly used by [CreatePair] -/// -/// # Examples -/// -/// ``` -/// use actix::Addr; -/// use model::{AccountId, Role}; -/// use token_manager::*; -/// async fn create_pair(token_manager: Addr) { -/// match token_manager.send(CreateToken { customer_id: uuid::Uuid::new_v4(), role: Role::Admin, subject: AccountId::from(1), audience: None, exp: None }).await { -/// Ok(Ok(pair)) => {} -/// Ok(Err(manager_error)) => {} -/// Err(actor_error) => {} -/// } -/// } -/// ``` -#[derive(Debug, Clone)] -pub struct CreateToken { - pub customer_id: uuid::Uuid, - pub role: Role, - pub subject: AccountId, - pub audience: Option, - pub exp: Option, -} - -impl CreateToken { - pub async fn run( - self, - db: Addr, - config: SharedAppConfig, - ) -> Result<(Token, AccessTokenString)> { - let CreateToken { - customer_id, - role, - subject, - audience, - exp, - } = self; - - let audience = audience.unwrap_or_default(); - - let token: Token = match exp { - None => query_db!( - db, - database_manager::CreateToken { - customer_id, - role, - subject, - audience, - }, - Error::Save, - Error::SaveInternal - ), - Some(exp) => query_db!( - db, - database_manager::CreateExtendedToken { - customer_id, - role, - subject, - audience, - expiration_time: exp - }, - Error::Save, - Error::SaveInternal - ), - }; - - let token_string = { - use jwt::SignWithKey; - - let secret = config.lock().web().jwt_secret(); - let key: Hmac = build_key(secret)?; - let mut claims = BTreeMap::new(); - - // cti (customer id): Customer uuid identifier used by payment service - claims.insert("cti", format!("{}", token.customer_id)); - // arl (account role): account role - claims.insert("arl", String::from(token.role.as_str())); - // iss (issuer): Issuer of the JWT - claims.insert("iss", token.issuer.to_string()); - // sub (subject): Subject of the JWT (the user) - claims.insert("sub", format!("{}", token.subject)); - // aud (audience): Recipient for which the JWT is intended - claims.insert("aud", String::from(token.audience.as_str())); - // exp (expiration time): Time after which the JWT expires - claims.insert( - "exp", - format!( - "{}", - Utc.from_utc_datetime(&token.expiration_time).format("%+") - ), - ); - // nbt (not before time): Time before which the JWT must not be accepted - // for processing - claims.insert( - "nbt", - format!( - "{}", - Utc.from_utc_datetime(&token.not_before_time).format("%+") - ), - ); - // iat (issued at time): Time at which the JWT was issued; can be used - // to determine age of the JWT, - claims.insert( - "iat", - format!( - "{}", - Utc.from_utc_datetime(&token.issued_at_time).format("%+") - ), - ); - // jti (JWT ID): Unique identifier; can be used to prevent the JWT from - // being replayed (allows a token to be used only once) - claims.insert("jti", format!("{}", token.jwt_id)); - - let s = match claims.sign_with_key(&key) { - Ok(s) => s, - Err(e) => { - tracing::error!("{e:?}"); - return Err(Error::SaveInternal); - } - }; - AccessTokenString::new(s) - }; - Ok((token, token_string)) - } -} - -#[derive(Debug, Clone)] -pub struct AuthPair { - pub access_token: Token, - pub access_token_string: AccessTokenString, - pub _refresh_token: Token, - pub refresh_token_string: model::RefreshTokenString, -} - -/// Creates access token and refresh token -/// -/// # Examples -/// -/// ``` -/// use actix::Addr; -/// use model::{AccountId, Role}; -/// use token_manager::CreatePair; -/// async fn create_pair(token_manager: Addr) { -/// match token_manager.send(CreatePair { customer_id: uuid::Uuid::new_v4(), account_id: AccountId::from(0), role: Role::Admin }).await { -/// Ok(Ok(pair)) => {} -/// Ok(Err(manager_error)) => {} -/// Err(actor_error) => {} -/// } -/// } -/// ``` -#[derive(Debug, Clone)] -pub struct CreatePair { - pub customer_id: uuid::Uuid, - pub role: Role, - pub account_id: AccountId, -} - -impl CreatePair { - pub async fn run(self, db: Database, config: SharedAppConfig) -> Result { - let (access_token, refresh_token) = tokio::join!( - create_token( - CreateToken { - customer_id: self.customer_id, - role: self.role, - subject: self.account_id, - audience: Some(model::Audience::Web), - exp: None - }, - db.clone(), - config.clone() - ), - create_token( - CreateToken { - customer_id: self.customer_id, - role: self.role, - subject: self.account_id, - audience: Some(model::Audience::Web), - exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc()) - }, - db.clone(), - config.clone() - ) - ); - let (access_token, access_token_string): (Token, AccessTokenString) = access_token?; - let (refresh_token, refresh_token_string): (Token, AccessTokenString) = refresh_token?; - Ok(AuthPair { - access_token, - access_token_string, - _refresh_token: refresh_token, - refresh_token_string: refresh_token_string.into(), - }) - } -} - -/// Checks if token is still valid -/// -/// # Examples -/// -/// ``` -/// use actix::Addr; -/// use model::{AccessTokenString, AccountId, Role}; -/// use token_manager::{CreatePair, Validate}; -/// async fn create_pair(token_manager: Addr, token: AccessTokenString) { -/// match token_manager.send(Validate { token }).await { -/// Ok(Ok(pair)) => {} -/// Ok(Err(manager_error)) => {} -/// Err(actor_error) => {} -/// } -/// } -/// ``` -#[derive(Debug, Clone)] -pub struct Validate { - pub token: AccessTokenString, -} - -impl Validate { - pub async fn run(self, db: Database, config: SharedAppConfig) -> Result { - use jwt::VerifyWithKey; - - tracing::info!("Validating token {:?}", self.token); - - let secret = config.lock().web().jwt_secret(); - let key: Hmac = build_key(secret)?; - let claims: BTreeMap = match self.token.verify_with_key(&key) { - Ok(claims) => claims, - _ => return Err(Error::Validate), - }; - let jti = match claims.get("jti") { - Some(jti) => jti, - _ => return Err(Error::Validate), - }; - - let token: Token = query_db!( - db, - database_manager::TokenByJti { - jti: match uuid::Uuid::from_str(jti) { - Ok(uid) => uid, - _ => return Err(Error::Validate), - }, - }, - Error::Validate, - Error::ValidateInternal - ); - - if token.expiration_time < Utc::now().naive_utc() { - return Err(Error::Validate); - } - - validate_pair(&claims, "cti", token.customer_id, validate_uuid)?; - validate_pair(&claims, "arl", token.role, eq)?; - validate_pair(&claims, "iss", &token.issuer, eq)?; - validate_pair(&claims, "sub", token.subject, validate_num)?; - validate_pair(&claims, "aud", token.audience, eq)?; - validate_pair(&claims, "exp", &token.expiration_time, validate_time)?; - validate_pair(&claims, "nbt", &token.not_before_time, validate_time)?; - validate_pair(&claims, "iat", &token.issued_at_time, validate_time)?; - - tracing::info!("JWT token valid"); - Ok(token) - } -} - -pub struct Refresh {} - -impl Refresh { - pub async fn run(self, db: Database, config: SharedAppConfig) -> Result { - todo!() - } -} - -fn build_key(secret: String) -> Result> { - match Hmac::new_from_slice(secret.as_bytes()) { - Ok(key) => Ok(key), - Err(e) => { - tracing::error!("{e:?}"); - dbg!(e); - Err(Error::ValidateInternal) - } - } -} - -#[inline(always)] -fn validate_pair( - claims: &BTreeMap, - key: &str, - v: V, - cmp: F, -) -> std::result::Result<(), Error> -where - F: for<'s> FnOnce(V, &'s str) -> bool, - V: PartialEq, -{ - claims - .get(key) - .map(|s| cmp(v, s.as_str())) - .unwrap_or_default() - .then_some(()) - .ok_or(Error::Invalid) -} - -#[inline(always)] -fn eq(value: V, text: &str) -> bool -where - V: for<'s> PartialEq<&'s str>, -{ - value == text -} - -#[inline(always)] -fn validate_time(left: &NaiveDateTime, right: &str) -> bool { - chrono::DateTime::parse_from_str(right, "%+") - .map(|t| t.naive_utc() == *left) - .unwrap_or_default() -} - -#[inline(always)] -fn validate_num(left: i32, right: &str) -> bool { - right.parse::().map(|n| left == n).unwrap_or_default() -} - -#[inline(always)] -fn validate_uuid(left: uuid::Uuid, right: &str) -> bool { - uuid::Uuid::from_str(right) - .map(|u| u == left) - .unwrap_or_default() -} - -// #[cfg(test)] -// mod tests { -// use actix::Actor; -// use config::UpdateConfig; -// use database_manager::Database; -// use model::*; -// -// use super::*; -// -// pub struct NoOpts; -// -// impl UpdateConfig for NoOpts {} -// -// #[actix::test] -// async fn create_token() { -// testx::db!(config, db); -// let db = db.start(); -// -// let (token, _text) = super::create_token( -// CreateToken { -// customer_id: Default::default(), -// role: Role::Admin, -// subject: AccountId::from(1), -// audience: None, -// exp: None, -// }, -// db.clone(), -// config, -// ) -// .await -// .unwrap(); -// -// db.send(database_manager::DeleteToken { token_id: token.id }) -// .await -// .ok(); -// } -// -// #[actix::test] -// async fn create_pair() { -// testx::db!(config, db); -// let db = db.start(); -// -// let AuthPair { -// access_token, -// access_token_string: _, -// refresh_token_string: _, -// _refresh_token, -// } = super::create_pair( -// CreatePair { -// customer_id: Default::default(), -// role: Role::Admin, -// account_id: AccountId::from(0), -// }, -// db.clone(), -// config, -// ) -// .await -// .unwrap(); -// -// db.send(database_manager::DeleteToken { -// token_id: access_token.id, -// }) -// .await -// .ok(); -// -// db.send(database_manager::DeleteToken { -// token_id: _refresh_token.id, -// }) -// .await -// .ok(); -// } -// -// #[actix::test] -// async fn validate() { -// testx::db!(config, db); -// let db = db.start(); -// -// let (token, text) = super::create_token( -// CreateToken { -// customer_id: Default::default(), -// role: Role::Admin, -// subject: AccountId::from(1), -// audience: None, -// exp: None, -// }, -// db.clone(), -// config.clone(), -// ) -// .await -// .unwrap(); -// -// super::validate(Validate { token: text }, db.clone(), config.clone()) -// .await -// .unwrap(); -// -// db.send(database_manager::DeleteToken { token_id: token.id }) -// .await -// .ok(); -// } -// } +#[derive(gumdrop::Options)] +pub struct Opts {} #[tokio::main] -async fn main() {} +async fn main() { + config::init_tracing("payments"); + + let opts: Opts = gumdrop::parse_args_default_or_exit(); + + let config = config::default_load(&opts); + + let _db = db::Database::build(config).await; +}