#![allow(non_snake_case)]
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use zeroize::Zeroize;
use crate::core::error::RiError;
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RiJWTClaims {
#[serde(rename = "sub")]
pub sub: String,
#[serde(rename = "exp")]
pub exp: u64,
#[serde(rename = "iat")]
pub iat: u64,
#[serde(rename = "roles")]
pub roles: Vec<String>,
#[serde(rename = "permissions")]
pub permissions: Vec<String>,
}
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct RiJWTValidationOptions {
pub validate_exp: bool,
pub validate_iat: bool,
pub required_roles: Vec<String>,
pub required_permissions: Vec<String>,
}
#[cfg(feature = "pyo3")]
#[pyo3::prelude::pymethods]
impl RiJWTValidationOptions {
#[new]
fn py_new(
validate_exp: bool,
validate_iat: bool,
required_roles: Vec<String>,
required_permissions: Vec<String>,
) -> Self {
Self {
validate_exp,
validate_iat,
required_roles,
required_permissions,
}
}
}
impl Default for RiJWTValidationOptions {
fn default() -> Self {
Self {
validate_exp: true,
validate_iat: true,
required_roles: vec![],
required_permissions: vec![],
}
}
}
#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
pub struct RiJWTManager {
secret: String,
expiry_secs: u64,
encoding_key: EncodingKey,
decoding_key: DecodingKey,
}
impl Drop for RiJWTManager {
fn drop(&mut self) {
self.secret.zeroize();
}
}
#[cfg(feature = "pyo3")]
#[pyo3::prelude::pymethods]
impl RiJWTManager {
#[new]
pub fn new(secret: String, expiry_secs: u64) -> Self {
let secret_bytes = secret.as_bytes().to_vec();
Self {
secret,
expiry_secs,
encoding_key: EncodingKey::from_secret(&secret_bytes),
decoding_key: DecodingKey::from_secret(&secret_bytes),
}
}
pub fn py_generate_token(&self, user_id: &str, roles: Vec<String>, permissions: Vec<String>) -> pyo3::prelude::PyResult<String> {
self.generate_token(user_id, roles, permissions).map_err(crate::auth::security::ri_error_to_py_err)
}
pub fn py_validate_token(&self, token: &str) -> pyo3::prelude::PyResult<pyo3::Py<pyo3::PyAny>> {
use pyo3::prelude::*;
Python::attach(|py| {
self.validate_token(token)
.map_err(crate::auth::security::ri_error_to_py_err)
.map(|claims| {
Py::new(py, claims).unwrap().into_any()
})
})
}
pub fn py_get_token_expiry(&self) -> u64 {
self.expiry_secs
}
#[pyo3(name = "generate_token")]
pub fn generate_token_py(&self, user_id: &str, roles: Vec<String>, permissions: Vec<String>) -> pyo3::prelude::PyResult<String> {
self.py_generate_token(user_id, roles, permissions)
}
#[pyo3(name = "validate_token")]
pub fn validate_token_py(&self, token: &str) -> pyo3::prelude::PyResult<pyo3::Py<pyo3::PyAny>> {
self.py_validate_token(token)
}
#[pyo3(name = "get_token_expiry")]
pub fn get_token_expiry_py(&self) -> u64 {
self.py_get_token_expiry()
}
}
impl RiJWTManager {
pub fn create(secret: String, expiry_secs: u64) -> Self {
let secret_bytes = secret.as_bytes().to_vec();
Self {
secret,
expiry_secs,
encoding_key: EncodingKey::from_secret(&secret_bytes),
decoding_key: DecodingKey::from_secret(&secret_bytes),
}
}
pub fn generate_token(&self, user_id: &str, roles: Vec<String>, permissions: Vec<String>) -> Result<String, RiError> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| RiError::Other(format!("System time error: {}", e)))?
.as_secs();
let claims = RiJWTClaims {
sub: user_id.to_string(),
exp: now + self.expiry_secs,
iat: now,
roles,
permissions,
};
encode(&Header::default(), &claims, &self.encoding_key)
.map_err(|e| RiError::Other(format!("JWT encoding failed: {}", e)))
}
pub fn validate_token(&self, token: &str) -> Result<RiJWTClaims, RiError> {
let mut validation = Validation::default();
validation.set_required_spec_claims(&["exp"]);
validation.algorithms = vec![jsonwebtoken::Algorithm::HS256];
decode::<RiJWTClaims>(token, &self.decoding_key, &validation)
.map_err(|e| {
log::warn!("[Ri.JWT] Token validation failed: {}", e);
RiError::Other("Invalid token".to_string())
})
.map(|token_data| token_data.claims)
}
pub fn get_token_expiry(&self) -> u64 {
self.expiry_secs
}
pub fn get_key_fingerprint(&self) -> String {
use sha2::{Sha256, Digest};
let prefix = if self.secret.len() > 8 {
&self.secret[..8]
} else {
&self.secret
};
let mut hasher = Sha256::new();
hasher.update(prefix.as_bytes());
format!("{:x}", hasher.finalize())
}
}