oauth2_passkey/passkey/main/
types.rs1use 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#[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 #[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#[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 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#[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#[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#[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 pub(crate) async fn get_registration_user_fields(&self) -> (String, String) {
167 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 ("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 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 if self.origin != *ORIGIN {
251 return Err(PasskeyError::ClientData(format!(
252 "Invalid origin. Expected: {}, Got: {}",
253 *ORIGIN, self.origin
254 )));
255 }
256
257 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#[derive(Debug)]
272pub(super) struct AuthenticatorData {
273 pub(super) rp_id_hash: Vec<u8>,
275
276 pub(super) flags: u8,
284
285 pub(super) counter: u32,
287
288 pub(super) raw_data: Vec<u8>,
290}
291
292mod auth_data_flags {
294 pub(super) const UP: u8 = 1 << 0;
296 pub(super) const UV: u8 = 1 << 2;
298 pub(super) const BE: u8 = 1 << 3;
300 pub(super) const BS: u8 = 1 << 4;
302 pub(super) const AT: u8 = 1 << 6;
304 pub(super) const ED: u8 = 1 << 7;
306}
307
308impl AuthenticatorData {
309 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 pub(super) fn is_user_present(&self) -> bool {
336 (self.flags & auth_data_flags::UP) != 0
337 }
338
339 pub(super) fn is_user_verified(&self) -> bool {
341 (self.flags & auth_data_flags::UV) != 0
342 }
343
344 pub(super) fn is_discoverable(&self) -> bool {
346 (self.flags & auth_data_flags::BE) != 0
347 }
348
349 pub(super) fn is_backed_up(&self) -> bool {
351 (self.flags & auth_data_flags::BS) != 0
352 }
353
354 pub(super) fn has_attested_credential_data(&self) -> bool {
356 (self.flags & auth_data_flags::AT) != 0
357 }
358
359 pub(super) fn has_extension_data(&self) -> bool {
361 (self.flags & auth_data_flags::ED) != 0
362 }
363
364 pub(super) fn verify(&self) -> Result<(), PasskeyError> {
366 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 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 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, pub(super) origin: String,
412}
413
414#[cfg(test)]
415mod tests;