use cache::redis::RedisError; use jwt::error::JwtError; use jwt::session; use jwt::signer::{decode_access_token, decode_refresh_token, extract_kid}; use rate_limit::AccessTokenBlacklist; use crate::error::AuthError; use crate::features::AuthService; use super::{LogoutCmd, LogoutResp}; fn map_redis(e: RedisError) -> AuthError { AuthError::Internal(format!("redis: {e}")) } /// Revoke the session bound to a refresh token. Idempotent: an unknown token /// yields `revoked = false` without error so that retries are safe. /// /// When an access token is supplied (best-effort), its `jti` is also placed on /// the deny-list so the already-issued, stateless access token stops validating /// immediately instead of remaining live until expiry. pub async fn logout( cmd: LogoutCmd, svc: &AuthService, access_token: Option, ) -> Result { let db = svc.db.writer(); let kid = extract_kid(&cmd.refresh_token).ok_or(JwtError::InvalidHeader)?; let cache = svc.jwt.cache.read().await; let cached = cache .get(&kid) .ok_or(JwtError::UnknownKid(kid.to_string()))?; let _claims = decode_refresh_token( &cmd.refresh_token, &cached.decoding, cached.algorithm, &svc.jwt.settings.issuer, &svc.jwt.settings.audience, svc.jwt.settings.clock_skew_seconds, )?; drop(cache); let token_hash = session::hash_token(&cmd.refresh_token); let hit = session::find_session_by_hash(&db, &token_hash).await?; let resp = match hit { Some(sess) => { session::revoke_session(&db, sess.id).await?; LogoutResp { revoked: true } } None => LogoutResp { revoked: false }, }; // VULN-005: revoke the presented access token too (best-effort — an expired // or malformed token is simply ignored). if let Some(token) = access_token && let Some(akid) = extract_kid(&token) && let Some(acached) = svc.jwt.cache.read().await.get(&akid) && let Ok(claims) = decode_access_token( &token, &acached.decoding, acached.algorithm, &svc.jwt.settings.issuer, &svc.jwt.settings.audience, svc.jwt.settings.clock_skew_seconds, ) { let ttl = svc.jwt.settings.access_ttl.whole_seconds().max(1) as u64; AccessTokenBlacklist::new(svc.kv.clone()) .revoke_jti(claims.jti, ttl) .await .map_err(map_redis)?; } Ok(resp) }