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 extracts authentication-related data that was stored
106	/// as individual values in extensions by the authentication middleware.
107	///
108	/// # Returns
109	///
110	/// Returns `Some(AuthState)` if user_id and is_authenticated are found,
111	/// `None` otherwise.
112	pub fn from_extensions(extensions: &Extensions) -> Option<Self> {
113		Some(Self {
114			user_id: extensions.get::<String>()?,
115			is_authenticated: extensions.get::<bool>()?,
116			is_admin: false,
117			is_active: false,
118			_marker: AuthStateMarker,
119		})
120	}
121
122	/// Get the authenticated user's ID.
123	pub fn user_id(&self) -> &str {
124		&self.user_id
125	}
126
127	/// Check if the user is authenticated.
128	pub fn is_authenticated(&self) -> bool {
129		self.is_authenticated
130	}
131
132	/// Check if the user has admin privileges.
133	pub fn is_admin(&self) -> bool {
134		self.is_admin
135	}
136
137	/// Check if the user's account is active.
138	pub fn is_active(&self) -> bool {
139		self.is_active
140	}
141
142	/// Check if user is anonymous (not authenticated).
143	pub fn is_anonymous(&self) -> bool {
144		!self.is_authenticated
145	}
146}
147
148#[cfg(test)]
149mod tests {
150	use super::*;
151
152	#[test]
153	fn test_authenticated() {
154		let state = AuthState::authenticated("user-123", true, true);
155
156		assert_eq!(state.user_id(), "user-123");
157		assert!(state.is_authenticated());
158		assert!(state.is_admin());
159		assert!(state.is_active());
160	}
161
162	#[test]
163	fn test_anonymous() {
164		let state = AuthState::anonymous();
165
166		assert!(state.user_id().is_empty());
167		assert!(!state.is_authenticated());
168		assert!(!state.is_admin());
169		assert!(!state.is_active());
170	}
171}