oauth2_passkey/session/
types.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::session::errors::SessionError;
5use crate::storage::CacheData;
6use crate::userdb::User as DbUser;
7
8/// User information stored in the session.
9///
10/// This struct represents authenticated user data that is stored in the session
11/// and retrieved during authentication checks. It contains essential user identity
12/// and permission information needed for the application.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct User {
15    /// Unique identifier for the user
16    pub id: String,
17    /// User account name or login identifier
18    pub account: String,
19    /// Display name or label for the user
20    pub label: String,
21    /// Whether the user has administrative privileges
22    pub is_admin: bool,
23    /// Database-assigned sequence number (primary key), None for users not yet persisted
24    pub sequence_number: Option<i64>,
25    /// When the user account was created
26    pub created_at: DateTime<Utc>,
27    /// When the user account was last updated
28    pub updated_at: DateTime<Utc>,
29}
30
31impl From<DbUser> for User {
32    fn from(db_user: DbUser) -> Self {
33        Self {
34            id: db_user.id,
35            account: db_user.account,
36            label: db_user.label,
37            is_admin: db_user.is_admin,
38            sequence_number: db_user.sequence_number,
39            created_at: db_user.created_at,
40            updated_at: db_user.updated_at,
41        }
42    }
43}
44
45impl From<User> for DbUser {
46    fn from(session_user: User) -> Self {
47        Self {
48            id: session_user.id,
49            account: session_user.account,
50            label: session_user.label,
51            is_admin: session_user.is_admin,
52            sequence_number: session_user.sequence_number,
53            created_at: session_user.created_at,
54            updated_at: session_user.updated_at,
55        }
56    }
57}
58
59impl User {
60    /// Determines if the user has administrative privileges.
61    ///
62    /// A user has admin privileges if:
63    /// 1. They have the `is_admin` flag set to true, OR
64    /// 2. They are the first user in the system (sequence_number = 1)
65    ///
66    /// This method provides consistent admin privilege checking across the codebase
67    /// and ensures the first user always has admin access regardless of the is_admin flag.
68    ///
69    /// # Returns
70    /// * `true` if the user has administrative privileges
71    /// * `false` otherwise
72    ///
73    /// # Examples
74    /// ```
75    /// use oauth2_passkey::SessionUser as User;
76    /// use chrono::Utc;
77    ///
78    /// // Regular admin user
79    /// let admin_user = User {
80    ///     id: "user1".to_string(),
81    ///     account: "admin@example.com".to_string(),
82    ///     label: "Admin User".to_string(),
83    ///     is_admin: true,
84    ///     sequence_number: Some(5),
85    ///     created_at: Utc::now(),
86    ///     updated_at: Utc::now(),
87    /// };
88    /// assert!(admin_user.has_admin_privileges());
89    ///
90    /// // First user (always admin)
91    /// let first_user = User {
92    ///     id: "user1".to_string(),
93    ///     account: "first@example.com".to_string(),
94    ///     label: "First User".to_string(),
95    ///     is_admin: false,
96    ///     sequence_number: Some(1),
97    ///     created_at: Utc::now(),
98    ///     updated_at: Utc::now(),
99    /// };
100    /// assert!(first_user.has_admin_privileges());
101    ///
102    /// // Regular user
103    /// let regular_user = User {
104    ///     id: "user1".to_string(),
105    ///     account: "user@example.com".to_string(),
106    ///     label: "Regular User".to_string(),
107    ///     is_admin: false,
108    ///     sequence_number: Some(2),
109    ///     created_at: Utc::now(),
110    ///     updated_at: Utc::now(),
111    /// };
112    /// assert!(!regular_user.has_admin_privileges());
113    /// ```
114    /// IMPORTANT: This logic must stay in sync with DbUser::has_admin_privileges()
115    /// and AuthUser::has_admin_privileges() implementations.
116    pub fn has_admin_privileges(&self) -> bool {
117        self.is_admin || self.sequence_number == Some(1)
118    }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub(super) struct StoredSession {
123    pub(super) user_id: String,
124    pub(super) csrf_token: String,
125    pub(super) expires_at: DateTime<Utc>,
126    pub(super) ttl: u64,
127}
128
129impl From<StoredSession> for CacheData {
130    fn from(data: StoredSession) -> Self {
131        Self {
132            value: serde_json::to_string(&data).expect("Failed to serialize StoredSession"),
133        }
134    }
135}
136
137impl TryFrom<CacheData> for StoredSession {
138    type Error = SessionError;
139
140    fn try_from(data: CacheData) -> Result<Self, Self::Error> {
141        serde_json::from_str(&data.value).map_err(|e| SessionError::Storage(e.to_string()))
142    }
143}
144
145/// CSRF (Cross-Site Request Forgery) token for request validation.
146///
147/// This struct represents a security token that must be included in forms
148/// and state-changing requests to prevent cross-site request forgery attacks.
149/// It's a newtype wrapper around a String to provide type safety and prevent
150/// confusion with other string types.
151#[derive(Debug, Clone)]
152pub struct CsrfToken(String);
153
154/// Indicates whether the CSRF token was verified via an HTTP header.
155///
156/// This is typically set by middleware or other authentication layers that have
157/// already performed CSRF validation. It's used to avoid redundant validation
158/// when multiple layers of authentication checks are applied.
159///
160/// Contains a boolean where `true` means the CSRF token was already verified.
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub struct CsrfHeaderVerified(pub bool);
163
164/// Indicates the overall authentication status of a session.
165///
166/// This is a simple boolean wrapper that indicates whether a user is authenticated.
167/// It's used as a return type from authentication check functions to explicitly
168/// communicate the authentication state.
169///
170/// Contains a boolean where `true` means the user is authenticated.
171#[derive(Clone, Copy, Debug, PartialEq, Eq)]
172pub struct AuthenticationStatus(pub bool);
173
174impl std::fmt::Display for AuthenticationStatus {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        write!(f, "{}", self.0)
177    }
178}
179
180impl std::fmt::Display for CsrfHeaderVerified {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        write!(f, "{}", self.0)
183    }
184}
185
186impl CsrfToken {
187    /// Creates a new CSRF token from a string.
188    ///
189    /// # Arguments
190    /// * `token` - The token string
191    ///
192    /// # Returns
193    /// * A new CsrfToken instance
194    pub fn new(token: String) -> Self {
195        Self(token)
196    }
197
198    /// Returns the token as a string slice.
199    ///
200    /// This method is useful when you need to include the token in a
201    /// response or use it for comparison.
202    ///
203    /// # Returns
204    /// * A string slice containing the token
205    pub fn as_str(&self) -> &str {
206        &self.0
207    }
208}
209
210/// Type-safe wrapper for user identifiers.
211///
212/// This provides compile-time safety to prevent mixing up user IDs with other string types.
213/// It's used in coordination layer functions to ensure type safety when passing user identifiers.
214#[derive(Debug, Clone, PartialEq)]
215pub struct UserId(String);
216
217impl UserId {
218    /// Creates a new UserId from a string with validation.
219    ///
220    /// # Arguments
221    /// * `id` - The user ID string
222    ///
223    /// # Returns
224    /// * `Ok(UserId)` - If the ID is valid
225    /// * `Err(SessionError)` - If the ID is invalid
226    ///
227    /// # Validation Rules
228    /// * Must not be empty
229    /// * Must contain only safe characters (alphanumeric + basic symbols)
230    /// * Must not contain control characters or dangerous sequences
231    pub fn new(id: String) -> Result<Self, crate::session::SessionError> {
232        use crate::session::SessionError;
233
234        // Validate ID is not empty
235        if id.is_empty() {
236            return Err(SessionError::Validation(
237                "User ID cannot be empty".to_string(),
238            ));
239        }
240
241        // Validate ID length (reasonable bounds)
242        if id.len() > 255 {
243            return Err(SessionError::Validation("User ID too long".to_string()));
244        }
245
246        // Validate ID contains only safe characters
247        if !id.chars().all(|c| {
248            c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '@' | '+' | '(' | ')')
249        }) {
250            return Err(SessionError::Validation(
251                "User ID contains invalid characters".to_string(),
252            ));
253        }
254
255        // Check for dangerous sequences
256        if id.contains("..") || id.contains("--") || id.contains("__") {
257            return Err(SessionError::Validation(
258                "User ID contains dangerous character sequences".to_string(),
259            ));
260        }
261
262        Ok(UserId(id))
263    }
264
265    /// Returns the user ID as a string slice.
266    ///
267    /// # Returns
268    /// * A string slice containing the user ID
269    pub fn as_str(&self) -> &str {
270        &self.0
271    }
272}
273
274/// Type-safe wrapper for session identifiers.
275///
276/// This provides compile-time safety to prevent mixing up session IDs with other string types.
277/// It's used in coordination layer functions to ensure type safety when passing session identifiers.
278#[derive(Debug, Clone)]
279pub struct SessionId(String);
280
281impl SessionId {
282    /// Creates a new SessionId from a string with validation.
283    ///
284    /// # Arguments
285    /// * `id` - The session ID string
286    ///
287    /// # Returns
288    /// * `Ok(SessionId)` - If the ID is valid
289    /// * `Err(SessionError)` - If the ID is invalid
290    ///
291    /// # Validation Rules
292    /// * Must not be empty
293    /// * Must contain only safe characters (alphanumeric + URL-safe symbols)
294    /// * Must not contain control characters or whitespace
295    pub fn new(id: String) -> Result<Self, crate::session::SessionError> {
296        use crate::session::SessionError;
297
298        // Validate ID is not empty
299        if id.is_empty() {
300            return Err(SessionError::Validation(
301                "Session ID cannot be empty".to_string(),
302            ));
303        }
304
305        // Validate ID length (session IDs need sufficient entropy)
306        if id.len() < 10 {
307            return Err(SessionError::Validation("Session ID too short".to_string()));
308        }
309
310        if id.len() > 256 {
311            return Err(SessionError::Validation("Session ID too long".to_string()));
312        }
313
314        // Validate ID contains only URL-safe characters (no whitespace)
315        if !id
316            .chars()
317            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '~'))
318        {
319            return Err(SessionError::Validation(
320                "Session ID contains invalid characters".to_string(),
321            ));
322        }
323
324        Ok(SessionId(id))
325    }
326
327    /// Returns the session ID as a string slice.
328    ///
329    /// # Returns
330    /// * A string slice containing the session ID
331    pub fn as_str(&self) -> &str {
332        &self.0
333    }
334}
335
336/// Type-safe wrapper for session cookies.
337///
338/// This provides compile-time safety to prevent mixing up session cookies with other string types.
339/// Session cookies are HTTP cookie values used for user session identification and must be
340/// properly validated to prevent session hijacking and other security issues.
341#[derive(Debug, Clone, PartialEq)]
342pub struct SessionCookie(String);
343
344impl SessionCookie {
345    /// Creates a new SessionCookie from a string with validation.
346    ///
347    /// This constructor validates the session cookie format to ensure it meets
348    /// security requirements for session identification.
349    ///
350    /// # Arguments
351    /// * `cookie` - The session cookie string
352    ///
353    /// # Returns
354    /// * `Ok(SessionCookie)` - If the cookie is valid
355    /// * `Err(SessionError)` - If the cookie is invalid
356    ///
357    /// # Validation Rules
358    /// * Must not be empty
359    /// * Must contain only valid characters (alphanumeric + basic symbols)
360    /// * Must be reasonable length (not too short or too long)
361    pub fn new(cookie: String) -> Result<Self, crate::session::SessionError> {
362        use crate::session::SessionError;
363
364        // Validate cookie is not empty
365        if cookie.is_empty() {
366            return Err(SessionError::Cookie(
367                "Session cookie cannot be empty".to_string(),
368            ));
369        }
370
371        // Validate cookie length (reasonable bounds)
372        if cookie.len() < 10 {
373            return Err(SessionError::Cookie("Session cookie too short".to_string()));
374        }
375
376        if cookie.len() > 1024 {
377            return Err(SessionError::Cookie("Session cookie too long".to_string()));
378        }
379
380        // Validate cookie contains only safe characters
381        // Allow alphanumeric, hyphens, underscores, equals signs, and basic URL-safe characters
382        if !cookie
383            .chars()
384            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '=' | '.' | '+' | '/'))
385        {
386            return Err(SessionError::Cookie(
387                "Session cookie contains invalid characters".to_string(),
388            ));
389        }
390
391        Ok(SessionCookie(cookie))
392    }
393
394    /// Returns the session cookie as a string slice.
395    ///
396    /// # Returns
397    /// * A string slice containing the session cookie
398    pub fn as_str(&self) -> &str {
399        &self.0
400    }
401}
402
403#[cfg(test)]
404mod tests;