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 = actix_web::cookie::time::Duration::seconds(
360 self.config.token_validity.as_secs() as i64,
361 );
362
363 let mut cookie = Cookie::build(self.config.cookie_name.clone(), value)
364 .path(self.config.cookie_path.clone())
365 .max_age(max_age)
366 .http_only(self.config.cookie_http_only)
367 .same_site(self.config.cookie_same_site);
368
369 if let Some(domain) = &self.config.cookie_domain {
370 cookie = cookie.domain(domain.clone());
371 }
372
373 if self.config.cookie_secure {
374 cookie = cookie.secure(true);
375 }
376
377 cookie.finish()
378 }
379
380 pub fn cookie_name(&self) -> &str {
382 &self.config.cookie_name
383 }
384
385 pub fn parameter_name(&self) -> &str {
387 &self.config.parameter_name
388 }
389
390 pub fn is_always_remember(&self) -> bool {
392 self.config.always_remember
393 }
394
395 pub fn config(&self) -> &RememberMeConfig {
397 &self.config
398 }
399
400 #[allow(dead_code)]
402 fn generate_random_token() -> String {
403 let mut rng = rand::thread_rng();
404 let bytes: [u8; 32] = rng.gen();
405 BASE64_STANDARD.encode(bytes)
406 }
407}
408
409#[derive(Debug)]
415pub enum RememberMeError {
416 InvalidToken,
418 TokenExpired,
420 InvalidSignature,
422 UserNotFound,
424}
425
426impl std::fmt::Display for RememberMeError {
427 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428 match self {
429 RememberMeError::InvalidToken => write!(f, "Invalid remember-me token"),
430 RememberMeError::TokenExpired => write!(f, "Remember-me token expired"),
431 RememberMeError::InvalidSignature => write!(f, "Invalid token signature"),
432 RememberMeError::UserNotFound => write!(f, "User not found"),
433 }
434 }
435}
436
437impl std::error::Error for RememberMeError {}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 fn test_user() -> User {
444 User::new("testuser".to_string(), "password".to_string())
445 .roles(&["USER".into()])
446 }
447
448 #[test]
449 fn test_remember_me_config() {
450 let config = RememberMeConfig::new("secret")
451 .token_validity_days(7)
452 .cookie_name("my-remember-me")
453 .cookie_secure(false)
454 .parameter_name("rememberMe");
455
456 assert_eq!(config.get_key(), "secret");
457 assert_eq!(config.get_token_validity(), Duration::from_secs(7 * 24 * 60 * 60));
458 assert_eq!(config.get_cookie_name(), "my-remember-me");
459 assert_eq!(config.get_parameter_name(), "rememberMe");
460 }
461
462 #[test]
463 fn test_token_encode_decode() {
464 let token = RememberMeToken::new("testuser", Duration::from_secs(3600), "secret");
465 let encoded = token.encode();
466
467 let decoded = RememberMeToken::decode(&encoded).unwrap();
468 assert_eq!(decoded.username, "testuser");
469 assert_eq!(decoded.expiry, token.expiry);
470 assert_eq!(decoded.signature, token.signature);
471 }
472
473 #[test]
474 fn test_token_validation() {
475 let token = RememberMeToken::new("testuser", Duration::from_secs(3600), "secret");
476
477 assert!(token.validate("secret"));
479
480 assert!(!token.validate("wrong-secret"));
482 }
483
484 #[test]
485 fn test_token_expiry() {
486 let token = RememberMeToken {
488 username: "testuser".to_string(),
489 expiry: 1, signature: "invalid".to_string(),
491 };
492
493 assert!(token.is_expired());
494 assert!(!token.validate("secret"));
495 }
496
497 #[test]
498 fn test_remember_me_services() {
499 let config = RememberMeConfig::new("secret")
500 .token_validity_days(14)
501 .cookie_secure(false);
502
503 let services = RememberMeServices::new(config);
504 let user = test_user();
505
506 let cookie = services.login_success(&user);
508 assert_eq!(cookie.name(), "remember-me");
509
510 let username = services.auto_login(cookie.value());
512 assert_eq!(username, Some("testuser".to_string()));
513 }
514
515 #[test]
516 fn test_remember_me_logout() {
517 let config = RememberMeConfig::new("secret");
518 let services = RememberMeServices::new(config);
519
520 let cookie = services.logout();
521 assert_eq!(cookie.name(), "remember-me");
522 assert_eq!(cookie.value(), "");
523 }
524
525 #[test]
526 fn test_invalid_token() {
527 let config = RememberMeConfig::new("secret");
528 let services = RememberMeServices::new(config);
529
530 assert!(services.auto_login("not-valid-base64!!!").is_none());
532
533 let invalid = BASE64_STANDARD.encode("invalid");
535 assert!(services.auto_login(&invalid).is_none());
536 }
537}