actix_security_core/http/security/
remember_me.rs1use crate::http::security::User;
44use actix_web::cookie::{Cookie, SameSite};
45use base64::prelude::*;
46use rand::Rng;
47use std::time::{Duration, SystemTime, UNIX_EPOCH};
48
49#[derive(Clone)]
58pub struct RememberMeConfig {
59 key: String,
61 token_validity: Duration,
63 cookie_name: String,
65 cookie_path: String,
67 cookie_domain: Option<String>,
69 cookie_secure: bool,
71 cookie_http_only: bool,
73 cookie_same_site: SameSite,
75 parameter_name: String,
77 always_remember: bool,
79}
80
81impl RememberMeConfig {
82 pub fn new(key: &str) -> Self {
86 Self {
87 key: key.to_string(),
88 token_validity: Duration::from_secs(14 * 24 * 60 * 60), cookie_name: "remember-me".to_string(),
90 cookie_path: "/".to_string(),
91 cookie_domain: None,
92 cookie_secure: true,
93 cookie_http_only: true,
94 cookie_same_site: SameSite::Lax,
95 parameter_name: "remember-me".to_string(),
96 always_remember: false,
97 }
98 }
99
100 pub fn token_validity_days(mut self, days: u64) -> Self {
102 self.token_validity = Duration::from_secs(days * 24 * 60 * 60);
103 self
104 }
105
106 pub fn token_validity_seconds(mut self, seconds: u64) -> Self {
108 self.token_validity = Duration::from_secs(seconds);
109 self
110 }
111
112 pub fn cookie_name(mut self, name: &str) -> Self {
114 self.cookie_name = name.to_string();
115 self
116 }
117
118 pub fn cookie_path(mut self, path: &str) -> Self {
120 self.cookie_path = path.to_string();
121 self
122 }
123
124 pub fn cookie_domain(mut self, domain: &str) -> Self {
126 self.cookie_domain = Some(domain.to_string());
127 self
128 }
129
130 pub fn cookie_secure(mut self, secure: bool) -> Self {
132 self.cookie_secure = secure;
133 self
134 }
135
136 pub fn cookie_http_only(mut self, http_only: bool) -> Self {
138 self.cookie_http_only = http_only;
139 self
140 }
141
142 pub fn cookie_same_site(mut self, same_site: SameSite) -> Self {
144 self.cookie_same_site = same_site;
145 self
146 }
147
148 pub fn parameter_name(mut self, name: &str) -> Self {
150 self.parameter_name = name.to_string();
151 self
152 }
153
154 pub fn always_remember(mut self, always: bool) -> Self {
156 self.always_remember = always;
157 self
158 }
159
160 pub fn get_key(&self) -> &str {
162 &self.key
163 }
164
165 pub fn get_token_validity(&self) -> Duration {
167 self.token_validity
168 }
169
170 pub fn get_cookie_name(&self) -> &str {
172 &self.cookie_name
173 }
174
175 pub fn get_parameter_name(&self) -> &str {
177 &self.parameter_name
178 }
179
180 pub fn is_always_remember(&self) -> bool {
182 self.always_remember
183 }
184}
185
186#[derive(Debug, Clone)]
195pub struct RememberMeToken {
196 pub username: String,
198 pub expiry: u64,
200 pub signature: String,
202}
203
204impl RememberMeToken {
205 pub fn new(username: &str, validity: Duration, key: &str) -> Self {
207 let expiry = SystemTime::now()
208 .duration_since(UNIX_EPOCH)
209 .unwrap()
210 .as_secs()
211 + validity.as_secs();
212
213 let signature = Self::compute_signature(username, expiry, key);
214
215 Self {
216 username: username.to_string(),
217 expiry,
218 signature,
219 }
220 }
221
222 fn compute_signature(username: &str, expiry: u64, key: &str) -> String {
224 use std::collections::hash_map::DefaultHasher;
225 use std::hash::{Hash, Hasher};
226
227 let mut hasher = DefaultHasher::new();
229 format!("{}:{}:{}", username, expiry, key).hash(&mut hasher);
230 format!("{:016x}", hasher.finish())
231 }
232
233 pub fn encode(&self) -> String {
235 let data = format!("{}:{}:{}", self.username, self.expiry, self.signature);
236 BASE64_STANDARD.encode(data.as_bytes())
237 }
238
239 pub fn decode(encoded: &str) -> Option<Self> {
241 let decoded = BASE64_STANDARD.decode(encoded).ok()?;
242 let data = String::from_utf8(decoded).ok()?;
243
244 let parts: Vec<&str> = data.splitn(3, ':').collect();
245 if parts.len() != 3 {
246 return None;
247 }
248
249 Some(Self {
250 username: parts[0].to_string(),
251 expiry: parts[1].parse().ok()?,
252 signature: parts[2].to_string(),
253 })
254 }
255
256 pub fn validate(&self, key: &str) -> bool {
258 let now = SystemTime::now()
260 .duration_since(UNIX_EPOCH)
261 .unwrap()
262 .as_secs();
263
264 if now > self.expiry {
265 return false;
266 }
267
268 let expected_signature = Self::compute_signature(&self.username, self.expiry, key);
270 self.signature == expected_signature
271 }
272
273 pub fn is_expired(&self) -> bool {
275 let now = SystemTime::now()
276 .duration_since(UNIX_EPOCH)
277 .unwrap()
278 .as_secs();
279 now > self.expiry
280 }
281}
282
283#[derive(Clone)]
297pub struct RememberMeServices {
298 config: RememberMeConfig,
299}
300
301impl RememberMeServices {
302 pub fn new(config: RememberMeConfig) -> Self {
304 Self { config }
305 }
306
307 pub fn login_success(&self, user: &User) -> Cookie<'static> {
312 let token = RememberMeToken::new(
313 user.get_username(),
314 self.config.token_validity,
315 &self.config.key,
316 );
317
318 self.create_cookie(token.encode())
319 }
320
321 pub fn auto_login(&self, cookie_value: &str) -> Option<String> {
326 let token = RememberMeToken::decode(cookie_value)?;
327
328 if token.validate(&self.config.key) {
329 Some(token.username)
330 } else {
331 None
332 }
333 }
334
335 pub fn logout(&self) -> Cookie<'static> {
340 let mut cookie = Cookie::build(self.config.cookie_name.clone(), "")
341 .path(self.config.cookie_path.clone())
342 .max_age(actix_web::cookie::time::Duration::ZERO)
343 .http_only(self.config.cookie_http_only)
344 .same_site(self.config.cookie_same_site);
345
346 if let Some(domain) = &self.config.cookie_domain {
347 cookie = cookie.domain(domain.clone());
348 }
349
350 if self.config.cookie_secure {
351 cookie = cookie.secure(true);
352 }
353
354 cookie.finish()
355 }
356
357 fn create_cookie(&self, value: String) -> Cookie<'static> {
359 let max_age =
360 actix_web::cookie::time::Duration::seconds(self.config.token_validity.as_secs() as i64);
361
362 let mut cookie = Cookie::build(self.config.cookie_name.clone(), value)
363 .path(self.config.cookie_path.clone())
364 .max_age(max_age)
365 .http_only(self.config.cookie_http_only)
366 .same_site(self.config.cookie_same_site);
367
368 if let Some(domain) = &self.config.cookie_domain {
369 cookie = cookie.domain(domain.clone());
370 }
371
372 if self.config.cookie_secure {
373 cookie = cookie.secure(true);
374 }
375
376 cookie.finish()
377 }
378
379 pub fn cookie_name(&self) -> &str {
381 &self.config.cookie_name
382 }
383
384 pub fn parameter_name(&self) -> &str {
386 &self.config.parameter_name
387 }
388
389 pub fn is_always_remember(&self) -> bool {
391 self.config.always_remember
392 }
393
394 pub fn config(&self) -> &RememberMeConfig {
396 &self.config
397 }
398
399 #[allow(dead_code)]
401 fn generate_random_token() -> String {
402 let mut rng = rand::thread_rng();
403 let bytes: [u8; 32] = rng.gen();
404 BASE64_STANDARD.encode(bytes)
405 }
406}
407
408#[derive(Debug)]
414pub enum RememberMeError {
415 InvalidToken,
417 TokenExpired,
419 InvalidSignature,
421 UserNotFound,
423}
424
425impl std::fmt::Display for RememberMeError {
426 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427 match self {
428 RememberMeError::InvalidToken => write!(f, "Invalid remember-me token"),
429 RememberMeError::TokenExpired => write!(f, "Remember-me token expired"),
430 RememberMeError::InvalidSignature => write!(f, "Invalid token signature"),
431 RememberMeError::UserNotFound => write!(f, "User not found"),
432 }
433 }
434}
435
436impl std::error::Error for RememberMeError {}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 fn test_user() -> User {
443 User::new("testuser".to_string(), "password".to_string()).roles(&["USER".into()])
444 }
445
446 #[test]
447 fn test_remember_me_config() {
448 let config = RememberMeConfig::new("secret")
449 .token_validity_days(7)
450 .cookie_name("my-remember-me")
451 .cookie_secure(false)
452 .parameter_name("rememberMe");
453
454 assert_eq!(config.get_key(), "secret");
455 assert_eq!(
456 config.get_token_validity(),
457 Duration::from_secs(7 * 24 * 60 * 60)
458 );
459 assert_eq!(config.get_cookie_name(), "my-remember-me");
460 assert_eq!(config.get_parameter_name(), "rememberMe");
461 }
462
463 #[test]
464 fn test_token_encode_decode() {
465 let token = RememberMeToken::new("testuser", Duration::from_secs(3600), "secret");
466 let encoded = token.encode();
467
468 let decoded = RememberMeToken::decode(&encoded).unwrap();
469 assert_eq!(decoded.username, "testuser");
470 assert_eq!(decoded.expiry, token.expiry);
471 assert_eq!(decoded.signature, token.signature);
472 }
473
474 #[test]
475 fn test_token_validation() {
476 let token = RememberMeToken::new("testuser", Duration::from_secs(3600), "secret");
477
478 assert!(token.validate("secret"));
480
481 assert!(!token.validate("wrong-secret"));
483 }
484
485 #[test]
486 fn test_token_expiry() {
487 let token = RememberMeToken {
489 username: "testuser".to_string(),
490 expiry: 1, signature: "invalid".to_string(),
492 };
493
494 assert!(token.is_expired());
495 assert!(!token.validate("secret"));
496 }
497
498 #[test]
499 fn test_remember_me_services() {
500 let config = RememberMeConfig::new("secret")
501 .token_validity_days(14)
502 .cookie_secure(false);
503
504 let services = RememberMeServices::new(config);
505 let user = test_user();
506
507 let cookie = services.login_success(&user);
509 assert_eq!(cookie.name(), "remember-me");
510
511 let username = services.auto_login(cookie.value());
513 assert_eq!(username, Some("testuser".to_string()));
514 }
515
516 #[test]
517 fn test_remember_me_logout() {
518 let config = RememberMeConfig::new("secret");
519 let services = RememberMeServices::new(config);
520
521 let cookie = services.logout();
522 assert_eq!(cookie.name(), "remember-me");
523 assert_eq!(cookie.value(), "");
524 }
525
526 #[test]
527 fn test_invalid_token() {
528 let config = RememberMeConfig::new("secret");
529 let services = RememberMeServices::new(config);
530
531 assert!(services.auto_login("not-valid-base64!!!").is_none());
533
534 let invalid = BASE64_STANDARD.encode("invalid");
536 assert!(services.auto_login(&invalid).is_none());
537 }
538}