use std::{
sync::{
Arc, Mutex, PoisonError, RwLock,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
use serde_json::Value;
use super::{KeyRing, PgSigningKeyStore};
use crate::error::{AuthError, Result};
#[derive(Debug)]
pub struct LiveKeyRing {
store: PgSigningKeyStore,
token_validity_secs: u64,
max_staleness_secs: u64,
reload_cooldown: Duration,
ring: RwLock<Option<Read>>,
last_miss_reload: Mutex<Option<Instant>>,
stale: AtomicBool,
stale_logged: Mutex<Option<u64>>,
}
#[derive(Debug, Clone)]
struct Read {
ring: Arc<KeyRing>,
at: u64,
}
impl LiveKeyRing {
#[must_use]
pub const fn new(
store: PgSigningKeyStore,
token_validity_secs: u64,
max_staleness_secs: u64,
reload_cooldown: Duration,
) -> Self {
Self {
store,
token_validity_secs,
max_staleness_secs,
reload_cooldown,
ring: RwLock::new(None),
last_miss_reload: Mutex::new(None),
stale: AtomicBool::new(false),
stale_logged: Mutex::new(None),
}
}
#[must_use]
pub const fn publish_window_secs(&self) -> u64 {
self.token_validity_secs.saturating_add(self.max_staleness_secs)
}
#[must_use]
pub const fn max_staleness_secs(&self) -> u64 {
self.max_staleness_secs
}
pub async fn open(
store: PgSigningKeyStore,
now: u64,
token_validity_secs: u64,
max_staleness_secs: u64,
reload_cooldown: Duration,
) -> Result<Self> {
let ring = Self::new(store, token_validity_secs, max_staleness_secs, reload_cooldown);
ring.tick(now).await?;
Ok(ring)
}
pub fn current(&self) -> Result<Arc<KeyRing>> {
Ok(self.read()?.ring)
}
pub fn signing_ring(&self, now: u64) -> Result<Arc<KeyRing>> {
let read = self.read()?;
let age = now.saturating_sub(read.at);
if age <= self.max_staleness_secs {
return Ok(read.ring);
}
let mut logged = unpoisoned(self.stale_logged.lock());
if *logged != Some(read.at) {
*logged = Some(read.at);
tracing::error!(
read_at = read.at,
age_secs = age,
max_staleness_secs = self.max_staleness_secs,
"the first-party issuer refuses to sign: its signing keys were last read too \
long ago (is the auth database reachable?)"
);
}
Err(AuthError::ConfigError {
message: format!(
"the signing keys were last read {age}s ago, more than the {}s this issuer \
signs from",
self.max_staleness_secs
),
})
}
fn read(&self) -> Result<Read> {
unpoisoned(self.ring.read()).clone().ok_or_else(|| AuthError::ConfigError {
message: "the first-party issuer's signing keys have not been read yet".to_string(),
})
}
pub async fn reload(&self, now: u64) -> Result<usize> {
let ring = self.store.load(now, self.publish_window_secs()).await?;
let published = ring.published_count();
*unpoisoned(self.ring.write()) = Some(Read {
ring: Arc::new(ring),
at: now,
});
self.stale.store(false, Ordering::Release);
Ok(published)
}
pub async fn published_key(&self, kid: &str, now: u64) -> Result<Option<Value>> {
let snapshot = unpoisoned(self.ring.read()).clone().map(|read| read.ring);
if let (false, Some(ring)) = (self.stale.load(Ordering::Acquire), snapshot) {
if let Some(jwk) = ring.published_key(kid) {
return Ok(Some(jwk.clone()));
}
if !self.claim_miss_reload() {
return Ok(None);
}
}
self.reload(now).await?;
Ok(self.current()?.published_key(kid).cloned())
}
pub fn invalidate(&self) {
self.stale.store(true, Ordering::Release);
}
pub async fn tick(&self, now: u64) -> Result<()> {
self.store.ensure_active(now).await?;
self.store.sweep(now, self.publish_window_secs()).await?;
self.reload(now).await?;
Ok(())
}
fn claim_miss_reload(&self) -> bool {
let mut last = unpoisoned(self.last_miss_reload.lock());
if last.is_some_and(|at| at.elapsed() < self.reload_cooldown) {
return false;
}
*last = Some(Instant::now());
true
}
}
fn unpoisoned<T>(result: std::result::Result<T, PoisonError<T>>) -> T {
result.unwrap_or_else(PoisonError::into_inner)
}