Skip to main content

actix_security_core/http/security/
authenticator.rs

1//! In-Memory User Details Manager.
2//!
3//! # Spring Security Equivalent
4//! `org.springframework.security.provisioning.InMemoryUserDetailsManager`
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use actix_web::dev::ServiceRequest;
10use rand::distributions::Alphanumeric;
11use rand::{thread_rng, Rng};
12
13use crate::http::security::config::Authenticator;
14use crate::http::security::crypto::{NoOpPasswordEncoder, PasswordEncoder};
15use crate::http::security::user::User;
16
17#[cfg(feature = "http-basic")]
18use crate::http::security::http_basic::extract_basic_auth;
19
20/// In-memory user store for authentication.
21///
22/// # Spring Security Equivalent
23/// `InMemoryUserDetailsManager`
24///
25/// # Example
26/// ```ignore
27/// use actix_security_core::http::security::{AuthenticationManager, User};
28/// use actix_security_core::http::security::crypto::Argon2PasswordEncoder;
29///
30/// let encoder = Argon2PasswordEncoder::new();
31/// let authenticator = AuthenticationManager::in_memory_authentication()
32///     .password_encoder(encoder.clone())
33///     .with_user(
34///         User::with_encoded_password("admin", encoder.encode("secret"))
35///             .roles(&["ADMIN".into()])
36///     );
37/// ```
38pub struct MemoryAuthenticator {
39    users: HashMap<String, User>,
40    logged_users: HashMap<String, String>,
41    password_encoder: Arc<dyn PasswordEncoder>,
42}
43
44impl MemoryAuthenticator {
45    /// Creates a new in-memory authenticator with no users.
46    pub fn new() -> Self {
47        MemoryAuthenticator {
48            users: HashMap::new(),
49            logged_users: HashMap::new(),
50            password_encoder: Arc::new(NoOpPasswordEncoder),
51        }
52    }
53
54    /// Sets the password encoder for verifying passwords.
55    ///
56    /// # Spring Security Equivalent
57    /// `AuthenticationManagerBuilder.passwordEncoder(PasswordEncoder)`
58    ///
59    /// # Example
60    /// ```ignore
61    /// let encoder = Argon2PasswordEncoder::new();
62    /// let authenticator = MemoryAuthenticator::new()
63    ///     .password_encoder(encoder);
64    /// ```
65    pub fn password_encoder<E: PasswordEncoder + 'static>(mut self, encoder: E) -> Self {
66        self.password_encoder = Arc::new(encoder);
67        self
68    }
69
70    /// Adds a user to the in-memory store.
71    ///
72    /// # Example
73    /// ```ignore
74    /// let authenticator = MemoryAuthenticator::new()
75    ///     .with_user(User::new("admin".into(), "password".into()));
76    /// ```
77    pub fn with_user(mut self, user: User) -> Self {
78        use std::collections::hash_map::Entry;
79        let user_name = user.get_username().to_string();
80        match self.users.entry(user_name) {
81            Entry::Occupied(e) => {
82                eprintln!("Warning: User {} already exists, skipping", e.key());
83            }
84            Entry::Vacant(e) => {
85                e.insert(user);
86            }
87        }
88        self
89    }
90
91    /// Logs in a user and returns a session ID.
92    ///
93    /// Returns `None` if credentials are invalid.
94    pub fn login(&mut self, user_name: String, password: String) -> Option<String> {
95        self.users.get(&user_name).and_then(|u| {
96            if self.password_encoder.matches(&password, u.get_password()) {
97                let id: String = thread_rng()
98                    .sample_iter(&Alphanumeric)
99                    .take(30)
100                    .map(char::from)
101                    .collect();
102                self.logged_users.insert(id.clone(), user_name);
103                Some(id)
104            } else {
105                None
106            }
107        })
108    }
109
110    /// Logs out a user by session ID.
111    pub fn logout(&mut self, id: &str) {
112        self.logged_users.remove(id);
113    }
114
115    /// Verifies credentials and returns the user if valid.
116    pub(crate) fn verify_credentials(&self, username: &str, password: &str) -> Option<User> {
117        self.users.get(username).and_then(|user| {
118            if self.password_encoder.matches(password, user.get_password()) {
119                Some(user.clone())
120            } else {
121                None
122            }
123        })
124    }
125}
126
127impl Default for MemoryAuthenticator {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl Clone for MemoryAuthenticator {
134    fn clone(&self) -> Self {
135        MemoryAuthenticator {
136            logged_users: self
137                .logged_users
138                .iter()
139                .map(|(k, v)| (k.clone(), v.clone()))
140                .collect(),
141            users: self
142                .users
143                .iter()
144                .map(|(k, v)| (k.clone(), v.clone()))
145                .collect(),
146            password_encoder: Arc::clone(&self.password_encoder),
147        }
148    }
149}
150
151impl Authenticator for MemoryAuthenticator {
152    fn get_user(&self, req: &ServiceRequest) -> Option<User> {
153        // Try HTTP Basic Auth first (if feature enabled)
154        #[cfg(feature = "http-basic")]
155        if let Some(user) = extract_basic_auth(req, |username, password| {
156            self.verify_credentials(username, password)
157        }) {
158            return Some(user);
159        }
160
161        // Fall back to header-based auth (for backward compatibility)
162        let user_name = req.headers().get("user_name")?.to_str().ok()?;
163        let password = req.headers().get("password")?.to_str().ok()?;
164        self.verify_credentials(user_name, password)
165    }
166}