123 lines
3.9 KiB
Rust
123 lines
3.9 KiB
Rust
use std::env;
|
|
|
|
use argon2::{password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
|
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
|
|
use rand::prelude::*;
|
|
use rand_chacha::ChaCha20Rng;
|
|
use serde::{Deserialize, Serialize};
|
|
use siren::ServiceError;
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct TokenClaims {
|
|
sub: String, // Subject (User)
|
|
id: String, // Access Token ID
|
|
iss: String, // Issuer (Service)
|
|
exp: i64, // Expiration time
|
|
iat: i64, // Issued At
|
|
nbf: i64 // Not Before
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct AccessToken {
|
|
pub token: Option<String>, // Access Token
|
|
pub id: String, // Access Token ID
|
|
pub email: String, // Subject (User)
|
|
pub expiration: Option<i64> // Expiration time
|
|
}
|
|
|
|
impl AccessToken {
|
|
fn new(email: &str) -> Result<Self, ServiceError> {
|
|
let ttl = env::var("ACCESS_TOKEN_MAXAGE")
|
|
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
|
.parse::<i64>()
|
|
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
|
let keys_dir = env::var("KEYS_DIR_PATH")?;
|
|
let private_key = std::fs::read_to_string(format!("{}/private_key.pem", keys_dir))?;
|
|
let now = chrono::Utc::now();
|
|
let mut token_details = Self {
|
|
token: None,
|
|
id: csprng_128bit(),
|
|
email: email.to_string(),
|
|
expiration: Some((now + chrono::Duration::minutes(ttl)).timestamp())
|
|
};
|
|
let claims = TokenClaims {
|
|
sub: token_details.email.clone(),
|
|
iss: "siren".to_string(),
|
|
id: token_details.id.to_string(),
|
|
exp: token_details.expiration.unwrap(),
|
|
iat: now.timestamp(),
|
|
nbf: now.timestamp()
|
|
};
|
|
let header = Header::new(Algorithm::RS256);
|
|
let key = EncodingKey::from_rsa_pem(private_key.as_bytes())?;
|
|
let token = encode(&header, &claims, &key)?;
|
|
token_details.token = Some(token);
|
|
Ok(token_details)
|
|
}
|
|
|
|
pub fn decode(token: &str, public_key: &str) -> Result<Self, ServiceError> {
|
|
let key = DecodingKey::from_rsa_pem(public_key.as_bytes())?;
|
|
let validation = Validation::new(Algorithm::RS256);
|
|
let decoded = decode::<TokenClaims>(token, &key, &validation)?;
|
|
let email = decoded.claims.sub;
|
|
let id = decoded.claims.id;
|
|
Ok(Self { token: None, id, email, expiration: None })
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct RefreshToken {
|
|
pub id: String,
|
|
pub email: String,
|
|
pub ip_address: String,
|
|
pub tokens: Vec<String>,
|
|
pub expiration: i64,
|
|
}
|
|
|
|
impl RefreshToken {
|
|
pub fn new(email: &str, ip_address: &str) -> Self {
|
|
let ttl = env::var("REFRESH_TOKEN_MAXAGE")
|
|
.expect("REFRESH_TOKEN_MAXAGE must be set")
|
|
.parse::<i64>()
|
|
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
|
let now = chrono::Utc::now();
|
|
Self {
|
|
id: csprng_128bit(),
|
|
email: email.to_string(),
|
|
ip_address: hash(&ip_address).unwrap(),
|
|
tokens: vec![],
|
|
expiration: (now + chrono::Duration::minutes(ttl)).timestamp()
|
|
}
|
|
}
|
|
|
|
pub fn create_access_token(&mut self) -> Result<AccessToken, ServiceError> {
|
|
let access_token = AccessToken::new(&self.email)?;
|
|
self.tokens.push(access_token.id.clone());
|
|
Ok(access_token)
|
|
}
|
|
}
|
|
|
|
fn csprng_128bit() -> String {
|
|
// Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9)
|
|
let rng = ChaCha20Rng::from_entropy();
|
|
rng.sample_iter(rand::distributions::Alphanumeric).take(16).map(char::from).collect()
|
|
}
|
|
|
|
pub fn hash(str: &str) -> Result<String, ServiceError> {
|
|
let salt = SaltString::generate(&mut OsRng);
|
|
let bytes = str.as_bytes();
|
|
let hash = Argon2::default().hash_password(bytes, &salt)?.to_string();
|
|
Ok(hash)
|
|
}
|
|
|
|
pub fn verify_hash(str: &str, hash: &str) -> bool {
|
|
let bytes = str.as_bytes();
|
|
let parsed_hash = match PasswordHash::new(hash) {
|
|
Ok(h) => h,
|
|
Err(_) => return false
|
|
};
|
|
match Argon2::default().verify_password(bytes, &parsed_hash) {
|
|
Ok(_) => true,
|
|
Err(_) => false
|
|
}
|
|
} |