This commit is contained in:
Adrian Woźniak 2022-12-21 20:44:54 +01:00
parent e0fae4ede8
commit a1e755061e
5 changed files with 538 additions and 478 deletions

View File

@ -1077,7 +1077,7 @@ impl ShoppingCartItem {
pub struct TokenId(RecordId); pub struct TokenId(RecordId);
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Token { pub struct Token {
pub id: TokenId, pub id: TokenId,
pub customer_id: uuid::Uuid, pub customer_id: uuid::Uuid,

View File

@ -32,6 +32,7 @@ thiserror = { version = "1" }
tokio = { version = "1", features = ['full'] } tokio = { version = "1", features = ['full'] }
tracing = { version = "0" } tracing = { version = "0" }
uuid = { version = "1", features = ['v4'] } uuid = { version = "1", features = ['v4'] }
gumdrop = { version = "0" }
[dev-dependencies] [dev-dependencies]
fake = { version = "2" } fake = { version = "2" }

View File

@ -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<T> = std::result::Result<T, Error>;
/// 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<token_manager::TokenManager>) {
/// 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<Audience>,
pub exp: Option<NaiveDateTime>,
}
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<Sha256> = 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<token_manager::TokenManager>) {
/// 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<AuthPair> {
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<AuthPair> {
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_manager::TokenManager>, 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<Token> {
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<Token> {
use jwt::VerifyWithKey;
tracing::info!("Validating token {:?}", self.token);
let secret = config.lock().web().jwt_secret();
let key: Hmac<Sha256> = build_key(secret)?;
let claims: BTreeMap<String, String> = 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<AuthPair> {
todo!()
}
}
fn build_key(secret: String) -> Result<Hmac<Sha256>> {
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<F, V>(
claims: &BTreeMap<String, String>,
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<V>(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::<i32>().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();
// }
// }

View File

@ -1,8 +1,6 @@
use actix::Message; use db_utils::PgT;
use model::{AccountId, Audience, Token, TokenId}; use model::{AccountId, Audience, Token, TokenId};
use crate::{db_async_handler, Result};
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)] #[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("Failed to save new token")] #[error("Failed to save new token")]
@ -37,7 +35,7 @@ pub struct TokenByJti {
} }
impl TokenByJti { impl TokenByJti {
pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Token> { pub async fn run(self, t: &mut PgT<'_>) -> Result<Token> {
sqlx::query_as(r#" sqlx::query_as(r#"
SELECT id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id SELECT id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id
FROM tokens FROM tokens
@ -62,7 +60,7 @@ pub struct CreateToken {
} }
impl CreateToken { impl CreateToken {
pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Token> { pub async fn run(self, t: &mut PgT<'_>) -> Result<Token> {
let CreateToken { let CreateToken {
customer_id, customer_id,
role, role,
@ -97,7 +95,7 @@ pub struct CreateExtendedToken {
} }
impl CreateExtendedToken { impl CreateExtendedToken {
pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Token> { pub async fn run(self, t: &mut PgT<'_>) -> Result<Token> {
let CreateExtendedToken { let CreateExtendedToken {
customer_id, customer_id,
role, role,
@ -130,7 +128,7 @@ pub struct DeleteToken {
} }
impl DeleteToken { impl DeleteToken {
pub async fn run(self, t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Option<Token>> { pub async fn run(self, t: &mut PgT<'_>) -> Result<Option<Token>> {
sqlx::query_as(r#" sqlx::query_as(r#"
DELETE FROM tokens DELETE FROM tokens
WHERE id = $1 WHERE id = $1

View File

@ -70,21 +70,9 @@
//! } //! }
//! ``` //! ```
mod actions;
mod db; 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 { /*struct Jwt {
/// cti (customer id): Customer uuid identifier used by payment service /// cti (customer id): Customer uuid identifier used by payment service
pub cti: uuid::Uuid, pub cti: uuid::Uuid,
@ -109,462 +97,16 @@ use crate::db::Database;
pub jti: uuid::Uuid, pub jti: uuid::Uuid,
}*/ }*/
#[derive(Debug, Copy, Clone, serde::Serialize, thiserror::Error)] #[derive(gumdrop::Options)]
#[serde(rename_all = "kebab-case", tag = "token")] pub struct Opts {}
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<T> = std::result::Result<T, Error>;
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<token_manager::TokenManager>) {
/// 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<Audience>,
pub exp: Option<NaiveDateTime>,
}
impl CreateToken {
pub async fn run(
self,
db: Addr<Database>,
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<Sha256> = 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<token_manager::TokenManager>) {
/// 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<AuthPair> {
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_manager::TokenManager>, 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<Token> {
use jwt::VerifyWithKey;
tracing::info!("Validating token {:?}", self.token);
let secret = config.lock().web().jwt_secret();
let key: Hmac<Sha256> = build_key(secret)?;
let claims: BTreeMap<String, String> = 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<AuthPair> {
todo!()
}
}
fn build_key(secret: String) -> Result<Hmac<Sha256>> {
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<F, V>(
claims: &BTreeMap<String, String>,
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<V>(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::<i32>().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();
// }
// }
#[tokio::main] #[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;
}