Skip to main content

reinhardt_http/
auth_state.rs

1//! Authentication state stored in request extensions.
2//!
3//! This module provides [`AuthState`], a helper struct that stores
4//! authentication information in request extensions.
5//!
6//! `AuthState` uses a private validation marker to prevent external construction
7//! via struct literal syntax. Only the provided constructors
8//! ([`AuthState::authenticated`], [`AuthState::anonymous`], [`AuthState::from_extensions`])
9//! can create valid instances, preventing type collision attacks where
10//! malicious code could insert a spoofed auth state into request extensions.
11
12use crate::Extensions;
13
14/// Private marker to validate that an `AuthState` was created through
15/// official constructors, not through external struct literal construction.
16#[derive(Clone, Debug, PartialEq, Eq)]
17struct AuthStateMarker;
18
19/// Helper struct to store authentication state in request extensions.
20///
21/// This struct is used by authentication middleware to communicate
22/// the authenticated user's information to downstream handlers.
23///
24/// The struct contains a private field to prevent external construction
25/// via struct literal syntax. Use the provided constructors instead.
26///
27/// # Security Note
28///
29/// If this state is serialized and sent to client-side code (e.g., in
30/// a WASM SPA), the permission checks (`is_authenticated()`,
31/// `is_admin()`, `is_active()`) should only be used for **UI display
32/// purposes** (showing/hiding elements). An attacker can modify
33/// client-side state, so all authorization decisions must be enforced
34/// server-side through authentication middleware and permission
35/// classes (see `reinhardt-auth`).
36///
37/// # Example
38///
39/// ```rust,no_run
40/// # use reinhardt_http::AuthState;
41/// # struct Request { extensions: Extensions }
42/// # struct Extensions;
43/// # impl Extensions {
44/// #     fn insert<T>(&mut self, _value: T) {}
45/// #     fn get<T>(&self) -> Option<T> { None }
46/// # }
47/// # let mut request = Request { extensions: Extensions };
48/// // In middleware (after authentication)
49/// request.extensions.insert(AuthState::authenticated("123", false, true));
50///
51/// // In handler (via CurrentUser or directly)
52/// let auth_state: Option<AuthState> = request.extensions.get();
53/// ```
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct AuthState {
56	/// The authenticated user's ID as a string.
57	///
58	/// This is typically a UUID or database primary key serialized to string.
59	user_id: String,
60
61	/// Whether the user is authenticated.
62	is_authenticated: bool,
63
64	/// Whether the user has admin/superuser privileges.
65	is_admin: bool,
66
67	/// Whether the user's account is active.
68	is_active: bool,
69
70	/// Private validation marker to prevent external construction.
71	_marker: AuthStateMarker,
72}
73
74impl AuthState {
75	/// Creates a new authenticated state.
76	///
77	/// # Arguments
78	///
79	/// * `user_id` - The authenticated user's ID
80	/// * `is_admin` - Whether the user has admin privileges
81	/// * `is_active` - Whether the user's account is active
82	pub fn authenticated(user_id: impl Into<String>, is_admin: bool, is_active: bool) -> Self {
83		Self {
84			user_id: user_id.into(),
85			is_authenticated: true,
86			is_admin,
87			is_active,
88			_marker: AuthStateMarker,
89		}
90	}
91
92	/// Creates an anonymous (unauthenticated) state.
93	pub fn anonymous() -> Self {
94		Self {
95			user_id: String::new(),
96			is_authenticated: false,
97			is_admin: false,
98			is_active: false,
99			_marker: AuthStateMarker,
100		}
101	}
102
103	/// Create auth state from request extensions.
104	///
105	/// This method first attempts to retrieve an `AuthState` object that was
106	/// inserted directly into extensions (e.g., by custom middleware). If no
107	/// `AuthState` object is found, it falls back to reconstructing one from
108	/// individual `String` (user_id) and `bool` (is_authenticated) entries
109	/// stored in extensions by legacy middleware. Note that the fallback path
110	/// sets `is_admin` and `is_active` to `false` since those values are not
111	/// available as individual extension entries.
112	///
113	/// # Returns
114	///
115	/// Returns `Some(AuthState)` if an `AuthState` object is found or if both
116	/// user_id and is_authenticated individual entries exist, `None` otherwise.
117	pub fn from_extensions(extensions: &Extensions) -> Option<Self> {
118		// Primary: try to get AuthState object directly
119		if let Some(state) = extensions.get::<AuthState>() {
120			return Some(state);
121		}
122		// Fallback: reconstruct from individual extension entries (backward compatibility)
123		Some(Self {
124			user_id: extensions.get::<String>()?,
125			is_authenticated: extensions.get::<bool>()?,
126			is_admin: false,
127			is_active: false,
128			_marker: AuthStateMarker,
129		})
130	}
131
132	/// Get the authenticated user's ID.
133	pub fn user_id(&self) -> &str {
134		&self.user_id
135	}
136
137	/// Check if the user is authenticated.
138	pub fn is_authenticated(&self) -> bool {
139		self.is_authenticated
140	}
141
142	/// Check if the user has admin privileges.
143	pub fn is_admin(&self) -> bool {
144		self.is_admin
145	}
146
147	/// Check if the user's account is active.
148	pub fn is_active(&self) -> bool {
149		self.is_active
150	}
151
152	/// Check if user is anonymous (not authenticated).
153	pub fn is_anonymous(&self) -> bool {
154		!self.is_authenticated
155	}
156}
157
158#[cfg(test)]
159mod tests {
160	use super::*;
161	use rstest::rstest;
162
163	#[test]
164	fn test_authenticated() {
165		let state = AuthState::authenticated("user-123", true, true);
166
167		assert_eq!(state.user_id(), "user-123");
168		assert!(state.is_authenticated());
169		assert!(state.is_admin());
170		assert!(state.is_active());
171	}
172
173	#[test]
174	fn test_anonymous() {
175		let state = AuthState::anonymous();
176
177		assert!(state.user_id().is_empty());
178		assert!(!state.is_authenticated());
179		assert!(!state.is_admin());
180		assert!(!state.is_active());
181	}
182
183	#[rstest]
184	fn test_from_extensions_with_authstate_object() {
185		// Arrange
186		let extensions = Extensions::new();
187		let state = AuthState::authenticated("user-456", true, true);
188		extensions.insert(state.clone());
189
190		// Act
191		let result = AuthState::from_extensions(&extensions);
192
193		// Assert
194		assert_eq!(result, Some(state));
195		let retrieved = result.unwrap();
196		assert_eq!(retrieved.user_id(), "user-456");
197		assert!(retrieved.is_authenticated());
198		assert!(retrieved.is_admin());
199		assert!(retrieved.is_active());
200	}
201
202	#[rstest]
203	fn test_from_extensions_with_individual_values() {
204		// Arrange
205		let extensions = Extensions::new();
206		extensions.insert("user-789".to_string());
207		extensions.insert(true);
208
209		// Act
210		let result = AuthState::from_extensions(&extensions);
211
212		// Assert
213		assert!(result.is_some());
214		let retrieved = result.unwrap();
215		assert_eq!(retrieved.user_id(), "user-789");
216		assert!(retrieved.is_authenticated());
217		assert!(!retrieved.is_admin());
218		assert!(!retrieved.is_active());
219	}
220
221	#[rstest]
222	fn test_from_extensions_empty() {
223		// Arrange
224		let extensions = Extensions::new();
225
226		// Act
227		let result = AuthState::from_extensions(&extensions);
228
229		// Assert
230		assert_eq!(result, None);
231	}
232
233	#[rstest]
234	fn test_from_extensions_preserves_admin_and_active() {
235		// Arrange
236		let extensions = Extensions::new();
237		let state = AuthState::authenticated("admin-user", true, true);
238		extensions.insert(state);
239
240		// Act
241		let result = AuthState::from_extensions(&extensions);
242
243		// Assert
244		let retrieved = result.unwrap();
245		assert_eq!(retrieved.user_id(), "admin-user");
246		assert!(retrieved.is_authenticated());
247		assert!(retrieved.is_admin());
248		assert!(retrieved.is_active());
249	}
250}