Tokens
This commit is contained in:
parent
e0fae4ede8
commit
a1e755061e
@ -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,
|
||||
|
@ -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" }
|
||||
|
519
crates/token_manager/src/actions.rs
Normal file
519
crates/token_manager/src/actions.rs
Normal 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();
|
||||
// }
|
||||
// }
|
@ -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<Token> {
|
||||
pub async fn run(self, t: &mut PgT<'_>) -> Result<Token> {
|
||||
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<Token> {
|
||||
pub async fn run(self, t: &mut PgT<'_>) -> Result<Token> {
|
||||
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<Token> {
|
||||
pub async fn run(self, t: &mut PgT<'_>) -> Result<Token> {
|
||||
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<Option<Token>> {
|
||||
pub async fn run(self, t: &mut PgT<'_>) -> Result<Option<Token>> {
|
||||
sqlx::query_as(r#"
|
||||
DELETE FROM tokens
|
||||
WHERE id = $1
|
||||
|
@ -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<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();
|
||||
// }
|
||||
// }
|
||||
#[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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user