reinhardt_auth/core/base_user.rs
1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::fmt::Display;
4use unicode_normalization::UnicodeNormalization;
5
6use crate::core::hasher::PasswordHasher;
7
8/// BaseUser trait - Django-style authentication
9///
10/// This trait provides the core user authentication functionality, including
11/// password hashing and verification. It is inspired by Django's AbstractBaseUser.
12///
13/// # Type Parameters
14///
15/// * `PrimaryKey` - The type of the user's primary key (e.g., `Uuid`, `i64`)
16/// * `Hasher` - The password hasher implementation (default: `Argon2Hasher`)
17///
18/// # Examples
19///
20/// ```
21/// use reinhardt_auth::{BaseUser, PasswordHasher};
22/// #[cfg(feature = "argon2-hasher")]
23/// use reinhardt_auth::Argon2Hasher;
24/// use uuid::Uuid;
25/// use chrono::{DateTime, Utc};
26/// use serde::{Serialize, Deserialize};
27///
28/// #[derive(Serialize, Deserialize)]
29/// struct MyUser {
30/// id: Uuid,
31/// email: String,
32/// password_hash: Option<String>,
33/// last_login: Option<DateTime<Utc>>,
34/// is_active: bool,
35/// }
36///
37/// #[cfg(feature = "argon2-hasher")]
38/// impl BaseUser for MyUser {
39/// type PrimaryKey = Uuid;
40/// type Hasher = Argon2Hasher;
41///
42/// fn get_username_field() -> &'static str { "email" }
43/// fn get_username(&self) -> &str { &self.email }
44/// fn password_hash(&self) -> Option<&str> { self.password_hash.as_deref() }
45/// fn set_password_hash(&mut self, hash: String) { self.password_hash = Some(hash); }
46/// fn last_login(&self) -> Option<DateTime<Utc>> { self.last_login }
47/// fn set_last_login(&mut self, time: DateTime<Utc>) { self.last_login = Some(time); }
48/// fn is_active(&self) -> bool { self.is_active }
49/// }
50///
51/// # #[cfg(feature = "argon2-hasher")]
52/// # {
53/// let mut user = MyUser {
54/// id: Uuid::now_v7(),
55/// email: "user@example.com".to_string(),
56/// password_hash: None,
57/// last_login: None,
58/// is_active: true,
59/// };
60///
61/// // Set password
62/// user.set_password("secure_password123").unwrap();
63/// assert!(user.has_usable_password());
64///
65/// // Check password
66/// assert!(user.check_password("secure_password123").unwrap());
67/// assert!(!user.check_password("wrong_password").unwrap());
68/// # }
69/// ```
70pub trait BaseUser: Send + Sync + Serialize + for<'de> Deserialize<'de> {
71 /// The type of the primary key (e.g., Uuid, i64)
72 type PrimaryKey: Clone + Send + Sync + Display;
73
74 /// The password hasher to use (e.g., Argon2Hasher)
75 type Hasher: PasswordHasher + Default;
76
77 /// Returns the name of the username field
78 ///
79 /// This is typically "username" or "email", depending on your user model.
80 fn get_username_field() -> &'static str;
81
82 /// Returns the username value
83 fn get_username(&self) -> &str;
84
85 /// Returns the hashed password, if set
86 fn password_hash(&self) -> Option<&str>;
87
88 /// Sets the hashed password
89 fn set_password_hash(&mut self, hash: String);
90
91 /// Returns the last login time
92 fn last_login(&self) -> Option<DateTime<Utc>>;
93
94 /// Sets the last login time
95 fn set_last_login(&mut self, time: DateTime<Utc>);
96
97 /// Returns whether the user account is active
98 fn is_active(&self) -> bool;
99
100 /// Normalizes the username for consistent storage
101 ///
102 /// By default, applies NFKC Unicode normalization. Override this method
103 /// if you need different normalization behavior.
104 ///
105 /// # Examples
106 ///
107 /// ```
108 /// use reinhardt_auth::BaseUser;
109 /// # use reinhardt_auth::PasswordHasher;
110 /// # #[cfg(feature = "argon2-hasher")]
111 /// # use reinhardt_auth::Argon2Hasher;
112 /// # use uuid::Uuid;
113 /// # use chrono::{DateTime, Utc};
114 /// # use serde::{Serialize, Deserialize};
115 /// # #[derive(Serialize, Deserialize)]
116 /// # struct MyUser { id: Uuid, email: String, password_hash: Option<String>,
117 /// # last_login: Option<DateTime<Utc>>, is_active: bool }
118 /// # #[cfg(feature = "argon2-hasher")]
119 /// # impl BaseUser for MyUser {
120 /// # type PrimaryKey = Uuid;
121 /// # type Hasher = Argon2Hasher;
122 /// # fn get_username_field() -> &'static str { "email" }
123 /// # fn get_username(&self) -> &str { &self.email }
124 /// # fn password_hash(&self) -> Option<&str> { self.password_hash.as_deref() }
125 /// # fn set_password_hash(&mut self, hash: String) { self.password_hash = Some(hash); }
126 /// # fn last_login(&self) -> Option<DateTime<Utc>> { self.last_login }
127 /// # fn set_last_login(&mut self, time: DateTime<Utc>) { self.last_login = Some(time); }
128 /// # fn is_active(&self) -> bool { self.is_active }
129 /// # }
130 ///
131 /// # #[cfg(feature = "argon2-hasher")]
132 /// # {
133 /// let normalized = MyUser::normalize_username("Åsa@example.com");
134 /// assert_eq!(normalized, "Åsa@example.com"); // NFKC normalized
135 /// # }
136 /// ```
137 fn normalize_username(username: &str) -> String {
138 username.nfkc().collect()
139 }
140
141 /// Sets the password, hashing it first
142 ///
143 /// # Examples
144 ///
145 /// ```
146 /// # use reinhardt_auth::BaseUser;
147 /// # use reinhardt_auth::PasswordHasher;
148 /// # #[cfg(feature = "argon2-hasher")]
149 /// # use reinhardt_auth::Argon2Hasher;
150 /// # use uuid::Uuid;
151 /// # use chrono::{DateTime, Utc};
152 /// # use serde::{Serialize, Deserialize};
153 /// # #[derive(Serialize, Deserialize)]
154 /// # struct MyUser { id: Uuid, email: String, password_hash: Option<String>,
155 /// # last_login: Option<DateTime<Utc>>, is_active: bool }
156 /// # #[cfg(feature = "argon2-hasher")]
157 /// # impl BaseUser for MyUser {
158 /// # type PrimaryKey = Uuid;
159 /// # type Hasher = Argon2Hasher;
160 /// # fn get_username_field() -> &'static str { "email" }
161 /// # fn get_username(&self) -> &str { &self.email }
162 /// # fn password_hash(&self) -> Option<&str> { self.password_hash.as_deref() }
163 /// # fn set_password_hash(&mut self, hash: String) { self.password_hash = Some(hash); }
164 /// # fn last_login(&self) -> Option<DateTime<Utc>> { self.last_login }
165 /// # fn set_last_login(&mut self, time: DateTime<Utc>) { self.last_login = Some(time); }
166 /// # fn is_active(&self) -> bool { self.is_active }
167 /// # }
168 ///
169 /// # #[cfg(feature = "argon2-hasher")]
170 /// # {
171 /// let mut user = MyUser {
172 /// id: Uuid::now_v7(),
173 /// email: "user@example.com".to_string(),
174 /// password_hash: None,
175 /// last_login: None,
176 /// is_active: true,
177 /// };
178 ///
179 /// user.set_password("my_secure_password").unwrap();
180 /// assert!(user.password_hash().is_some());
181 /// # }
182 /// ```
183 fn set_password(&mut self, password: &str) -> Result<(), reinhardt_core::exception::Error> {
184 let hasher = Self::Hasher::default();
185 let hash = hasher.hash(password)?;
186 self.set_password_hash(hash);
187 Ok(())
188 }
189
190 /// Checks if the given password is correct
191 ///
192 /// # Examples
193 ///
194 /// ```
195 /// # use reinhardt_auth::BaseUser;
196 /// # use reinhardt_auth::PasswordHasher;
197 /// # #[cfg(feature = "argon2-hasher")]
198 /// # use reinhardt_auth::Argon2Hasher;
199 /// # use uuid::Uuid;
200 /// # use chrono::{DateTime, Utc};
201 /// # use serde::{Serialize, Deserialize};
202 /// # #[derive(Serialize, Deserialize)]
203 /// # struct MyUser { id: Uuid, email: String, password_hash: Option<String>,
204 /// # last_login: Option<DateTime<Utc>>, is_active: bool }
205 /// # #[cfg(feature = "argon2-hasher")]
206 /// # impl BaseUser for MyUser {
207 /// # type PrimaryKey = Uuid;
208 /// # type Hasher = Argon2Hasher;
209 /// # fn get_username_field() -> &'static str { "email" }
210 /// # fn get_username(&self) -> &str { &self.email }
211 /// # fn password_hash(&self) -> Option<&str> { self.password_hash.as_deref() }
212 /// # fn set_password_hash(&mut self, hash: String) { self.password_hash = Some(hash); }
213 /// # fn last_login(&self) -> Option<DateTime<Utc>> { self.last_login }
214 /// # fn set_last_login(&mut self, time: DateTime<Utc>) { self.last_login = Some(time); }
215 /// # fn is_active(&self) -> bool { self.is_active }
216 /// # }
217 ///
218 /// # #[cfg(feature = "argon2-hasher")]
219 /// # {
220 /// let mut user = MyUser {
221 /// id: Uuid::now_v7(),
222 /// email: "user@example.com".to_string(),
223 /// password_hash: None,
224 /// last_login: None,
225 /// is_active: true,
226 /// };
227 ///
228 /// user.set_password("correct_password").unwrap();
229 ///
230 /// assert!(user.check_password("correct_password").unwrap());
231 /// assert!(!user.check_password("wrong_password").unwrap());
232 /// # }
233 /// ```
234 fn check_password(&self, password: &str) -> Result<bool, reinhardt_core::exception::Error> {
235 // Return false early if password is not usable (e.g., "!" marker)
236 if !self.has_usable_password() {
237 return Ok(false);
238 }
239
240 match self.password_hash() {
241 Some(hash) => {
242 let hasher = Self::Hasher::default();
243 hasher.verify(password, hash)
244 }
245 None => Ok(false),
246 }
247 }
248
249 /// Sets an unusable password (user cannot log in with password)
250 ///
251 /// # Examples
252 ///
253 /// ```no_run
254 /// # use reinhardt_auth::BaseUser;
255 /// # use reinhardt_auth::PasswordHasher;
256 /// # #[cfg(feature = "argon2-hasher")]
257 /// # use reinhardt_auth::Argon2Hasher;
258 /// # use uuid::Uuid;
259 /// # use chrono::{DateTime, Utc};
260 /// # use serde::{Serialize, Deserialize};
261 /// # #[derive(Serialize, Deserialize)]
262 /// # struct MyUser { id: Uuid, email: String, password_hash: Option<String>,
263 /// # last_login: Option<DateTime<Utc>>, is_active: bool }
264 /// # #[cfg(feature = "argon2-hasher")]
265 /// # impl BaseUser for MyUser {
266 /// # type PrimaryKey = Uuid;
267 /// # type Hasher = Argon2Hasher;
268 /// # fn get_username_field() -> &'static str { "email" }
269 /// # fn get_username(&self) -> &str { &self.email }
270 /// # fn password_hash(&self) -> Option<&str> { self.password_hash.as_deref() }
271 /// # fn set_password_hash(&mut self, hash: String) { self.password_hash = Some(hash); }
272 /// # fn last_login(&self) -> Option<DateTime<Utc>> { self.last_login }
273 /// # fn set_last_login(&mut self, time: DateTime<Utc>) { self.last_login = Some(time); }
274 /// # fn is_active(&self) -> bool { self.is_active }
275 /// # }
276 ///
277 /// let mut user = MyUser {
278 /// id: Uuid::now_v7(),
279 /// email: "user@example.com".to_string(),
280 /// password_hash: None,
281 /// last_login: None,
282 /// is_active: true,
283 /// };
284 ///
285 /// user.set_unusable_password();
286 /// assert!(!user.has_usable_password());
287 /// ```
288 fn set_unusable_password(&mut self) {
289 self.set_password_hash("!".to_string());
290 }
291
292 /// Returns whether the user has a usable password
293 ///
294 /// # Examples
295 ///
296 /// ```
297 /// # use reinhardt_auth::BaseUser;
298 /// # use reinhardt_auth::PasswordHasher;
299 /// # #[cfg(feature = "argon2-hasher")]
300 /// # use reinhardt_auth::Argon2Hasher;
301 /// # use uuid::Uuid;
302 /// # use chrono::{DateTime, Utc};
303 /// # use serde::{Serialize, Deserialize};
304 /// # #[derive(Serialize, Deserialize)]
305 /// # struct MyUser { id: Uuid, email: String, password_hash: Option<String>,
306 /// # last_login: Option<DateTime<Utc>>, is_active: bool }
307 /// # #[cfg(feature = "argon2-hasher")]
308 /// # impl BaseUser for MyUser {
309 /// # type PrimaryKey = Uuid;
310 /// # type Hasher = Argon2Hasher;
311 /// # fn get_username_field() -> &'static str { "email" }
312 /// # fn get_username(&self) -> &str { &self.email }
313 /// # fn password_hash(&self) -> Option<&str> { self.password_hash.as_deref() }
314 /// # fn set_password_hash(&mut self, hash: String) { self.password_hash = Some(hash); }
315 /// # fn last_login(&self) -> Option<DateTime<Utc>> { self.last_login }
316 /// # fn set_last_login(&mut self, time: DateTime<Utc>) { self.last_login = Some(time); }
317 /// # fn is_active(&self) -> bool { self.is_active }
318 /// # }
319 ///
320 /// # #[cfg(feature = "argon2-hasher")]
321 /// # {
322 /// let mut user = MyUser {
323 /// id: Uuid::now_v7(),
324 /// email: "user@example.com".to_string(),
325 /// password_hash: None,
326 /// last_login: None,
327 /// is_active: true,
328 /// };
329 ///
330 /// assert!(!user.has_usable_password());
331 ///
332 /// user.set_password("password123").unwrap();
333 /// assert!(user.has_usable_password());
334 ///
335 /// user.set_unusable_password();
336 /// assert!(!user.has_usable_password());
337 /// # }
338 /// ```
339 fn has_usable_password(&self) -> bool {
340 match self.password_hash() {
341 Some(hash) => !hash.is_empty() && hash != "!",
342 None => false,
343 }
344 }
345
346 /// Returns a hash of the session authentication credentials
347 ///
348 /// Uses HMAC-SHA256 with the provided secret as key material combined with the
349 /// password hash. The secret should be a server-side secret (e.g., `SECRET_KEY`
350 /// from settings) to prevent session forgery.
351 ///
352 /// # Arguments
353 ///
354 /// * `secret` - A server-side secret used as HMAC key material. This should be
355 /// the application's `SECRET_KEY` or equivalent cryptographic secret.
356 ///
357 /// # Examples
358 ///
359 /// ```
360 /// # use reinhardt_auth::BaseUser;
361 /// # use reinhardt_auth::PasswordHasher;
362 /// # #[cfg(feature = "argon2-hasher")]
363 /// # use reinhardt_auth::Argon2Hasher;
364 /// # use uuid::Uuid;
365 /// # use chrono::{DateTime, Utc};
366 /// # use serde::{Serialize, Deserialize};
367 /// # #[derive(Serialize, Deserialize)]
368 /// # struct MyUser { id: Uuid, email: String, password_hash: Option<String>,
369 /// # last_login: Option<DateTime<Utc>>, is_active: bool }
370 /// # #[cfg(feature = "argon2-hasher")]
371 /// # impl BaseUser for MyUser {
372 /// # type PrimaryKey = Uuid;
373 /// # type Hasher = Argon2Hasher;
374 /// # fn get_username_field() -> &'static str { "email" }
375 /// # fn get_username(&self) -> &str { &self.email }
376 /// # fn password_hash(&self) -> Option<&str> { self.password_hash.as_deref() }
377 /// # fn set_password_hash(&mut self, hash: String) { self.password_hash = Some(hash); }
378 /// # fn last_login(&self) -> Option<DateTime<Utc>> { self.last_login }
379 /// # fn set_last_login(&mut self, time: DateTime<Utc>) { self.last_login = Some(time); }
380 /// # fn is_active(&self) -> bool { self.is_active }
381 /// # }
382 ///
383 /// # #[cfg(feature = "argon2-hasher")]
384 /// # {
385 /// let mut user = MyUser {
386 /// id: Uuid::now_v7(),
387 /// email: "user@example.com".to_string(),
388 /// password_hash: None,
389 /// last_login: None,
390 /// is_active: true,
391 /// };
392 ///
393 /// let secret = "my-server-secret-key";
394 /// user.set_password("password123").unwrap();
395 /// let hash1 = user.get_session_auth_hash(secret);
396 ///
397 /// user.set_password("new_password").unwrap();
398 /// let hash2 = user.get_session_auth_hash(secret);
399 ///
400 /// assert_ne!(hash1, hash2); // Hash changes when password changes
401 /// # }
402 /// ```
403 fn get_session_auth_hash(&self, secret: &str) -> String {
404 use hmac::{Hmac, Mac};
405 use sha2::Sha256;
406
407 let password_hash = self.password_hash().unwrap_or("");
408 // Derive HMAC key from the server secret combined with a domain separator
409 let key = format!("reinhardt.auth.session_hash:{}", secret);
410
411 let mut mac =
412 Hmac::<Sha256>::new_from_slice(key.as_bytes()).expect("HMAC can take key of any size");
413 mac.update(password_hash.as_bytes());
414
415 hex::encode(mac.finalize().into_bytes())
416 }
417}