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