Skip to main content

systemprompt_api/services/middleware/jwt/
revocation.rs

1//! JTI revocation gate for the JWT context extractor.
2//!
3//! Runs as the final stateful check after a token's claims, its backing user,
4//! and the session row have all validated. It answers the one question
5//! signature validation cannot: has this specific token been explicitly
6//! revoked (logout, admin revoke, refresh rotation)? A negative result is
7//! cached so the hot path costs one map lookup. Fails closed — a revocation
8//! store error rejects the request rather than admitting an unverifiable token.
9
10use std::sync::Arc;
11use systemprompt_database::DbPool;
12use systemprompt_models::execution::context::ContextExtractionError;
13use systemprompt_oauth::OauthResult;
14use systemprompt_oauth::repository::{JtiRevocationCache, OAuthRepository};
15
16#[derive(Clone)]
17pub struct JtiRevocationChecker {
18    repo: Arc<OAuthRepository>,
19    cache: Arc<JtiRevocationCache>,
20}
21
22impl std::fmt::Debug for JtiRevocationChecker {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        f.debug_struct("JtiRevocationChecker")
25            .finish_non_exhaustive()
26    }
27}
28
29impl JtiRevocationChecker {
30    pub fn from_pool(db: &DbPool) -> OauthResult<Self> {
31        Ok(Self {
32            repo: Arc::new(OAuthRepository::new(db)?),
33            cache: Arc::new(JtiRevocationCache::new()),
34        })
35    }
36
37    pub async fn ensure_not_revoked(&self, jti: &str) -> Result<(), ContextExtractionError> {
38        if jti.is_empty() {
39            return Ok(());
40        }
41        match self.cache.peek(jti) {
42            Some(true) => return Err(ContextExtractionError::Revoked),
43            Some(false) => return Ok(()),
44            None => {},
45        }
46
47        let revoked = self.repo.is_jti_revoked(jti).await.map_err(|e| {
48            ContextExtractionError::DatabaseError(format!("JTI revocation lookup failed: {e}"))
49        })?;
50        self.cache.record(jti, revoked);
51        if revoked {
52            Err(ContextExtractionError::Revoked)
53        } else {
54            Ok(())
55        }
56    }
57}