Skip to main content

oauth2_passkey/passkey/main/
types.rs

1use ciborium::value::Value as CborValue;
2use ring::digest;
3use serde::{Deserialize, Serialize};
4
5use crate::passkey::{
6    config::{ORIGIN, PASSKEY_RP_ID, PASSKEY_USER_VERIFICATION},
7    errors::PasskeyError,
8    types::PublicKeyCredentialUserEntity,
9};
10use crate::utils::base64url_decode;
11
12/// Options for initiating a WebAuthn authentication request.
13///
14/// This structure contains the parameters needed to create a credential request
15/// to authenticate a user with a previously registered passkey. It follows the
16/// WebAuthn specification format for navigator.credentials.get() options.
17///
18/// The browser uses these options to prompt the user for their passkey and
19/// generate a signed authentication assertion.
20#[derive(Serialize, Debug)]
21#[serde(rename_all = "camelCase")]
22pub struct AuthenticationOptions {
23    pub(super) challenge: String,
24    pub(super) timeout: u32,
25    pub(super) rp_id: String,
26    pub(super) allow_credentials: Vec<AllowCredential>,
27    pub(super) user_verification: String,
28    pub(super) auth_id: String,
29}
30
31#[derive(Serialize, Debug)]
32pub(super) struct AllowCredential {
33    pub(super) type_: String,
34    pub(super) id: String,
35}
36
37#[derive(Serialize, Debug, Clone, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub(super) struct AuthenticatorSelection {
40    /// `None` omits the field from JSON, which per WebAuthn spec lets the
41    /// browser accept either `platform` or `cross-platform` authenticators.
42    /// `Some("platform")` / `Some("cross-platform")` restrict to that type.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub(super) authenticator_attachment: Option<String>,
45    pub(super) resident_key: String,
46    pub(super) user_verification: String,
47    pub(super) require_resident_key: bool,
48}
49
50/// Response from the authenticator during a WebAuthn authentication flow.
51///
52/// This structure contains the data returned from the browser after a successful
53/// credential authentication request. It includes the credential ID and the authentication
54/// assertion containing the signature that proves possession of the private key.
55///
56/// The server uses this data to verify the user's identity and complete the authentication.
57#[allow(unused)]
58#[derive(Deserialize, Debug)]
59pub struct AuthenticatorResponse {
60    pub(super) id: String,
61    raw_id: String,
62    pub(super) response: AuthenticatorAssertionResponse,
63    authenticator_attachment: Option<String>,
64    pub(super) auth_id: String,
65}
66
67impl AuthenticatorResponse {
68    /// Get the credential ID from the authenticator response
69    pub(crate) fn credential_id(&self) -> &str {
70        &self.id
71    }
72
73    #[cfg(test)]
74    pub(super) fn new_for_test(
75        id: String,
76        response: AuthenticatorAssertionResponse,
77        auth_id: String,
78    ) -> Self {
79        Self {
80            id,
81            raw_id: "test_raw_id".to_string(),
82            response,
83            authenticator_attachment: None,
84            auth_id,
85        }
86    }
87}
88
89#[derive(Deserialize, Debug)]
90pub(super) struct AuthenticatorAssertionResponse {
91    pub(super) client_data_json: String,
92    pub(super) authenticator_data: String,
93    pub(super) signature: String,
94    pub(super) user_handle: Option<String>,
95}
96
97#[derive(Serialize, Debug)]
98pub(super) struct PubKeyCredParam {
99    #[serde(rename = "type")]
100    pub(super) type_: String,
101    pub(super) alg: i32,
102}
103
104/// Options for initiating a WebAuthn registration request.
105///
106/// This structure contains the parameters needed to create a new passkey credential
107/// for a user. It follows the WebAuthn specification format for navigator.credentials.create()
108/// options.
109///
110/// The browser uses these options to prompt the user to create a new passkey using
111/// their authenticator (platform or cross-platform).
112#[derive(Serialize, Debug)]
113#[serde(rename_all = "camelCase")]
114pub struct RegistrationOptions {
115    pub(super) challenge: String,
116    pub(super) rp_id: String,
117    pub(super) rp: RelyingParty,
118    pub(super) user: PublicKeyCredentialUserEntity,
119    pub(super) pub_key_cred_params: Vec<PubKeyCredParam>,
120    pub(super) authenticator_selection: AuthenticatorSelection,
121    pub(super) timeout: u32,
122    pub(super) attestation: String,
123    #[serde(skip_serializing_if = "Vec::is_empty")]
124    pub(super) exclude_credentials: Vec<ExcludeCredentialDescriptor>,
125}
126
127/// Descriptor for a credential to exclude during registration.
128///
129/// Used in `excludeCredentials` to prevent duplicate credential creation
130/// on the same authenticator.
131#[derive(Serialize, Debug)]
132pub(super) struct ExcludeCredentialDescriptor {
133    #[serde(rename = "type")]
134    pub(super) type_: String,
135    pub(super) id: String,
136}
137
138#[derive(Serialize, Debug)]
139pub(super) struct RelyingParty {
140    pub(super) name: String,
141    pub(super) id: String,
142}
143
144#[allow(unused)]
145/// Credential data received during WebAuthn registration.
146///
147/// This structure represents the data returned from the browser after a successful
148/// credential creation request. It includes the credential ID and the attestation
149/// information that allows the server to verify the authenticator's properties
150/// and the newly generated credential.
151///
152/// This is used in the registration flow to create and store a new passkey credential.
153#[derive(Deserialize, Debug)]
154pub struct RegisterCredential {
155    pub(super) id: String,
156    pub(super) raw_id: String,
157    pub(super) response: AuthenticatorAttestationResponse,
158    #[serde(rename = "type")]
159    pub(super) type_: String,
160    pub(super) user_handle: Option<String>,
161}
162
163impl RegisterCredential {
164    /// Attempts to retrieve the user fields (name, display_name) from stored registration data
165    /// If the stored options are no longer available, falls back to default values
166    pub(crate) async fn get_registration_user_fields(&self) -> (String, String) {
167        // Try to get the stored options if user_handle exists
168        if let Some(handle) = &self.user_handle {
169            let challenge_type = crate::passkey::types::ChallengeType::registration();
170            let challenge_id = match crate::passkey::types::ChallengeId::new(handle.clone()) {
171                Ok(id) => id,
172                Err(_) => {
173                    tracing::warn!("Invalid challenge ID format, using defaults");
174                    return ("Passkey User".to_string(), "Passkey User".to_string());
175                }
176            };
177            match super::challenge::get_and_validate_options(&challenge_type, &challenge_id).await {
178                Ok(stored_options) => (stored_options.user.name, stored_options.user.display_name),
179                Err(e) => {
180                    tracing::warn!("Failed to get stored user: {}", e);
181                    ("Passkey User".to_string(), "Passkey User".to_string())
182                }
183            }
184        } else {
185            // Fall back to default if user_handle is None
186            ("Passkey User".to_string(), "Passkey User".to_string())
187        }
188    }
189}
190
191#[derive(Deserialize, Debug)]
192pub(super) struct AuthenticatorAttestationResponse {
193    pub(super) client_data_json: String,
194    pub(super) attestation_object: String,
195}
196
197#[derive(Debug)]
198pub(super) struct AttestationObject {
199    pub(super) fmt: String,
200    pub(super) auth_data: Vec<u8>,
201    pub(super) att_stmt: Vec<(CborValue, CborValue)>,
202}
203
204#[derive(Debug)]
205pub(super) struct ParsedClientData {
206    pub(super) challenge: String,
207    pub(super) origin: String,
208    pub(super) type_: String,
209    pub(super) raw_data: Vec<u8>,
210}
211
212impl ParsedClientData {
213    pub(super) fn from_base64(client_data_json: &str) -> Result<Self, PasskeyError> {
214        let raw_data = base64url_decode(client_data_json)
215            .map_err(|e| PasskeyError::Format(format!("Failed to decode: {e}")))?;
216
217        let data_str = String::from_utf8(raw_data.clone())
218            .map_err(|e| PasskeyError::Format(format!("Invalid UTF-8: {e}")))?;
219
220        let data: serde_json::Value = serde_json::from_str(&data_str)
221            .map_err(|e| PasskeyError::Format(format!("Invalid JSON: {e}")))?;
222
223        let challenge_str = data["challenge"]
224            .as_str()
225            .ok_or_else(|| PasskeyError::ClientData("Missing challenge".into()))?;
226
227        Ok(Self {
228            challenge: challenge_str.to_string(),
229            origin: data["origin"]
230                .as_str()
231                .ok_or_else(|| PasskeyError::ClientData("Missing origin".into()))?
232                .to_string(),
233            type_: data["type"]
234                .as_str()
235                .ok_or_else(|| PasskeyError::ClientData("Missing type".into()))?
236                .to_string(),
237            raw_data,
238        })
239    }
240
241    pub(super) fn verify(&self, stored_challenge: &str) -> Result<(), PasskeyError> {
242        // Verify challenge
243        if self.challenge != stored_challenge {
244            return Err(PasskeyError::Challenge(
245                "Challenge mismatch. For more details, run with RUST_LOG=debug".into(),
246            ));
247        }
248
249        // Verify origin
250        if self.origin != *ORIGIN {
251            return Err(PasskeyError::ClientData(format!(
252                "Invalid origin. Expected: {}, Got: {}",
253                *ORIGIN, self.origin
254            )));
255        }
256
257        // Verify type for authentication
258        if self.type_ != "webauthn.get" {
259            return Err(PasskeyError::ClientData(format!(
260                "Invalid type. Expected 'webauthn.get', Got: {}",
261                self.type_
262            )));
263        }
264
265        Ok(())
266    }
267}
268
269/// AuthenticatorData structure as defined in WebAuthn spec Level 2
270/// https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data
271#[derive(Debug)]
272pub(super) struct AuthenticatorData {
273    /// SHA-256 hash of the RP ID (32 bytes)
274    pub(super) rp_id_hash: Vec<u8>,
275
276    /// Flags (1 byte) indicating various attributes:
277    /// - Bit 0: User Present (UP)
278    /// - Bit 2: User Verified (UV)
279    /// - Bit 3: Backup Eligibility (BE) - Indicates if credential is discoverable
280    /// - Bit 4: Backup State (BS)
281    /// - Bit 6: Attested Credential Data Present (AT)
282    /// - Bit 7: Extension Data Present (ED)
283    pub(super) flags: u8,
284
285    /// Signature counter (4 bytes), 32-bit unsigned big-endian integer
286    pub(super) counter: u32,
287
288    /// Raw authenticator data for verification
289    pub(super) raw_data: Vec<u8>,
290}
291
292/// Flags for AuthenticatorData as defined in WebAuthn spec Level 2
293mod auth_data_flags {
294    /// User Present (UP) - Bit 0
295    pub(super) const UP: u8 = 1 << 0;
296    /// User Verified (UV) - Bit 2
297    pub(super) const UV: u8 = 1 << 2;
298    /// Backup Eligibility (BE) - Bit 3 - Indicates if credential is discoverable
299    pub(super) const BE: u8 = 1 << 3;
300    /// Backup State (BS) - Bit 4
301    pub(super) const BS: u8 = 1 << 4;
302    /// Attested Credential Data Present - Bit 6
303    pub(super) const AT: u8 = 1 << 6;
304    /// Extension Data Present - Bit 7
305    pub(super) const ED: u8 = 1 << 7;
306}
307
308impl AuthenticatorData {
309    /// Parse base64url-encoded authenticator data
310    /// Format (minimum 37 bytes):
311    /// - RP ID Hash (32 bytes)
312    /// - Flags (1 byte)
313    /// - Counter (4 bytes)
314    /// - Optional: Attested Credential Data
315    /// - Optional: Extensions
316    pub(super) fn from_base64(auth_data: &str) -> Result<Self, PasskeyError> {
317        let data = base64url_decode(auth_data)
318            .map_err(|e| PasskeyError::Format(format!("Failed to decode: {e}")))?;
319
320        if data.len() < 37 {
321            return Err(PasskeyError::AuthenticatorData(
322                "Authenticator data too short. For more details, run with RUST_LOG=debug".into(),
323            ));
324        }
325
326        Ok(Self {
327            rp_id_hash: data[..32].to_vec(),
328            flags: data[32],
329            counter: u32::from_be_bytes([data[33], data[34], data[35], data[36]]),
330            raw_data: data,
331        })
332    }
333
334    /// Check if user was present during the authentication
335    pub(super) fn is_user_present(&self) -> bool {
336        (self.flags & auth_data_flags::UP) != 0
337    }
338
339    /// Check if user was verified by the authenticator
340    pub(super) fn is_user_verified(&self) -> bool {
341        (self.flags & auth_data_flags::UV) != 0
342    }
343
344    /// Check if this is a discoverable credential (previously known as resident key)
345    pub(super) fn is_discoverable(&self) -> bool {
346        (self.flags & auth_data_flags::BE) != 0
347    }
348
349    /// Check if this credential is backed up
350    pub(super) fn is_backed_up(&self) -> bool {
351        (self.flags & auth_data_flags::BS) != 0
352    }
353
354    /// Check if attested credential data is present
355    pub(super) fn has_attested_credential_data(&self) -> bool {
356        (self.flags & auth_data_flags::AT) != 0
357    }
358
359    /// Check if extension data is present
360    pub(super) fn has_extension_data(&self) -> bool {
361        (self.flags & auth_data_flags::ED) != 0
362    }
363
364    /// Verify the authenticator data
365    pub(super) fn verify(&self) -> Result<(), PasskeyError> {
366        // Verify rpIdHash matches SHA-256 hash of rpId
367        let expected_hash = digest::digest(&digest::SHA256, PASSKEY_RP_ID.as_bytes());
368        if self.rp_id_hash != expected_hash.as_ref() {
369            return Err(PasskeyError::AuthenticatorData(format!(
370                "Invalid RP ID hash. Expected: {:?}, Got: {:?}",
371                expected_hash.as_ref(),
372                self.rp_id_hash
373            )));
374        }
375
376        // Verify user present flag
377        if !self.is_user_present() {
378            return Err(PasskeyError::Authentication(
379                "User not present. For more details, run with RUST_LOG=debug".into(),
380            ));
381        }
382
383        // Verify user verification if required
384        if *PASSKEY_USER_VERIFICATION == "required" && !self.is_user_verified() {
385            return Err(PasskeyError::AuthenticatorData(format!(
386                "User verification required but flag not set. Flags: {:02x}",
387                self.flags
388            )));
389        }
390
391        tracing::debug!("Authenticator data verification passed");
392        tracing::debug!("User present: {}", self.is_user_present());
393        tracing::debug!("User verified: {}", self.is_user_verified());
394        tracing::debug!("Discoverable credential: {}", self.is_discoverable());
395        tracing::debug!("Backed up: {}", self.is_backed_up());
396        tracing::debug!(
397            "Attested credential data: {}",
398            self.has_attested_credential_data()
399        );
400        tracing::debug!("Extension data: {}", self.has_extension_data());
401
402        Ok(())
403    }
404}
405
406#[derive(Clone, Serialize, Deserialize, Debug)]
407pub(super) struct WebAuthnClientData {
408    #[serde(rename = "type")]
409    pub(super) type_: String,
410    pub(super) challenge: String, // base64url encoded
411    pub(super) origin: String,
412}
413
414#[cfg(test)]
415mod tests;