oauth2_passkey/passkey/
types.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use super::errors::PasskeyError;
5use crate::session::UserId;
6use crate::storage::CacheData;
7
8#[derive(Clone, Serialize, Deserialize, Debug, Default)]
9pub struct PublicKeyCredentialUserEntity {
10    pub user_handle: String,
11    pub name: String,
12    #[serde(rename = "displayName")]
13    pub display_name: String,
14}
15
16#[derive(Clone, Serialize, Deserialize, Debug)]
17pub(super) struct StoredOptions {
18    pub(super) challenge: String,
19    pub(super) user: PublicKeyCredentialUserEntity,
20    pub(super) timestamp: u64,
21    pub(super) ttl: u64,
22}
23
24/// Stored credential information for a WebAuthn/Passkey.
25///
26/// This struct represents a stored passkey credential that can be used for authentication.
27/// It contains all the necessary information to verify subsequent authentications using
28/// the same credential, including the public key, credential ID, and counter value.
29///
30/// The credential is associated with a specific user and includes metadata about when
31/// it was created, updated, and last used.
32#[derive(Clone, Serialize, Deserialize, Debug)]
33pub struct PasskeyCredential {
34    /// Raw credential ID bytes
35    pub credential_id: String,
36    /// User ID associated with this credential (database ID)
37    pub user_id: String,
38    /// Public key bytes for the credential
39    pub public_key: String,
40    /// AAGUID of the authenticator
41    pub aaguid: String,
42    /// Counter value for the credential (used to prevent replay attacks)
43    pub counter: u32,
44    /// User entity information
45    pub user: PublicKeyCredentialUserEntity,
46    /// When the credential was created
47    pub created_at: DateTime<Utc>,
48    /// When the credential was last updated
49    pub updated_at: DateTime<Utc>,
50    /// When the credential was last used
51    pub last_used_at: DateTime<Utc>,
52}
53
54#[derive(Clone, Serialize, Deserialize, Debug)]
55pub(super) struct UserIdCredentialIdStr {
56    pub(super) user_id: String,
57    pub(super) credential_id: String,
58}
59
60#[derive(Clone, Serialize, Deserialize, Debug)]
61pub(super) struct SessionInfo {
62    pub(super) user: crate::session::User,
63}
64
65/// Search field options for credential lookup.
66///
67/// This enum provides various ways to search for passkey credentials in storage,
68/// supporting different lookup strategies based on the available identifier.
69/// Each variant represents a different search parameter type with compile-time type safety.
70#[allow(dead_code)]
71#[derive(Debug)]
72pub enum CredentialSearchField {
73    /// Search by credential ID (type-safe)
74    CredentialId(CredentialId),
75    /// Search by user ID (database ID, type-safe)
76    UserId(UserId),
77    /// Search by user handle (WebAuthn user handle, type-safe)
78    UserHandle(UserHandle),
79    /// Search by username (type-safe)
80    UserName(UserName),
81}
82
83/// Helper functions for cache store operations to improve code reuse and maintainability
84impl From<SessionInfo> for CacheData {
85    fn from(data: SessionInfo) -> Self {
86        Self {
87            value: serde_json::to_string(&data).expect("Failed to serialize SessionInfo"),
88        }
89    }
90}
91
92impl TryFrom<CacheData> for SessionInfo {
93    type Error = PasskeyError;
94
95    fn try_from(data: CacheData) -> Result<Self, Self::Error> {
96        serde_json::from_str(&data.value).map_err(|e| PasskeyError::Storage(e.to_string()))
97    }
98}
99
100impl From<StoredOptions> for CacheData {
101    fn from(data: StoredOptions) -> Self {
102        Self {
103            value: serde_json::to_string(&data).expect("Failed to serialize StoredOptions"),
104        }
105    }
106}
107
108impl TryFrom<CacheData> for StoredOptions {
109    type Error = PasskeyError;
110
111    fn try_from(data: CacheData) -> Result<Self, Self::Error> {
112        serde_json::from_str(&data.value).map_err(|e| PasskeyError::Storage(e.to_string()))
113    }
114}
115
116/// Type-safe wrapper for credential identifiers.
117///
118/// This provides compile-time safety to prevent mixing up credential IDs with other string types.
119/// It's used in passkey coordination functions to ensure type safety when passing credential identifiers.
120#[derive(Debug, Clone, PartialEq)]
121pub struct CredentialId(String);
122
123impl CredentialId {
124    /// Creates a new CredentialId from a string with validation.
125    ///
126    /// # Arguments
127    /// * `id` - The credential ID string
128    ///
129    /// # Returns
130    /// * `Ok(CredentialId)` - If the ID is valid
131    /// * `Err(PasskeyError)` - If the ID is invalid
132    ///
133    /// # Validation Rules
134    /// * Must not be empty
135    /// * Must contain only safe characters (alphanumeric + URL-safe symbols)
136    /// * Must not contain control characters or dangerous sequences
137    pub fn new(id: String) -> Result<Self, crate::passkey::PasskeyError> {
138        use crate::passkey::PasskeyError;
139
140        // Validate ID is not empty
141        if id.is_empty() {
142            return Err(PasskeyError::Validation(
143                "Credential ID cannot be empty".to_string(),
144            ));
145        }
146
147        // Validate ID length (credential IDs need sufficient entropy and can be substantial)
148        if id.len() < 10 {
149            return Err(PasskeyError::Validation(
150                "Credential ID too short".to_string(),
151            ));
152        }
153
154        if id.len() > 1024 {
155            return Err(PasskeyError::Validation(
156                "Credential ID too long".to_string(),
157            ));
158        }
159
160        // Validate ID contains only URL-safe characters
161        if !id.chars().all(|c| {
162            c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '~' | '=' | '+' | '/')
163        }) {
164            return Err(PasskeyError::Validation(
165                "Credential ID contains invalid characters".to_string(),
166            ));
167        }
168
169        Ok(CredentialId(id))
170    }
171
172    /// Returns the credential ID as a string slice.
173    ///
174    /// # Returns
175    /// * A string slice containing the credential ID
176    pub fn as_str(&self) -> &str {
177        &self.0
178    }
179}
180
181/// Type-safe wrapper for WebAuthn user handles.
182///
183/// This provides compile-time safety to prevent mixing up user handles with other string types.
184/// User handles are WebAuthn-specific identifiers that may differ from usernames or display names.
185#[derive(Debug, Clone, PartialEq)]
186pub struct UserHandle(String);
187
188impl UserHandle {
189    /// Creates a new UserHandle from a string.
190    ///
191    /// # Arguments
192    /// * `handle` - The user handle string
193    ///
194    /// # Returns
195    /// * A new UserHandle instance
196    pub fn new(handle: String) -> Self {
197        Self(handle)
198    }
199
200    /// Returns the user handle as a string slice.
201    ///
202    /// # Returns
203    /// * A string slice containing the user handle
204    pub fn as_str(&self) -> &str {
205        &self.0
206    }
207}
208
209/// Type-safe wrapper for usernames.
210///
211/// This provides compile-time safety to prevent mixing up usernames with other string types.
212/// Usernames are user-facing identifiers that may be used for display or authentication.
213#[derive(Debug, Clone, PartialEq)]
214pub struct UserName(String);
215
216impl UserName {
217    /// Creates a new UserName from a string with validation.
218    ///
219    /// # Arguments
220    /// * `name` - The username string
221    ///
222    /// # Returns
223    /// * `Ok(UserName)` - If the name is valid
224    /// * `Err(PasskeyError)` - If the name is invalid
225    ///
226    /// # Validation Rules
227    /// * Must not be empty
228    /// * Must not contain dangerous sequences
229    /// * Must not consist only of whitespace
230    pub fn new(name: String) -> Result<Self, crate::passkey::PasskeyError> {
231        use crate::passkey::PasskeyError;
232
233        // Validate name is not empty
234        if name.is_empty() {
235            return Err(PasskeyError::Validation(
236                "Username cannot be empty".to_string(),
237            ));
238        }
239
240        // Validate name length (WebAuthn username limits)
241        if name.len() > 64 {
242            return Err(PasskeyError::Validation("Username too long".to_string()));
243        }
244
245        // Validate name doesn't consist only of whitespace
246        if name.trim().is_empty() {
247            return Err(PasskeyError::Validation(
248                "Username cannot consist only of whitespace".to_string(),
249            ));
250        }
251
252        // Check for dangerous sequences
253        if name.contains("..") || name.contains("--") || name.contains("__") {
254            return Err(PasskeyError::Validation(
255                "Username contains dangerous character sequences".to_string(),
256            ));
257        }
258
259        Ok(UserName(name))
260    }
261
262    /// Returns the username as a string slice.
263    ///
264    /// # Returns
265    /// * A string slice containing the username
266    pub fn as_str(&self) -> &str {
267        &self.0
268    }
269}
270
271/// Type-safe wrapper for WebAuthn challenge types.
272///
273/// This provides compile-time safety to prevent mixing up challenge types with other string types.
274/// Challenge types identify the kind of WebAuthn operation (registration, authentication) and are
275/// used as cache prefixes for storing challenge data.
276#[derive(Debug, Clone, PartialEq)]
277pub struct ChallengeType(String);
278
279impl ChallengeType {
280    /// Creates a new ChallengeType from a string with validation.
281    ///
282    /// This constructor validates the challenge type to ensure it meets
283    /// requirements for cache operations and WebAuthn flow identification.
284    ///
285    /// # Arguments
286    /// * `challenge_type` - The challenge type string
287    ///
288    /// # Returns
289    /// * `Ok(ChallengeType)` - If the challenge type is valid
290    /// * `Err(PasskeyError)` - If the challenge type is invalid
291    ///
292    /// # Validation Rules
293    /// * Must not be empty
294    /// * Must contain only alphanumeric characters and underscores
295    /// * Must be reasonable length
296    pub fn new(challenge_type: String) -> Result<Self, super::errors::PasskeyError> {
297        use super::errors::PasskeyError;
298
299        // Validate challenge type is not empty
300        if challenge_type.is_empty() {
301            return Err(PasskeyError::Challenge(
302                "Challenge type cannot be empty".to_string(),
303            ));
304        }
305
306        // Validate challenge type length (reasonable bounds)
307        if challenge_type.len() > 64 {
308            return Err(PasskeyError::Challenge(
309                "Challenge type too long".to_string(),
310            ));
311        }
312
313        // Validate challenge type contains only safe characters for cache prefixes
314        if !challenge_type
315            .chars()
316            .all(|c| c.is_ascii_alphanumeric() || c == '_')
317        {
318            return Err(PasskeyError::Challenge(
319                "Challenge type contains invalid characters".to_string(),
320            ));
321        }
322
323        Ok(ChallengeType(challenge_type))
324    }
325
326    /// Returns the challenge type as a string slice.
327    ///
328    /// # Returns
329    /// * A string slice containing the challenge type
330    pub fn as_str(&self) -> &str {
331        &self.0
332    }
333
334    /// Creates a registration challenge type.
335    ///
336    /// # Returns
337    /// * A ChallengeType for registration operations
338    pub fn registration() -> Self {
339        ChallengeType("registration".to_string())
340    }
341
342    /// Creates an authentication challenge type.
343    ///
344    /// # Returns
345    /// * A ChallengeType for authentication operations
346    pub fn authentication() -> Self {
347        ChallengeType("authentication".to_string())
348    }
349}
350
351/// Type-safe wrapper for WebAuthn challenge identifiers.
352///
353/// This provides compile-time safety to prevent mixing up challenge IDs with other string types.
354/// Challenge IDs are unique identifiers for specific WebAuthn challenge instances and are used
355/// as cache keys for storing and retrieving challenge data.
356#[derive(Debug, Clone, PartialEq)]
357pub struct ChallengeId(String);
358
359impl ChallengeId {
360    /// Creates a new ChallengeId from a string with validation.
361    ///
362    /// This constructor validates the challenge ID to ensure it meets
363    /// requirements for cache operations and uniqueness.
364    ///
365    /// # Arguments
366    /// * `id` - The challenge ID string
367    ///
368    /// # Returns
369    /// * `Ok(ChallengeId)` - If the challenge ID is valid
370    /// * `Err(PasskeyError)` - If the challenge ID is invalid
371    ///
372    /// # Validation Rules
373    /// * Must not be empty
374    /// * Must contain only safe characters for cache keys
375    /// * Must be reasonable length
376    pub fn new(id: String) -> Result<Self, super::errors::PasskeyError> {
377        use super::errors::PasskeyError;
378
379        // Validate ID is not empty
380        if id.is_empty() {
381            return Err(PasskeyError::Challenge(
382                "Challenge ID cannot be empty".to_string(),
383            ));
384        }
385
386        // Validate ID length (reasonable bounds)
387        if id.len() < 8 {
388            return Err(PasskeyError::Challenge(
389                "Challenge ID too short".to_string(),
390            ));
391        }
392
393        if id.len() > 256 {
394            return Err(PasskeyError::Challenge("Challenge ID too long".to_string()));
395        }
396
397        // Validate ID contains only safe characters for cache keys
398        if !id
399            .chars()
400            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '+'))
401        {
402            return Err(PasskeyError::Challenge(
403                "Challenge ID contains invalid characters".to_string(),
404            ));
405        }
406
407        Ok(ChallengeId(id))
408    }
409
410    /// Returns the challenge ID as a string slice.
411    ///
412    /// # Returns
413    /// * A string slice containing the challenge ID
414    pub fn as_str(&self) -> &str {
415        &self.0
416    }
417}