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}