Skip to main content

actix_jwt/core/
token.rs

1//! Token data structures.
2//!
3//! Mirrors
4//! [`core/token.go`](https://github.com/LdDl/echo-jwt/blob/master/core/token.go)
5//! from the Go implementation.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// Data stored alongside each refresh token in the [`crate::core::TokenStore`].
11///
12/// # Examples
13///
14/// ```
15/// use chrono::{Duration, Utc};
16/// use actix_jwt::RefreshTokenData;
17///
18/// let data = RefreshTokenData {
19///     user_data: serde_json::json!({"user_id": 42}),
20///     expiry: Utc::now() + Duration::hours(24),
21///     created: Utc::now(),
22/// };
23/// assert!(!data.is_expired());
24/// ```
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct RefreshTokenData {
27    /// Arbitrary JSON payload associated with the token (e.g. user profile).
28    pub user_data: serde_json::Value,
29    /// Point in time after which the token is no longer valid.
30    pub expiry: DateTime<Utc>,
31    /// When the token was originally issued.
32    pub created: DateTime<Utc>,
33}
34
35impl RefreshTokenData {
36    /// Returns `true` when [`Utc::now`] is past the [`expiry`](Self::expiry).
37    pub fn is_expired(&self) -> bool {
38        Utc::now() > self.expiry
39    }
40}
41
42/// A complete JWT token pair returned by login / refresh handlers.
43///
44/// Follows the [RFC 6749 ยง5.1](https://datatracker.ietf.org/doc/html/rfc6749#section-5.1)
45/// response format.
46///
47/// # Examples
48///
49/// ```
50/// use chrono::{Duration, Utc};
51/// use actix_jwt::Token;
52///
53/// let now = Utc::now();
54/// let token = Token {
55///     access_token: "eyJ...".to_string(),
56///     token_type: "Bearer".to_string(),
57///     refresh_token: Some("dGVzdA".to_string()),
58///     expires_at: (now + Duration::hours(1)).timestamp(),
59///     created_at: now.timestamp(),
60/// };
61///
62/// assert!(token.expires_in() > 0);
63/// assert_eq!(token.token_type, "Bearer");
64/// ```
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct Token {
67    /// The signed JWT access token string.
68    pub access_token: String,
69    /// Token type, typically `"Bearer"`.
70    pub token_type: String,
71    /// Opaque refresh token (present when refresh-token rotation is enabled).
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub refresh_token: Option<String>,
74    /// Unix timestamp (seconds) when the access token expires.
75    pub expires_at: i64,
76    /// Unix timestamp (seconds) when the token pair was created.
77    pub created_at: i64,
78}
79
80impl Token {
81    /// Returns the number of seconds until the access token expires.
82    ///
83    /// A negative value means the token has already expired.
84    pub fn expires_in(&self) -> i64 {
85        self.expires_at - Utc::now().timestamp()
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use chrono::Duration;
93
94    #[test]
95    fn test_token_fields() {
96        let now = Utc::now();
97        let expires_at = (now + Duration::hours(1)).timestamp();
98        let created_at = now.timestamp();
99
100        let token = Token {
101            access_token: "test.access.token".to_string(),
102            token_type: "Bearer".to_string(),
103            refresh_token: Some("test-refresh-token".to_string()),
104            expires_at,
105            created_at,
106        };
107
108        assert_eq!(token.access_token, "test.access.token");
109        assert_eq!(token.token_type, "Bearer");
110        assert_eq!(token.refresh_token, Some("test-refresh-token".to_string()));
111        assert_eq!(token.expires_at, expires_at);
112        assert_eq!(token.created_at, created_at);
113    }
114
115    #[test]
116    fn test_token_expires_in() {
117        // Future expiry (~30 minutes from now)
118        let future_token = Token {
119            access_token: String::new(),
120            token_type: String::new(),
121            refresh_token: None,
122            expires_at: (Utc::now() + Duration::minutes(30)).timestamp(),
123            created_at: Utc::now().timestamp(),
124        };
125        let expires_in = future_token.expires_in();
126        // Allow 5 seconds tolerance for test execution time
127        assert!(
128            (expires_in - 1800).abs() <= 5,
129            "Future expiry: expires_in={}, expected ~1800",
130            expires_in
131        );
132
133        // Past expiry (~30 minutes ago)
134        let past_token = Token {
135            access_token: String::new(),
136            token_type: String::new(),
137            refresh_token: None,
138            expires_at: (Utc::now() - Duration::minutes(30)).timestamp(),
139            created_at: Utc::now().timestamp(),
140        };
141        let expires_in = past_token.expires_in();
142        assert!(
143            (expires_in + 1800).abs() <= 5,
144            "Past expiry: expires_in={}, expected ~-1800",
145            expires_in
146        );
147    }
148
149    #[test]
150    fn test_refresh_token_data_is_expired() {
151        // Valid (not expired) token
152        let valid = RefreshTokenData {
153            user_data: serde_json::json!({"user_id": "abc"}),
154            expiry: Utc::now() + Duration::hours(1),
155            created: Utc::now(),
156        };
157        assert!(
158            !valid.is_expired(),
159            "Token with future expiry should not be expired"
160        );
161
162        // Expired token
163        let expired = RefreshTokenData {
164            user_data: serde_json::json!({"user_id": "abc"}),
165            expiry: Utc::now() - Duration::hours(1),
166            created: Utc::now() - Duration::hours(2),
167        };
168        assert!(
169            expired.is_expired(),
170            "Token with past expiry should be expired"
171        );
172    }
173}