1use crate::api::{ApiResponse, ApiState, extract_bearer_token, validate_api_token};
2use axum::{extract::State, http::HeaderMap, response::Json};
3use base64::Engine;
4use rand::Rng;
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct WebAuthnConfig {
31 pub rp_id: String,
33 pub rp_name: String,
35 pub attestation: String,
37 pub timeout_ms: u64,
39}
40
41impl Default for WebAuthnConfig {
42 fn default() -> Self {
43 Self {
44 rp_id: "localhost".to_string(),
45 rp_name: "AuthFramework".to_string(),
46 attestation: "direct".to_string(),
47 timeout_ms: 60_000,
48 }
49 }
50}
51
52impl WebAuthnConfig {
53 pub fn new(rp_id: impl Into<String>, rp_name: impl Into<String>) -> Self {
55 Self {
56 rp_id: rp_id.into(),
57 rp_name: rp_name.into(),
58 ..Self::default()
59 }
60 }
61
62 pub fn from_env() -> Self {
71 Self {
72 rp_id: std::env::var("WEBAUTHN_RP_ID").unwrap_or_else(|_| "localhost".to_string()),
73 rp_name: std::env::var("WEBAUTHN_RP_NAME")
74 .unwrap_or_else(|_| "AuthFramework".to_string()),
75 attestation: std::env::var("WEBAUTHN_ATTESTATION")
76 .unwrap_or_else(|_| "direct".to_string()),
77 timeout_ms: std::env::var("WEBAUTHN_TIMEOUT_MS")
78 .ok()
79 .and_then(|v| v.parse().ok())
80 .unwrap_or(60_000),
81 }
82 }
83
84 pub fn attestation(mut self, attestation: impl Into<String>) -> Self {
86 self.attestation = attestation.into();
87 self
88 }
89
90 pub fn timeout(mut self, ms: u64) -> Self {
92 self.timeout_ms = ms;
93 self
94 }
95}
96
97#[derive(Debug, Serialize, Deserialize)]
99pub struct WebAuthnRegistrationInitRequest {
100 pub username: String,
101 pub display_name: Option<String>,
102 pub authenticator_attachment: Option<String>, pub user_verification: Option<String>, }
105
106#[derive(Debug, Serialize, Deserialize)]
108pub struct WebAuthnRegistrationResponse {
109 pub challenge: String,
110 pub rp: PublicKeyCredentialRpEntity,
111 pub user: PublicKeyCredentialUserEntity,
112 pub pubkey_cred_params: Vec<PublicKeyCredentialParameters>,
113 pub timeout: Option<u64>,
114 #[serde(rename = "excludeCredentials")]
115 pub exclude_credentials: Option<Vec<PublicKeyCredentialDescriptor>>,
116 #[serde(rename = "authenticatorSelection")]
117 pub authenticator_selection: Option<AuthenticatorSelectionCriteria>,
118 pub attestation: String,
119 pub session_id: String,
120}
121
122#[derive(Debug, Serialize, Deserialize)]
124pub struct WebAuthnRegistrationCompleteRequest {
125 pub session_id: String,
126 pub credential_id: String,
127 pub credential_public_key: String,
128 pub attestation_object: String,
129 pub client_data_json: String,
130 pub authenticator_data: String,
131 pub signature: String,
132}
133
134#[derive(Debug, Serialize, Deserialize)]
136pub struct WebAuthnAuthenticationRequest {
137 pub username: Option<String>,
138 pub user_verification: Option<String>,
139}
140
141#[derive(Debug, Serialize, Deserialize)]
143pub struct WebAuthnAuthenticationResponse {
144 pub challenge: String,
145 pub allow_credentials: Vec<PublicKeyCredentialDescriptor>,
146 pub timeout: Option<u64>,
147 pub user_verification: String,
148 pub session_id: String,
149}
150
151#[derive(Debug, Serialize, Deserialize)]
153pub struct WebAuthnAuthenticationCompleteRequest {
154 pub session_id: String,
155 pub credential_id: String,
156 pub authenticator_data: String,
157 pub client_data_json: String,
158 pub signature: String,
159 pub user_handle: Option<String>,
160}
161
162#[derive(Debug, Serialize, Deserialize)]
164pub struct PublicKeyCredentialRpEntity {
165 pub id: String,
166 pub name: String,
167}
168
169#[derive(Debug, Serialize, Deserialize)]
170pub struct PublicKeyCredentialUserEntity {
171 pub id: String,
172 pub name: String,
173 pub display_name: String,
174}
175
176#[derive(Debug, Serialize, Deserialize)]
177pub struct PublicKeyCredentialParameters {
178 #[serde(rename = "type")]
179 pub type_field: String,
180 pub alg: i32,
181}
182
183impl PublicKeyCredentialParameters {
184 pub fn es256() -> Self {
186 Self {
187 type_field: "public-key".to_string(),
188 alg: -7,
189 }
190 }
191
192 pub fn rs256() -> Self {
194 Self {
195 type_field: "public-key".to_string(),
196 alg: -257,
197 }
198 }
199
200 pub fn defaults() -> Vec<Self> {
202 vec![Self::es256(), Self::rs256()]
203 }
204}
205
206#[derive(Debug, Serialize, Deserialize)]
207pub struct PublicKeyCredentialDescriptor {
208 #[serde(rename = "type")]
209 pub type_field: String,
210 pub id: String,
211 pub transports: Option<Vec<String>>,
212}
213
214#[derive(Debug, Serialize, Deserialize)]
215pub struct AuthenticatorSelectionCriteria {
216 pub authenticator_attachment: Option<String>,
217 pub require_resident_key: Option<bool>,
218 pub user_verification: String,
219}
220
221pub async fn webauthn_registration_init(
223 State(state): State<ApiState>,
224 Json(request): Json<WebAuthnRegistrationInitRequest>,
225) -> Json<ApiResponse<WebAuthnRegistrationResponse>> {
226 if let Err(e) = crate::utils::validation::validate_username(&request.username) {
228 return Json(ApiResponse::error_typed("VALIDATION_ERROR", format!("{e}")));
229 }
230
231 let mut challenge_bytes = [0u8; 32];
233 rand::rng().fill_bytes(&mut challenge_bytes);
234 let challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(challenge_bytes);
235
236 let user_id =
238 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(request.username.as_bytes());
239
240 let session_id = format!("webauthn_{}", uuid::Uuid::new_v4());
242
243 let webauthn_cfg = WebAuthnConfig::from_env();
244
245 let response = WebAuthnRegistrationResponse {
246 challenge: challenge.clone(),
247 rp: PublicKeyCredentialRpEntity {
248 id: webauthn_cfg.rp_id,
249 name: webauthn_cfg.rp_name,
250 },
251 user: PublicKeyCredentialUserEntity {
252 id: user_id,
253 name: request.username.clone(),
254 display_name: request.display_name.unwrap_or(request.username.clone()),
255 },
256 pubkey_cred_params: PublicKeyCredentialParameters::defaults(),
257 timeout: Some(webauthn_cfg.timeout_ms),
258 exclude_credentials: None,
259 authenticator_selection: Some(AuthenticatorSelectionCriteria {
260 authenticator_attachment: request.authenticator_attachment,
261 require_resident_key: Some(false),
262 user_verification: request.user_verification.unwrap_or("preferred".to_string()),
263 }),
264 attestation: webauthn_cfg.attestation,
268 session_id: session_id.clone(),
269 };
270
271 let session_key = format!("webauthn_reg_session:{}", session_id);
273 let session_data = serde_json::json!({
274 "challenge": challenge,
275 "username": request.username,
276 "timestamp": chrono::Utc::now().timestamp()
277 });
278 let _ = state
279 .auth_framework
280 .storage()
281 .store_kv(
282 &session_key,
283 session_data.to_string().as_bytes(),
284 Some(std::time::Duration::from_secs(300)),
285 )
286 .await;
287
288 Json(ApiResponse::success_with_message(
289 response,
290 "WebAuthn registration challenge generated",
291 ))
292}
293
294pub async fn webauthn_registration_complete(
296 State(state): State<ApiState>,
297 Json(request): Json<WebAuthnRegistrationCompleteRequest>,
298) -> Json<ApiResponse<()>> {
299 let session_key = format!("webauthn_reg_session:{}", request.session_id);
301 let storage = state.auth_framework.storage();
302
303 let (username, stored_challenge) = match storage.get_kv(&session_key).await {
304 Ok(Some(data)) => {
305 let session: serde_json::Value =
306 serde_json::from_slice(&data).unwrap_or(serde_json::Value::Null);
307 let uname = session
308 .get("username")
309 .and_then(|u| u.as_str())
310 .unwrap_or("unknown")
311 .to_string();
312 let challenge = session
313 .get("challenge")
314 .and_then(|c| c.as_str())
315 .unwrap_or("")
316 .to_string();
317 (uname, challenge)
318 }
319 _ => {
320 return Json(ApiResponse::validation_error(
321 "Session not found or expired",
322 ));
323 }
324 };
325
326 if let Err(e) = storage.delete_kv(&session_key).await {
328 tracing::warn!("Failed to delete WebAuthn registration session: {}", e);
329 }
330
331 if request.credential_id.is_empty() || request.attestation_object.is_empty() {
333 return Json(ApiResponse::validation_error("Invalid credential data"));
334 }
335
336 let client_data_bytes = match base64::engine::general_purpose::URL_SAFE_NO_PAD
338 .decode(&request.client_data_json)
339 .or_else(|_| base64::engine::general_purpose::STANDARD.decode(&request.client_data_json))
340 {
341 Ok(b) => b,
342 Err(_) => {
343 return Json(ApiResponse::validation_error(
344 "Invalid client_data_json encoding",
345 ));
346 }
347 };
348
349 let client_data: serde_json::Value = match serde_json::from_slice(&client_data_bytes) {
350 Ok(v) => v,
351 Err(_) => {
352 return Json(ApiResponse::validation_error(
353 "Invalid client_data_json format",
354 ));
355 }
356 };
357
358 if client_data.get("type").and_then(|t| t.as_str()) != Some("webauthn.create") {
360 return Json(ApiResponse::validation_error(
361 "Invalid ceremony type: expected webauthn.create",
362 ));
363 }
364
365 if let Some(received_challenge) = client_data.get("challenge").and_then(|c| c.as_str()) {
367 if received_challenge != stored_challenge {
368 return Json(ApiResponse::validation_error(
369 "Challenge mismatch: possible replay attack",
370 ));
371 }
372 } else {
373 return Json(ApiResponse::validation_error(
374 "Missing challenge in client data",
375 ));
376 }
377
378 let expected_rp_id = WebAuthnConfig::from_env().rp_id;
380 if let Some(origin) = client_data.get("origin").and_then(|o| o.as_str()) {
381 if let Ok(origin_url) = url::Url::parse(origin) {
383 if origin_url.host_str() != Some(&expected_rp_id) {
384 return Json(ApiResponse::validation_error(
385 "Origin mismatch: does not match relying party ID",
386 ));
387 }
388 } else if origin != expected_rp_id {
389 return Json(ApiResponse::validation_error(
390 "Origin mismatch: does not match relying party ID",
391 ));
392 }
393 }
394
395 let credential_key = format!("webauthn_credential:{}:{}", username, request.credential_id);
397 let credential_data = serde_json::json!({
398 "credential_id": request.credential_id,
399 "credential_public_key": request.credential_public_key,
400 "username": username,
401 "registered_at": chrono::Utc::now().timestamp(),
402 "sign_count": 0u64
403 });
404 let _ = storage
405 .store_kv(
406 &credential_key,
407 credential_data.to_string().as_bytes(),
408 None,
409 )
410 .await;
411
412 let index_key = format!("webauthn_creds_index:{}", username);
414 let mut existing_ids: Vec<String> = match storage.get_kv(&index_key).await {
415 Ok(Some(data)) => serde_json::from_slice(&data).unwrap_or_default(),
416 _ => Vec::new(),
417 };
418 if !existing_ids.contains(&request.credential_id) {
419 existing_ids.push(request.credential_id.clone());
420 let _ = storage
421 .store_kv(
422 &index_key,
423 serde_json::to_string(&existing_ids)
424 .unwrap_or_default()
425 .as_bytes(),
426 None,
427 )
428 .await;
429 }
430
431 Json(ApiResponse::<()>::ok_with_message(
432 "WebAuthn credential registered successfully",
433 ))
434}
435
436pub async fn webauthn_authentication_init(
438 State(state): State<ApiState>,
439 Json(request): Json<WebAuthnAuthenticationRequest>,
440) -> Json<ApiResponse<WebAuthnAuthenticationResponse>> {
441 let mut challenge_bytes = [0u8; 32];
442 rand::rng().fill_bytes(&mut challenge_bytes);
443 let challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(challenge_bytes);
444
445 let session_id = format!("webauthn_auth_{}", uuid::Uuid::new_v4());
446 let storage = state.auth_framework.storage();
447
448 let username = request.username.as_deref().unwrap_or("");
450 let allow_credentials = if !username.is_empty() {
451 let index_key = format!("webauthn_creds_index:{}", username);
453 match storage.get_kv(&index_key).await {
454 Ok(Some(data)) => {
455 if let Ok(ids) = serde_json::from_slice::<Vec<String>>(&data) {
456 ids.into_iter()
457 .map(|id| PublicKeyCredentialDescriptor {
458 type_field: "public-key".to_string(),
459 id,
460 transports: Some(vec!["internal".to_string(), "usb".to_string()]),
461 })
462 .collect::<Vec<_>>()
463 } else {
464 Vec::new()
465 }
466 }
467 _ => Vec::new(),
468 }
469 } else {
470 Vec::new()
471 };
472
473 let session_key = format!("webauthn_auth_session:{}", session_id);
475 let session_data = serde_json::json!({
476 "challenge": challenge,
477 "username": request.username,
478 "timestamp": chrono::Utc::now().timestamp()
479 });
480 let _ = storage
481 .store_kv(
482 &session_key,
483 session_data.to_string().as_bytes(),
484 Some(std::time::Duration::from_secs(300)), )
486 .await;
487
488 let response = WebAuthnAuthenticationResponse {
489 challenge,
490 allow_credentials,
491 timeout: Some(60000),
492 user_verification: request.user_verification.unwrap_or("preferred".to_string()),
493 session_id,
494 };
495
496 Json(ApiResponse::success_with_message(
497 response,
498 "WebAuthn authentication challenge generated",
499 ))
500}
501
502pub async fn webauthn_authentication_complete(
504 State(state): State<ApiState>,
505 Json(request): Json<WebAuthnAuthenticationCompleteRequest>,
506) -> Json<ApiResponse<serde_json::Value>> {
507 let storage = state.auth_framework.storage();
508 let session_key = format!("webauthn_auth_session:{}", request.session_id);
509
510 let (username, stored_challenge) = match storage.get_kv(&session_key).await {
512 Ok(Some(data)) => {
513 let session: serde_json::Value =
514 serde_json::from_slice(&data).unwrap_or(serde_json::Value::Null);
515 let uname = session
516 .get("username")
517 .and_then(|u| u.as_str())
518 .unwrap_or("webauthn_user")
519 .to_string();
520 let challenge = session
521 .get("challenge")
522 .and_then(|c| c.as_str())
523 .unwrap_or("")
524 .to_string();
525 (uname, challenge)
526 }
527 _ => {
528 return Json(ApiResponse::validation_error_typed(
529 "Authentication session not found or expired",
530 ));
531 }
532 };
533
534 if let Err(e) = storage.delete_kv(&session_key).await {
536 tracing::warn!("Failed to delete WebAuthn authentication session: {}", e);
537 }
538
539 let client_data_bytes = match base64::engine::general_purpose::URL_SAFE_NO_PAD
541 .decode(&request.client_data_json)
542 .or_else(|_| base64::engine::general_purpose::STANDARD.decode(&request.client_data_json))
543 {
544 Ok(b) => b,
545 Err(_) => {
546 return Json(ApiResponse::validation_error_typed(
547 "Invalid client_data_json encoding",
548 ));
549 }
550 };
551
552 let client_data: serde_json::Value = match serde_json::from_slice(&client_data_bytes) {
553 Ok(v) => v,
554 Err(_) => {
555 return Json(ApiResponse::validation_error_typed(
556 "Invalid client_data_json format",
557 ));
558 }
559 };
560
561 if client_data.get("type").and_then(|t| t.as_str()) != Some("webauthn.get") {
563 return Json(ApiResponse::validation_error_typed(
564 "Invalid ceremony type: expected webauthn.get",
565 ));
566 }
567
568 if let Some(received_challenge) = client_data.get("challenge").and_then(|c| c.as_str()) {
570 if received_challenge != stored_challenge {
571 return Json(ApiResponse::validation_error_typed(
572 "Challenge mismatch: possible replay attack",
573 ));
574 }
575 } else {
576 return Json(ApiResponse::validation_error_typed(
577 "Missing challenge in client data",
578 ));
579 }
580
581 let expected_rp_id = WebAuthnConfig::from_env().rp_id;
583 if let Some(origin) = client_data.get("origin").and_then(|o| o.as_str()) {
584 if let Ok(origin_url) = url::Url::parse(origin) {
585 if origin_url.host_str() != Some(&expected_rp_id) {
586 return Json(ApiResponse::validation_error_typed(
587 "Origin mismatch: does not match relying party ID",
588 ));
589 }
590 } else if origin != expected_rp_id {
591 return Json(ApiResponse::validation_error_typed(
592 "Origin mismatch: does not match relying party ID",
593 ));
594 }
595 }
596
597 let credential_key = format!("webauthn_credential:{}:{}", username, request.credential_id);
599 let stored_credential = match storage.get_kv(&credential_key).await {
600 Ok(Some(data)) => {
601 serde_json::from_slice::<serde_json::Value>(&data).unwrap_or(serde_json::Value::Null)
602 }
603 _ => {
604 return Json(ApiResponse::validation_error_typed(
605 "Credential not found for this user",
606 ));
607 }
608 };
609
610 let stored_count = stored_credential
612 .get("sign_count")
613 .and_then(|c| c.as_u64())
614 .unwrap_or(0);
615 let new_count = base64::engine::general_purpose::URL_SAFE_NO_PAD
617 .decode(&request.authenticator_data)
618 .or_else(|_| base64::engine::general_purpose::STANDARD.decode(&request.authenticator_data))
619 .ok()
620 .filter(|d| d.len() >= 37)
621 .map(|d| u32::from_be_bytes([d[33], d[34], d[35], d[36]]) as u64)
622 .unwrap_or(0);
623 if new_count > 0 && new_count <= stored_count {
624 tracing::warn!(
625 "WebAuthn signature counter regression for user {}: stored={}, received={}. Possible cloned authenticator.",
626 username,
627 stored_count,
628 new_count
629 );
630 return Json(ApiResponse::validation_error_typed(
631 "Signature counter regression detected: possible cloned authenticator",
632 ));
633 }
634
635 let auth_data_bytes = match base64::engine::general_purpose::URL_SAFE_NO_PAD
638 .decode(&request.authenticator_data)
639 .or_else(|_| base64::engine::general_purpose::STANDARD.decode(&request.authenticator_data))
640 {
641 Ok(b) => b,
642 Err(_) => {
643 return Json(ApiResponse::validation_error_typed(
644 "Invalid authenticator_data encoding",
645 ));
646 }
647 };
648
649 let client_data_hash = {
651 let mut hasher = Sha256::new();
652 hasher.update(&client_data_bytes);
653 hasher.finalize()
654 };
655
656 let mut signed_message = auth_data_bytes.clone();
658 signed_message.extend_from_slice(&client_data_hash);
659
660 let signature_bytes = match base64::engine::general_purpose::URL_SAFE_NO_PAD
662 .decode(&request.signature)
663 .or_else(|_| base64::engine::general_purpose::STANDARD.decode(&request.signature))
664 {
665 Ok(b) => b,
666 Err(_) => {
667 return Json(ApiResponse::validation_error_typed(
668 "Invalid signature encoding",
669 ));
670 }
671 };
672
673 let credential_pub_key = stored_credential
675 .get("credential_public_key")
676 .and_then(|k| k.as_str())
677 .unwrap_or("");
678
679 if credential_pub_key.is_empty() {
680 return Json(ApiResponse::validation_error_typed(
681 "No public key stored for this credential",
682 ));
683 }
684
685 let pub_key_bytes = match base64::engine::general_purpose::URL_SAFE_NO_PAD
687 .decode(credential_pub_key)
688 .or_else(|_| base64::engine::general_purpose::STANDARD.decode(credential_pub_key))
689 {
690 Ok(b) => b,
691 Err(_) => {
692 return Json(ApiResponse::validation_error_typed(
693 "Failed to decode stored public key",
694 ));
695 }
696 };
697
698 let sig_valid = {
700 let es256_result = ring::signature::UnparsedPublicKey::new(
702 &ring::signature::ECDSA_P256_SHA256_ASN1,
703 &pub_key_bytes,
704 )
705 .verify(&signed_message, &signature_bytes);
706
707 if es256_result.is_ok() {
708 true
709 } else {
710 ring::signature::UnparsedPublicKey::new(
712 &ring::signature::RSA_PKCS1_2048_8192_SHA256,
713 &pub_key_bytes,
714 )
715 .verify(&signed_message, &signature_bytes)
716 .is_ok()
717 }
718 };
719
720 if !sig_valid {
721 tracing::warn!(
722 "WebAuthn signature verification failed for user {} credential {}",
723 username,
724 request.credential_id
725 );
726 return Json(ApiResponse::validation_error_typed(
727 "Signature verification failed: authentication assertion is not valid",
728 ));
729 }
730
731 let mut updated_cred = stored_credential.clone();
733 if let Some(obj) = updated_cred.as_object_mut() {
734 obj.insert("sign_count".to_string(), serde_json::json!(new_count));
735 }
736 if let Err(e) = storage
737 .store_kv(
738 &credential_key,
739 serde_json::to_string(&updated_cred)
740 .unwrap_or_default()
741 .as_bytes(),
742 None,
743 )
744 .await
745 {
746 tracing::warn!("Failed to update WebAuthn credential counter for {}: {}", username, e);
747 }
748
749 let token_lifetime = state.auth_framework.config().token_lifetime;
751 let token = match state.auth_framework.token_manager().create_jwt_token(
752 &username,
753 vec![],
754 Some(token_lifetime),
755 ) {
756 Ok(t) => t,
757 Err(e) => {
758 return Json(ApiResponse::validation_error_typed(format!(
759 "Token generation failed: {}",
760 e
761 )));
762 }
763 };
764
765 let auth_response = serde_json::json!({
766 "access_token": token,
767 "token_type": "Bearer",
768 "expires_in": token_lifetime.as_secs(),
769 "user_id": username,
770 "authentication_method": "webauthn"
771 });
772
773 Json(ApiResponse::success_with_message(
774 auth_response,
775 "WebAuthn authentication successful",
776 ))
777}
778
779pub async fn list_webauthn_credentials(
781 State(state): State<ApiState>,
782 headers: HeaderMap,
783 axum::extract::Path(username): axum::extract::Path<String>,
784) -> Json<ApiResponse<Vec<serde_json::Value>>> {
785 let token = match extract_bearer_token(&headers) {
787 Some(t) => t,
788 None => {
789 return Json(ApiResponse::error_typed(
790 "UNAUTHORIZED",
791 "Authentication required",
792 ));
793 }
794 };
795 let auth_token = match validate_api_token(&state.auth_framework, &token).await {
796 Ok(t) => t,
797 Err(_) => {
798 return Json(ApiResponse::error_typed(
799 "UNAUTHORIZED",
800 "Invalid or expired token",
801 ));
802 }
803 };
804
805 if auth_token.user_id != username && !auth_token.roles.contains(&"admin".to_string()) {
807 return Json(ApiResponse::error_typed(
808 "FORBIDDEN",
809 "You can only view your own credentials",
810 ));
811 }
812
813 let storage = state.auth_framework.storage();
814 let index_key = format!("webauthn_creds_index:{}", username);
815
816 let credentials = match storage.get_kv(&index_key).await {
817 Ok(Some(data)) => {
818 if let Ok(ids) = serde_json::from_slice::<Vec<String>>(&data) {
819 let mut creds = Vec::new();
820 for id in ids {
821 let cred_key = format!("webauthn_credential:{}:{}", username, id);
822 if let Ok(Some(cred_data)) = storage.get_kv(&cred_key).await
823 && let Ok(cred) = serde_json::from_slice::<serde_json::Value>(&cred_data)
824 {
825 creds.push(cred);
826 }
827 }
828 creds
829 } else {
830 Vec::new()
831 }
832 }
833 _ => Vec::new(),
834 };
835
836 Json(ApiResponse::success_with_message(
837 credentials,
838 format!("WebAuthn credentials retrieved for user: {}", username),
839 ))
840}
841
842pub async fn delete_webauthn_credential(
844 State(state): State<ApiState>,
845 headers: HeaderMap,
846 axum::extract::Path((username, credential_id)): axum::extract::Path<(String, String)>,
847) -> Json<ApiResponse<()>> {
848 let token = match extract_bearer_token(&headers) {
850 Some(t) => t,
851 None => {
852 return Json(ApiResponse::error(
853 "UNAUTHORIZED",
854 "Authentication required",
855 ));
856 }
857 };
858 let auth_token = match validate_api_token(&state.auth_framework, &token).await {
859 Ok(t) => t,
860 Err(_) => {
861 return Json(ApiResponse::error(
862 "UNAUTHORIZED",
863 "Invalid or expired token",
864 ));
865 }
866 };
867
868 if auth_token.user_id != username && !auth_token.roles.contains(&"admin".to_string()) {
870 return Json(ApiResponse::error(
871 "FORBIDDEN",
872 "You can only delete your own credentials",
873 ));
874 }
875
876 let storage = state.auth_framework.storage();
877 let credential_key = format!("webauthn_credential:{}:{}", username, credential_id);
878
879 match storage.get_kv(&credential_key).await {
881 Ok(Some(_)) => {
882 if let Err(e) = storage.delete_kv(&credential_key).await {
883 tracing::warn!("Failed to delete WebAuthn credential {}: {}", credential_id, e);
884 }
885
886 let index_key = format!("webauthn_creds_index:{}", username);
888 if let Ok(Some(idx_data)) = storage.get_kv(&index_key).await
889 && let Ok(mut ids) = serde_json::from_slice::<Vec<String>>(&idx_data)
890 {
891 ids.retain(|id| id != &credential_id);
892 if let Err(e) = storage
893 .store_kv(
894 &index_key,
895 serde_json::to_string(&ids).unwrap_or_default().as_bytes(),
896 None,
897 )
898 .await
899 {
900 tracing::warn!("Failed to update WebAuthn credentials index for {}: {}", username, e);
901 }
902 }
903
904 Json(ApiResponse::<()>::ok_with_message(
905 "WebAuthn credential deleted successfully",
906 ))
907 }
908 _ => Json(ApiResponse::validation_error("Credential not found")),
909 }
910}
911
912#[cfg(test)]
913mod tests {
914 use super::*;
915
916 #[test]
917 fn test_webauthn_config_default() {
918 let cfg = WebAuthnConfig::default();
919 assert_eq!(cfg.rp_id, "localhost");
920 assert_eq!(cfg.rp_name, "AuthFramework");
921 assert_eq!(cfg.attestation, "direct");
922 assert_eq!(cfg.timeout_ms, 60_000);
923 }
924
925 #[test]
926 fn test_webauthn_config_new_and_chain() {
927 let cfg = WebAuthnConfig::new("auth.example.com", "My Service")
928 .attestation("none")
929 .timeout(120_000);
930 assert_eq!(cfg.rp_id, "auth.example.com");
931 assert_eq!(cfg.rp_name, "My Service");
932 assert_eq!(cfg.attestation, "none");
933 assert_eq!(cfg.timeout_ms, 120_000);
934 }
935
936 #[test]
937 fn test_pubkey_cred_params_presets() {
938 let es = PublicKeyCredentialParameters::es256();
939 assert_eq!(es.alg, -7);
940 assert_eq!(es.type_field, "public-key");
941
942 let rs = PublicKeyCredentialParameters::rs256();
943 assert_eq!(rs.alg, -257);
944 assert_eq!(rs.type_field, "public-key");
945 }
946
947 #[test]
948 fn test_pubkey_cred_params_defaults_contains_both() {
949 let params = PublicKeyCredentialParameters::defaults();
950 assert_eq!(params.len(), 2);
951 assert_eq!(params[0].alg, -7);
952 assert_eq!(params[1].alg, -257);
953 }
954}