refresh_token.rs
191 lignes · 5376 octets
use chrono::{DateTime, Utc}; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// RefreshToken entity model /// /// Represents a refresh token in the authentication system. /// Refresh tokens allow users to obtain new JWT access tokens without re-authentication. /// /// # Security Features /// /// - Token rotation: Each use generates a new token and revokes the old one /// - Reuse detection: Using an already-used token revokes all tokens for that user /// - Expiration: Tokens have a configurable expiration (default 7 days) /// - Revocation: Tokens can be manually revoked (e.g., on logout) /// /// # Token Lifecycle /// /// 1. **Creation**: Token generated at login, stored with hash /// 2. **Usage**: Token used to refresh JWT, replaced by new token /// 3. **Revocation**: Token marked as revoked, cannot be used /// 4. **Expiration**: Token expires after configured duration #[derive(Clone, Debug, Serialize, Deserialize, DeriveEntityModel)] #[sea_orm(table_name = "refresh_token")] pub struct Model { /// Unique identifier for the refresh token #[sea_orm(primary_key)] pub id: Uuid, /// User ID this token belongs to pub user_id: Uuid, /// The refresh token value (stored as-is, should be generated securely) /// In production, consider hashing this value for extra security #[sea_orm(unique)] pub token: String, /// Expiration timestamp pub expires_at: DateTime<Utc>, /// Creation timestamp pub created_at: DateTime<Utc>, /// Whether the token has been revoked #[sea_orm(default_value = "false")] pub revoked: bool, /// When the token was revoked (if applicable) #[sea_orm(nullable)] pub revoked_at: Option<DateTime<Utc>>, /// ID of the token that replaced this one (for rotation tracking) #[sea_orm(nullable)] pub replaced_by: Option<Uuid>, } /// SeaORM relations for the RefreshToken entity. #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { /// The user this refresh token belongs to. #[sea_orm( belongs_to = "super::user::Entity", from = "Column::UserId", to = "super::user::Column::Id", on_delete = "Cascade" )] User, } impl Related<super::user::Entity> for Entity { fn to() -> RelationDef { Relation::User.def() } } impl ActiveModelBehavior for ActiveModel {} impl Model { /// Check if the token is expired /// /// # Returns /// /// `true` if the token has expired, `false` otherwise pub fn is_expired(&self) -> bool { Utc::now() > self.expires_at } /// Check if the token is valid (not expired and not revoked) /// /// # Returns /// /// `true` if the token is valid and can be used, `false` otherwise #[allow(dead_code)] // Utility method, used in tests pub fn is_valid(&self) -> bool { !self.is_expired() && !self.revoked } /// Check if the token has been used (replaced by another token) /// /// # Returns /// /// `true` if the token has been replaced, indicating potential reuse attack pub fn has_been_used(&self) -> bool { self.replaced_by.is_some() } } #[cfg(test)] mod tests { use super::*; use chrono::Duration; #[test] fn test_is_expired() { // Non-expired token let token = Model { id: Uuid::new_v4(), user_id: Uuid::new_v4(), token: "test_token".to_string(), expires_at: Utc::now() + Duration::days(7), created_at: Utc::now(), revoked: false, revoked_at: None, replaced_by: None, }; assert!(!token.is_expired()); // Expired token let expired_token = Model { expires_at: Utc::now() - Duration::days(1), ..token }; assert!(expired_token.is_expired()); } #[test] fn test_is_valid() { let base_token = Model { id: Uuid::new_v4(), user_id: Uuid::new_v4(), token: "test_token".to_string(), expires_at: Utc::now() + Duration::days(7), created_at: Utc::now(), revoked: false, revoked_at: None, replaced_by: None, }; // Valid token assert!(base_token.is_valid()); // Revoked token let revoked_token = Model { revoked: true, revoked_at: Some(Utc::now()), ..base_token.clone() }; assert!(!revoked_token.is_valid()); // Expired token let expired_token = Model { expires_at: Utc::now() - Duration::days(1), ..base_token }; assert!(!expired_token.is_valid()); } #[test] fn test_has_been_used() { let token = Model { id: Uuid::new_v4(), user_id: Uuid::new_v4(), token: "test_token".to_string(), expires_at: Utc::now() + Duration::days(7), created_at: Utc::now(), revoked: false, revoked_at: None, replaced_by: None, }; assert!(!token.has_been_used()); let used_token = Model { replaced_by: Some(Uuid::new_v4()), ..token }; assert!(used_token.has_been_used()); } }
GitRust