use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Instant;
use dashmap::DashMap;
use serde_json::Value;
use sha2::{Digest, Sha256};
use crate::config::JwtCacheConfig;
pub(crate) type TokenKey = [u8; 32];
pub(crate) fn token_key(token: &str) -> TokenKey {
Sha256::digest(token.as_bytes()).into()
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Now {
pub(crate) unix: u64,
pub(crate) mono: u64,
}
struct Entry {
claims: Arc<Value>,
exp: u64,
deadline: u64,
}
impl Entry {
fn live(&self, now: Now) -> bool {
now.unix < self.exp && now.mono < self.deadline
}
}
pub(crate) struct ClaimsCache {
entries: DashMap<TokenKey, Entry>,
taken: AtomicUsize,
max_entries: usize,
max_ttl_secs: u64,
max_token_bytes: usize,
started: Instant,
next_sweep: AtomicU64,
}
impl ClaimsCache {
pub(crate) fn build(config: &JwtCacheConfig) -> Result<Option<Self>, String> {
if !config.enabled {
return Ok(None);
}
if config.max_entries == 0 || config.max_ttl_secs == 0 || config.max_token_bytes == 0 {
return Err(
"auth.jwt.cache.max_entries, max_ttl_secs and max_token_bytes must be \
positive; set enabled: false to turn the cache off"
.to_string(),
);
}
Ok(Some(Self {
entries: DashMap::new(),
taken: AtomicUsize::new(0),
max_entries: config.max_entries,
max_ttl_secs: config.max_ttl_secs,
max_token_bytes: config.max_token_bytes,
started: Instant::now(),
next_sweep: AtomicU64::new(0),
}))
}
pub(crate) fn admits(&self, token: &str) -> bool {
token.len() <= self.max_token_bytes
}
pub(crate) fn now(&self) -> Now {
Now {
unix: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs()),
mono: self.started.elapsed().as_secs(),
}
}
pub(crate) fn get(&self, key: &TokenKey, now: Now) -> Option<Arc<Value>> {
{
let entry = self.entries.get(key)?;
if entry.live(now) {
return Some(entry.claims.clone());
}
}
if self
.entries
.remove_if(key, |_, entry| !entry.live(now))
.is_some()
{
self.taken.fetch_sub(1, Ordering::Relaxed);
}
None
}
pub(crate) fn insert(&self, key: TokenKey, claims: &Arc<Value>, now: Now) {
let Some(exp) = numeric_date(claims, "exp") else {
return;
};
if exp <= now.unix || numeric_date(claims, "nbf").is_some_and(|nbf| nbf > now.unix) {
return;
}
let deadline = now.mono.saturating_add(self.max_ttl_secs);
if !self.reserve() {
if !self.claim_sweep(now.mono) {
return;
}
self.sweep(now);
if !self.reserve() {
return;
}
}
let entry = Entry {
claims: claims.clone(),
exp,
deadline,
};
if self.entries.insert(key, entry).is_some() {
self.taken.fetch_sub(1, Ordering::Relaxed);
}
}
fn reserve(&self) -> bool {
self.taken
.try_update(Ordering::Relaxed, Ordering::Relaxed, |taken| {
(taken < self.max_entries).then_some(taken + 1)
})
.is_ok()
}
fn claim_sweep(&self, mono: u64) -> bool {
self.next_sweep
.try_update(Ordering::Relaxed, Ordering::Relaxed, |next| {
(mono >= next).then_some(mono + 1)
})
.is_ok()
}
fn sweep(&self, now: Now) {
let mut freed = 0usize;
self.entries.retain(|_, entry| {
let keep = entry.live(now);
freed += usize::from(!keep);
keep
});
self.taken.fetch_sub(freed, Ordering::Relaxed);
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.entries.len()
}
}
fn numeric_date(claims: &Value, name: &str) -> Option<u64> {
let value = claims.get(name)?;
if let Some(secs) = value.as_u64() {
return Some(secs);
}
let secs = value.as_f64()?;
if !(secs.is_finite() && secs >= 0.0 && secs < u64::MAX as f64) {
return None;
}
Some(secs as u64)
}
#[cfg(test)]
mod tests;