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;