use crate::error::{FusekiError, FusekiResult};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenIntrospectionResponse {
pub active: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exp: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iat: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nbf: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aud: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct TokenRevocationRequest {
pub token: String,
pub token_type_hint: Option<String>,
pub client_id: Option<String>,
pub client_secret: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TokenInfo {
pub token: String,
pub token_type: String,
pub client_id: String,
pub subject: String,
pub scope: String,
pub issued_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub audience: Option<String>,
pub issuer: Option<String>,
pub jti: Option<String>,
}
pub struct TokenManager {
active_tokens: Arc<RwLock<HashMap<String, TokenInfo>>>,
revoked_tokens: Arc<RwLock<HashSet<String>>>,
revoked_jti: Arc<RwLock<HashSet<String>>>,
cleanup_interval_secs: u64,
}
impl TokenManager {
pub fn new(cleanup_interval_secs: u64) -> Self {
TokenManager {
active_tokens: Arc::new(RwLock::new(HashMap::new())),
revoked_tokens: Arc::new(RwLock::new(HashSet::new())),
revoked_jti: Arc::new(RwLock::new(HashSet::new())),
cleanup_interval_secs,
}
}
pub async fn register_token(&self, token_info: TokenInfo) -> FusekiResult<()> {
let mut tokens = self.active_tokens.write().await;
tokens.insert(token_info.token.clone(), token_info);
debug!("Registered new token");
Ok(())
}
pub async fn introspect_token(
&self,
token: &str,
_client_id: Option<&str>,
) -> FusekiResult<TokenIntrospectionResponse> {
{
let revoked = self.revoked_tokens.read().await;
if revoked.contains(token) {
debug!("Token introspection: token is revoked");
return Ok(TokenIntrospectionResponse {
active: false,
scope: None,
client_id: None,
username: None,
token_type: None,
exp: None,
iat: None,
nbf: None,
sub: None,
aud: None,
iss: None,
jti: None,
});
}
}
let tokens = self.active_tokens.read().await;
if let Some(token_info) = tokens.get(token) {
if Utc::now() > token_info.expires_at {
debug!("Token introspection: token has expired");
return Ok(TokenIntrospectionResponse {
active: false,
scope: None,
client_id: None,
username: None,
token_type: None,
exp: None,
iat: None,
nbf: None,
sub: None,
aud: None,
iss: None,
jti: None,
});
}
if let Some(ref jti) = token_info.jti {
let revoked_jti = self.revoked_jti.read().await;
if revoked_jti.contains(jti) {
debug!("Token introspection: token JTI is revoked");
return Ok(TokenIntrospectionResponse {
active: false,
scope: None,
client_id: None,
username: None,
token_type: None,
exp: None,
iat: None,
nbf: None,
sub: None,
aud: None,
iss: None,
jti: None,
});
}
}
debug!("Token introspection: token is active");
Ok(TokenIntrospectionResponse {
active: true,
scope: Some(token_info.scope.clone()),
client_id: Some(token_info.client_id.clone()),
username: Some(token_info.subject.clone()),
token_type: Some(token_info.token_type.clone()),
exp: Some(token_info.expires_at.timestamp()),
iat: Some(token_info.issued_at.timestamp()),
nbf: None,
sub: Some(token_info.subject.clone()),
aud: token_info.audience.clone(),
iss: token_info.issuer.clone(),
jti: token_info.jti.clone(),
})
} else {
debug!("Token introspection: token not found");
Ok(TokenIntrospectionResponse {
active: false,
scope: None,
client_id: None,
username: None,
token_type: None,
exp: None,
iat: None,
nbf: None,
sub: None,
aud: None,
iss: None,
jti: None,
})
}
}
pub async fn revoke_token(&self, request: TokenRevocationRequest) -> FusekiResult<()> {
info!("Revoking token (type hint: {:?})", request.token_type_hint);
{
let mut revoked = self.revoked_tokens.write().await;
revoked.insert(request.token.clone());
}
{
let tokens = self.active_tokens.read().await;
if let Some(token_info) = tokens.get(&request.token) {
if let Some(ref jti) = token_info.jti {
let mut revoked_jti = self.revoked_jti.write().await;
revoked_jti.insert(jti.clone());
debug!("Also revoked token by JTI: {}", jti);
}
}
}
{
let mut tokens = self.active_tokens.write().await;
tokens.remove(&request.token);
}
info!("Token revoked successfully");
Ok(())
}
pub async fn revoke_all_client_tokens(&self, client_id: &str) -> FusekiResult<usize> {
let mut count = 0;
let tokens_to_revoke: Vec<String> = {
let tokens = self.active_tokens.read().await;
tokens
.values()
.filter(|info| info.client_id == client_id)
.map(|info| info.token.clone())
.collect()
};
for token in tokens_to_revoke {
self.revoke_token(TokenRevocationRequest {
token,
token_type_hint: None,
client_id: Some(client_id.to_string()),
client_secret: None,
})
.await?;
count += 1;
}
info!("Revoked {} tokens for client: {}", count, client_id);
Ok(count)
}
pub async fn revoke_all_user_tokens(&self, subject: &str) -> FusekiResult<usize> {
let mut count = 0;
let tokens_to_revoke: Vec<String> = {
let tokens = self.active_tokens.read().await;
tokens
.values()
.filter(|info| info.subject == subject)
.map(|info| info.token.clone())
.collect()
};
for token in tokens_to_revoke {
self.revoke_token(TokenRevocationRequest {
token,
token_type_hint: None,
client_id: None,
client_secret: None,
})
.await?;
count += 1;
}
info!("Revoked {} tokens for user: {}", count, subject);
Ok(count)
}
pub async fn is_token_revoked(&self, token: &str) -> bool {
let revoked = self.revoked_tokens.read().await;
revoked.contains(token)
}
pub async fn is_jti_revoked(&self, jti: &str) -> bool {
let revoked_jti = self.revoked_jti.read().await;
revoked_jti.contains(jti)
}
pub async fn cleanup_expired(&self) {
let now = Utc::now();
let removed_active = {
let mut tokens = self.active_tokens.write().await;
let before_count = tokens.len();
tokens.retain(|_, info| info.expires_at > now);
before_count - tokens.len()
};
let _revocation_expiry = now - chrono::Duration::days(30);
if removed_active > 0 {
info!("Cleaned up {} expired active tokens", removed_active);
}
debug!(
"Token cleanup complete: removed {} active tokens",
removed_active
);
}
pub async fn get_stats(&self) -> TokenManagerStats {
let active = self.active_tokens.read().await.len();
let revoked = self.revoked_tokens.read().await.len();
let revoked_jti = self.revoked_jti.read().await.len();
TokenManagerStats {
active_tokens: active,
revoked_tokens: revoked,
revoked_jti,
}
}
pub fn start_cleanup_task(self: Arc<Self>) {
let manager = Arc::clone(&self);
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(
manager.cleanup_interval_secs,
));
loop {
interval.tick().await;
manager.cleanup_expired().await;
}
});
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TokenManagerStats {
pub active_tokens: usize,
pub revoked_tokens: usize,
pub revoked_jti: usize,
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_token_info(token: &str) -> TokenInfo {
TokenInfo {
token: token.to_string(),
token_type: "access_token".to_string(),
client_id: "client123".to_string(),
subject: "user123".to_string(),
scope: "openid profile email".to_string(),
issued_at: Utc::now(),
expires_at: Utc::now() + chrono::Duration::hours(1),
audience: Some("https://api.example.com".to_string()),
issuer: Some("https://issuer.example.com".to_string()),
jti: Some(format!("jti_{}", token)), }
}
#[tokio::test]
async fn test_token_registration() {
let manager = TokenManager::new(3600);
let token_info = create_test_token_info("token123");
manager.register_token(token_info).await.unwrap();
let stats = manager.get_stats().await;
assert_eq!(stats.active_tokens, 1);
}
#[tokio::test]
async fn test_token_introspection_active() {
let manager = TokenManager::new(3600);
let token_info = create_test_token_info("token123");
manager.register_token(token_info).await.unwrap();
let response = manager.introspect_token("token123", None).await.unwrap();
assert!(response.active);
assert_eq!(response.client_id.unwrap(), "client123");
}
#[tokio::test]
async fn test_token_introspection_not_found() {
let manager = TokenManager::new(3600);
let response = manager.introspect_token("nonexistent", None).await.unwrap();
assert!(!response.active);
}
#[tokio::test]
async fn test_token_revocation() {
let manager = TokenManager::new(3600);
let token_info = create_test_token_info("token123");
manager.register_token(token_info).await.unwrap();
manager
.revoke_token(TokenRevocationRequest {
token: "token123".to_string(),
token_type_hint: Some("access_token".to_string()),
client_id: None,
client_secret: None,
})
.await
.unwrap();
let response = manager.introspect_token("token123", None).await.unwrap();
assert!(!response.active);
let stats = manager.get_stats().await;
assert_eq!(stats.active_tokens, 0);
assert_eq!(stats.revoked_tokens, 1);
}
#[tokio::test]
async fn test_revoke_all_client_tokens() {
let manager = TokenManager::new(3600);
for i in 0..3 {
let mut token_info = create_test_token_info(&format!("token{}", i));
token_info.client_id = "client123".to_string();
manager.register_token(token_info).await.unwrap();
}
let mut other_token = create_test_token_info("other_token");
other_token.client_id = "client456".to_string();
manager.register_token(other_token).await.unwrap();
let count = manager.revoke_all_client_tokens("client123").await.unwrap();
assert_eq!(count, 3);
let response = manager.introspect_token("other_token", None).await.unwrap();
assert!(response.active);
}
#[tokio::test]
async fn test_expired_token_introspection() {
let manager = TokenManager::new(3600);
let mut token_info = create_test_token_info("token123");
token_info.expires_at = Utc::now() - chrono::Duration::hours(1);
manager.register_token(token_info).await.unwrap();
let response = manager.introspect_token("token123", None).await.unwrap();
assert!(!response.active);
}
#[tokio::test]
async fn test_jti_revocation() {
let manager = TokenManager::new(3600);
let token_info = create_test_token_info("token123");
manager.register_token(token_info).await.unwrap();
manager
.revoke_token(TokenRevocationRequest {
token: "token123".to_string(),
token_type_hint: None,
client_id: None,
client_secret: None,
})
.await
.unwrap();
assert!(manager.is_jti_revoked("jti_token123").await);
}
}