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}