use hmac::{Hmac, Mac}; use iam_service::models::{CreateTenantRequest, CreateUserRequest, LoginRequest}; use iam_service::services::{AuthService, TenantService}; use sha2::Sha256; use sqlx::PgPool; use uuid::Uuid; fn fingerprint(pepper: &str, token: &str) -> String { let mut mac = Hmac::::new_from_slice(pepper.as_bytes()).unwrap(); mac.update(token.as_bytes()); hex::encode(mac.finalize().into_bytes()) } #[tokio::test] async fn refresh_token_rotate_and_expire_cases() -> Result<(), Box> { let database_url = match std::env::var("DATABASE_URL") { Ok(v) if !v.trim().is_empty() => v, _ => return Ok(()), }; let pool = PgPool::connect(&database_url).await?; let has_fingerprint: bool = sqlx::query_scalar( r#" SELECT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='refresh_tokens' AND column_name='token_fingerprint' ) "#, ) .fetch_one(&pool) .await?; if !has_fingerprint { return Ok(()); } let pepper = "test-pepper"; let tenant_service = TenantService::new(pool.clone()); let auth_service = AuthService::new(pool.clone(), pepper.to_string()); let tenant = tenant_service .create_tenant(CreateTenantRequest { name: format!("refresh-{}", Uuid::new_v4()), config: None, }) .await?; let user = auth_service .register( tenant.id, CreateUserRequest { email: format!("u-{}@example.com", Uuid::new_v4()), password: "Password12345".to_string(), }, ) .await?; let login = auth_service .login( tenant.id, LoginRequest { email: user.email.clone(), password: "Password12345".to_string(), }, ) .await?; let old_refresh = login.refresh_token.clone(); let refreshed = auth_service .refresh_access_token(old_refresh.clone()) .await?; assert_ne!(refreshed.refresh_token, old_refresh); assert!(!refreshed.access_token.is_empty()); let second = auth_service.refresh_access_token(old_refresh.clone()).await; assert!(second.is_err()); let invalid = auth_service .refresh_access_token("not-a-real-token".to_string()) .await; assert!(invalid.is_err()); let login2 = auth_service .login( tenant.id, LoginRequest { email: user.email.clone(), password: "Password12345".to_string(), }, ) .await?; let fp = fingerprint(pepper, &login2.refresh_token); sqlx::query("UPDATE refresh_tokens SET expires_at = NOW() - INTERVAL '1 second' WHERE token_fingerprint = $1") .bind(fp) .execute(&pool) .await?; let expired = auth_service .refresh_access_token(login2.refresh_token.clone()) .await; assert!(expired.is_err()); Ok(()) }