2022-04-19 08:04:40 +02:00
|
|
|
use std::collections::BTreeMap;
|
2022-04-18 22:07:52 +02:00
|
|
|
use std::str::FromStr;
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
use actix::{Addr, Message};
|
|
|
|
use chrono::prelude::*;
|
2022-04-19 08:04:40 +02:00
|
|
|
use hmac::digest::KeyInit;
|
|
|
|
use hmac::Hmac;
|
|
|
|
use sha2::Sha256;
|
2022-04-18 22:07:52 +02:00
|
|
|
|
|
|
|
use crate::database::{Database, TokenByJti};
|
|
|
|
use crate::model::{AccountId, Audience, Token, TokenString};
|
2022-04-20 14:30:59 +02:00
|
|
|
use crate::{database, Role};
|
|
|
|
|
|
|
|
#[macro_export]
|
|
|
|
macro_rules! token_async_handler {
|
|
|
|
($msg: ty, $async: ident, $res: ty) => {
|
|
|
|
impl actix::Handler<$msg> for TokenManager {
|
|
|
|
type Result = actix::ResponseActFuture<Self, Result<$res>>;
|
|
|
|
|
|
|
|
fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result {
|
|
|
|
use actix::WrapFuture;
|
|
|
|
let db = self.db.clone();
|
|
|
|
let secret = self.secret.clone();
|
|
|
|
Box::pin(async { $async(msg, db, secret).await }.into_actor(self))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
2022-04-18 22:07:52 +02:00
|
|
|
|
2022-04-19 16:49:30 +02:00
|
|
|
/*struct Jwt {
|
2022-04-18 22:07:52 +02:00
|
|
|
/// cti (customer id): Customer uuid identifier used by payment service
|
|
|
|
pub cti: uuid::Uuid,
|
|
|
|
/// arl (account role): account role
|
|
|
|
pub arl: Role,
|
|
|
|
/// iss (issuer): Issuer of the JWT
|
|
|
|
pub iss: String,
|
|
|
|
/// sub (subject): Subject of the JWT (the user)
|
|
|
|
pub sub: i32,
|
|
|
|
/// aud (audience): Recipient for which the JWT is intended
|
|
|
|
pub aud: Audience,
|
|
|
|
/// exp (expiration time): Time after which the JWT expires
|
|
|
|
pub exp: chrono::NaiveDateTime,
|
|
|
|
/// nbt (not before time): Time before which the JWT must not be accepted
|
|
|
|
/// for processing
|
|
|
|
pub nbt: chrono::NaiveDateTime,
|
|
|
|
/// iat (issued at time): Time at which the JWT was issued; can be used to
|
|
|
|
/// determine age of the JWT,
|
|
|
|
pub iat: chrono::NaiveDateTime,
|
|
|
|
/// jti (JWT ID): Unique identifier; can be used to prevent the JWT from
|
|
|
|
/// being replayed (allows a token to be used only once)
|
|
|
|
pub jti: uuid::Uuid,
|
2022-04-19 16:49:30 +02:00
|
|
|
}*/
|
2022-04-18 22:07:52 +02:00
|
|
|
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub type Result<T> = std::result::Result<T, Error>;
|
|
|
|
|
|
|
|
pub struct TokenManager {
|
|
|
|
db: Addr<Database>,
|
|
|
|
secret: Arc<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl actix::Actor for TokenManager {
|
|
|
|
type Context = actix::Context<Self>;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TokenManager {
|
|
|
|
pub fn new(db: Addr<Database>) -> Self {
|
|
|
|
let secret = Arc::new(std::env::var("JWT_SECRET").expect("JWT_SECRET is required"));
|
|
|
|
Self { db, secret }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Message)]
|
|
|
|
#[rtype(result = "Result<(Token, TokenString)>")]
|
|
|
|
pub struct CreateToken {
|
|
|
|
pub customer_id: uuid::Uuid,
|
|
|
|
pub role: Role,
|
|
|
|
pub subject: AccountId,
|
|
|
|
pub audience: Option<Audience>,
|
|
|
|
}
|
|
|
|
|
|
|
|
token_async_handler!(CreateToken, create_token, (Token, TokenString));
|
|
|
|
|
2022-04-19 08:04:40 +02:00
|
|
|
pub(crate) async fn create_token(
|
2022-04-18 22:07:52 +02:00
|
|
|
msg: CreateToken,
|
|
|
|
db: Addr<Database>,
|
|
|
|
secret: Arc<String>,
|
|
|
|
) -> Result<(Token, TokenString)> {
|
|
|
|
let CreateToken {
|
|
|
|
customer_id,
|
|
|
|
role,
|
|
|
|
subject,
|
|
|
|
audience,
|
|
|
|
} = msg;
|
|
|
|
let audience = audience.unwrap_or_default();
|
|
|
|
|
|
|
|
let token: Token = match db
|
|
|
|
.send(database::CreateToken {
|
|
|
|
customer_id,
|
|
|
|
role,
|
|
|
|
subject,
|
|
|
|
audience,
|
|
|
|
})
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
Ok(Ok(token)) => token,
|
|
|
|
Ok(Err(db_err)) => {
|
|
|
|
log::error!("{db_err}");
|
|
|
|
return Err(Error::Save);
|
|
|
|
}
|
|
|
|
Err(act_err) => {
|
|
|
|
log::error!("{act_err:?}");
|
|
|
|
return Err(Error::SaveInternal);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let token_string = {
|
|
|
|
use jwt::SignWithKey;
|
|
|
|
|
2022-04-19 08:04:40 +02:00
|
|
|
let key: Hmac<Sha256> = build_key(secret)?;
|
2022-04-18 22:07:52 +02:00
|
|
|
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
|
2022-04-19 16:49:30 +02:00
|
|
|
claims.insert("arl", String::from(token.role.as_str()));
|
2022-04-18 22:07:52 +02:00
|
|
|
// iss (issuer): Issuer of the JWT
|
2022-04-19 16:49:30 +02:00
|
|
|
claims.insert("iss", token.issuer.to_string());
|
2022-04-18 22:07:52 +02:00
|
|
|
// sub (subject): Subject of the JWT (the user)
|
|
|
|
claims.insert("sub", format!("{}", token.subject));
|
|
|
|
// aud (audience): Recipient for which the JWT is intended
|
2022-04-19 16:49:30 +02:00
|
|
|
claims.insert("aud", String::from(token.audience.as_str()));
|
2022-04-18 22:07:52 +02:00
|
|
|
// exp (expiration time): Time after which the JWT expires
|
2022-04-19 16:49:30 +02:00
|
|
|
claims.insert(
|
|
|
|
"exp",
|
|
|
|
format!(
|
|
|
|
"{}",
|
|
|
|
Utc.from_utc_datetime(&token.expiration_time).format("%+")
|
|
|
|
),
|
|
|
|
);
|
2022-04-18 22:07:52 +02:00
|
|
|
// nbt (not before time): Time before which the JWT must not be accepted
|
|
|
|
// for processing
|
2022-04-19 16:49:30 +02:00
|
|
|
claims.insert(
|
|
|
|
"nbt",
|
|
|
|
format!(
|
|
|
|
"{}",
|
|
|
|
Utc.from_utc_datetime(&token.not_before_time).format("%+")
|
|
|
|
),
|
|
|
|
);
|
2022-04-18 22:07:52 +02:00
|
|
|
// iat (issued at time): Time at which the JWT was issued; can be used
|
|
|
|
// to determine age of the JWT,
|
2022-04-19 16:49:30 +02:00
|
|
|
claims.insert(
|
|
|
|
"iat",
|
|
|
|
format!(
|
|
|
|
"{}",
|
|
|
|
Utc.from_utc_datetime(&token.issued_at_time).format("%+")
|
|
|
|
),
|
|
|
|
);
|
2022-04-18 22:07:52 +02:00
|
|
|
// 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));
|
|
|
|
|
2022-04-19 16:49:30 +02:00
|
|
|
let s = match claims.sign_with_key(&key) {
|
2022-04-18 22:07:52 +02:00
|
|
|
Ok(s) => s,
|
|
|
|
Err(e) => {
|
|
|
|
log::error!("{e:?}");
|
|
|
|
return Err(Error::SaveInternal);
|
|
|
|
}
|
2022-04-19 16:49:30 +02:00
|
|
|
};
|
|
|
|
TokenString::from(s)
|
2022-04-18 22:07:52 +02:00
|
|
|
};
|
|
|
|
Ok((token, token_string))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Message)]
|
|
|
|
#[rtype(result = "Result<(Token, bool)>")]
|
|
|
|
pub struct Validate {
|
|
|
|
pub token: TokenString,
|
|
|
|
}
|
|
|
|
|
|
|
|
token_async_handler!(Validate, validate, (Token, bool));
|
|
|
|
|
|
|
|
pub(crate) async fn validate(
|
|
|
|
msg: Validate,
|
|
|
|
db: Addr<Database>,
|
|
|
|
secret: Arc<String>,
|
|
|
|
) -> Result<(Token, bool)> {
|
|
|
|
use jwt::VerifyWithKey;
|
|
|
|
|
|
|
|
log::info!("Validating token {:?}", msg.token);
|
|
|
|
|
2022-04-19 08:04:40 +02:00
|
|
|
let key: Hmac<Sha256> = build_key(secret)?;
|
2022-04-18 22:07:52 +02:00
|
|
|
let claims: BTreeMap<String, String> = match msg.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 = match db
|
|
|
|
.send(TokenByJti {
|
2022-04-19 16:49:30 +02:00
|
|
|
jti: match uuid::Uuid::from_str(jti) {
|
|
|
|
Ok(uid) => uid,
|
|
|
|
_ => return Err(Error::Validate),
|
|
|
|
},
|
2022-04-18 22:07:52 +02:00
|
|
|
})
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
Ok(Ok(token)) => token,
|
|
|
|
Ok(Err(e)) => {
|
|
|
|
log::error!("{e}");
|
|
|
|
return Err(Error::Validate);
|
|
|
|
}
|
|
|
|
Err(e) => {
|
|
|
|
log::error!("{e:?}");
|
|
|
|
return Err(Error::ValidateInternal);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-04-19 08:04:40 +02:00
|
|
|
if !validate_pair(&claims, "cti", token.customer_id, validate_uuid) {
|
|
|
|
return Ok((token, false));
|
2022-04-18 22:07:52 +02:00
|
|
|
}
|
2022-04-19 16:49:30 +02:00
|
|
|
if !validate_pair(&claims, "arl", token.role, |left, right| right == left) {
|
|
|
|
return Ok((token, false));
|
2022-04-18 22:07:52 +02:00
|
|
|
}
|
2022-04-19 16:49:30 +02:00
|
|
|
if !validate_pair(&claims, "iss", &token.issuer, |left, right| right == left) {
|
|
|
|
return Ok((token, false));
|
2022-04-18 22:07:52 +02:00
|
|
|
}
|
2022-04-19 08:04:40 +02:00
|
|
|
if !validate_pair(&claims, "sub", token.subject, validate_num) {
|
|
|
|
return Ok((token, false));
|
2022-04-18 22:07:52 +02:00
|
|
|
}
|
2022-04-19 16:49:30 +02:00
|
|
|
if !validate_pair(&claims, "aud", token.audience, |left, right| right == left) {
|
|
|
|
return Ok((token, false));
|
2022-04-18 22:07:52 +02:00
|
|
|
}
|
2022-04-19 08:04:40 +02:00
|
|
|
if !validate_pair(&claims, "exp", &token.expiration_time, validate_time) {
|
|
|
|
return Ok((token, false));
|
2022-04-18 22:07:52 +02:00
|
|
|
}
|
2022-04-19 08:04:40 +02:00
|
|
|
if !validate_pair(&claims, "nbt", &token.not_before_time, validate_time) {
|
|
|
|
return Ok((token, false));
|
2022-04-18 22:07:52 +02:00
|
|
|
}
|
2022-04-19 08:04:40 +02:00
|
|
|
if !validate_pair(&claims, "iat", &token.issued_at_time, validate_time) {
|
|
|
|
return Ok((token, false));
|
2022-04-18 22:07:52 +02:00
|
|
|
}
|
|
|
|
|
2022-04-19 16:49:30 +02:00
|
|
|
log::info!("JWT token valid");
|
2022-04-18 22:07:52 +02:00
|
|
|
Ok((token, true))
|
|
|
|
}
|
|
|
|
|
2022-04-19 08:04:40 +02:00
|
|
|
fn build_key(secret: Arc<String>) -> Result<Hmac<Sha256>> {
|
|
|
|
match Hmac::new_from_slice(secret.as_bytes()) {
|
|
|
|
Ok(key) => Ok(key),
|
|
|
|
Err(e) => {
|
|
|
|
log::error!("{e:?}");
|
|
|
|
Err(Error::ValidateInternal)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn validate_pair<F, V>(claims: &BTreeMap<String, String>, key: &str, v: V, cmp: F) -> bool
|
|
|
|
where
|
|
|
|
F: FnOnce(&str, V) -> bool,
|
|
|
|
V: PartialEq,
|
|
|
|
{
|
|
|
|
claims.get(key).map(|s| cmp(s, v)).unwrap_or_default()
|
|
|
|
}
|
|
|
|
|
2022-04-18 22:07:52 +02:00
|
|
|
fn validate_time(left: &str, right: &NaiveDateTime) -> bool {
|
|
|
|
chrono::DateTime::parse_from_str(left, "%+")
|
|
|
|
.map(|t| t.naive_utc() == *right)
|
|
|
|
.unwrap_or_default()
|
|
|
|
}
|
2022-04-19 08:04:40 +02:00
|
|
|
|
|
|
|
fn validate_num(left: &str, right: i32) -> bool {
|
|
|
|
left.parse::<i32>().map(|n| n == right).unwrap_or_default()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn validate_uuid(left: &str, right: uuid::Uuid) -> bool {
|
|
|
|
uuid::Uuid::from_str(left)
|
|
|
|
.map(|u| u == right)
|
|
|
|
.unwrap_or_default()
|
|
|
|
}
|