Skip to main content

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}