this repo has no description
1use axum::{ 2 Json, 3 extract::State, 4 http::{HeaderMap, StatusCode}, 5 response::{IntoResponse, Response}, 6}; 7use bcrypt::{DEFAULT_COST, hash}; 8use chrono::{Duration, Utc}; 9use jacquard::types::{did::Did, integer::LimitedU32, string::Tid}; 10use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore}; 11use rand::Rng; 12use serde::{Deserialize, Serialize}; 13use serde_json::json; 14use std::sync::Arc; 15use tracing::{error, info, warn}; 16use uuid::Uuid; 17 18use crate::state::{AppState, RateLimitKind}; 19use crate::validation::validate_password; 20 21fn extract_client_ip(headers: &HeaderMap) -> String { 22 if let Some(forwarded) = headers.get("x-forwarded-for") 23 && let Ok(value) = forwarded.to_str() 24 && let Some(first_ip) = value.split(',').next() 25 { 26 return first_ip.trim().to_string(); 27 } 28 if let Some(real_ip) = headers.get("x-real-ip") 29 && let Ok(value) = real_ip.to_str() 30 { 31 return value.trim().to_string(); 32 } 33 "unknown".to_string() 34} 35 36fn generate_setup_token() -> String { 37 let mut rng = rand::thread_rng(); 38 (0..32) 39 .map(|_| { 40 let idx = rng.gen_range(0..36); 41 if idx < 10 { 42 (b'0' + idx) as char 43 } else { 44 (b'a' + idx - 10) as char 45 } 46 }) 47 .collect() 48} 49 50fn generate_app_password() -> String { 51 let chars: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567"; 52 let mut rng = rand::thread_rng(); 53 let segments: Vec<String> = (0..4) 54 .map(|_| { 55 (0..4) 56 .map(|_| chars[rng.gen_range(0..chars.len())] as char) 57 .collect() 58 }) 59 .collect(); 60 segments.join("-") 61} 62 63#[derive(Deserialize)] 64#[serde(rename_all = "camelCase")] 65pub struct CreatePasskeyAccountInput { 66 pub handle: String, 67 pub email: Option<String>, 68 pub invite_code: Option<String>, 69 pub did: Option<String>, 70 pub did_type: Option<String>, 71 pub signing_key: Option<String>, 72 pub verification_channel: Option<String>, 73 pub discord_id: Option<String>, 74 pub telegram_username: Option<String>, 75 pub signal_number: Option<String>, 76} 77 78#[derive(Serialize)] 79#[serde(rename_all = "camelCase")] 80pub struct CreatePasskeyAccountResponse { 81 pub did: String, 82 pub handle: String, 83 pub setup_token: String, 84 pub setup_expires_at: chrono::DateTime<Utc>, 85} 86 87pub async fn create_passkey_account( 88 State(state): State<AppState>, 89 headers: HeaderMap, 90 Json(input): Json<CreatePasskeyAccountInput>, 91) -> Response { 92 let client_ip = extract_client_ip(&headers); 93 if !state 94 .check_rate_limit(RateLimitKind::AccountCreation, &client_ip) 95 .await 96 { 97 warn!(ip = %client_ip, "Account creation rate limit exceeded"); 98 return ( 99 StatusCode::TOO_MANY_REQUESTS, 100 Json(json!({ 101 "error": "RateLimitExceeded", 102 "message": "Too many account creation attempts. Please try again later." 103 })), 104 ) 105 .into_response(); 106 } 107 108 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 109 let pds_suffix = format!(".{}", hostname); 110 111 let handle = if !input.handle.contains('.') || input.handle.ends_with(&pds_suffix) { 112 let handle_to_validate = if input.handle.ends_with(&pds_suffix) { 113 input 114 .handle 115 .strip_suffix(&pds_suffix) 116 .unwrap_or(&input.handle) 117 } else { 118 &input.handle 119 }; 120 match crate::api::validation::validate_short_handle(handle_to_validate) { 121 Ok(h) => format!("{}.{}", h, hostname), 122 Err(e) => { 123 return ( 124 StatusCode::BAD_REQUEST, 125 Json(json!({"error": "InvalidHandle", "message": e.to_string()})), 126 ) 127 .into_response(); 128 } 129 } 130 } else { 131 input.handle.to_lowercase() 132 }; 133 134 let email = input 135 .email 136 .as_ref() 137 .map(|e| e.trim().to_string()) 138 .filter(|e| !e.is_empty()); 139 if let Some(ref email) = email 140 && !crate::api::validation::is_valid_email(email) 141 { 142 return ( 143 StatusCode::BAD_REQUEST, 144 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})), 145 ) 146 .into_response(); 147 } 148 149 if let Some(ref code) = input.invite_code { 150 let valid = sqlx::query_scalar!( 151 "SELECT available_uses > 0 AND NOT disabled FROM invite_codes WHERE code = $1", 152 code 153 ) 154 .fetch_optional(&state.db) 155 .await 156 .ok() 157 .flatten() 158 .unwrap_or(Some(false)); 159 160 if valid != Some(true) { 161 return ( 162 StatusCode::BAD_REQUEST, 163 Json(json!({"error": "InvalidInviteCode", "message": "Invalid or expired invite code"})), 164 ) 165 .into_response(); 166 } 167 } else { 168 let invite_required = std::env::var("INVITE_CODE_REQUIRED") 169 .map(|v| v == "true" || v == "1") 170 .unwrap_or(false); 171 if invite_required { 172 return ( 173 StatusCode::BAD_REQUEST, 174 Json(json!({"error": "InviteCodeRequired", "message": "An invite code is required to create an account"})), 175 ) 176 .into_response(); 177 } 178 } 179 180 let verification_channel = input.verification_channel.as_deref().unwrap_or("email"); 181 let verification_recipient = match verification_channel { 182 "email" => match &email { 183 Some(e) if !e.is_empty() => e.clone(), 184 _ => return ( 185 StatusCode::BAD_REQUEST, 186 Json(json!({"error": "MissingEmail", "message": "Email is required when using email verification"})), 187 ).into_response(), 188 }, 189 "discord" => match &input.discord_id { 190 Some(id) if !id.trim().is_empty() => id.trim().to_string(), 191 _ => return ( 192 StatusCode::BAD_REQUEST, 193 Json(json!({"error": "MissingDiscordId", "message": "Discord ID is required when using Discord verification"})), 194 ).into_response(), 195 }, 196 "telegram" => match &input.telegram_username { 197 Some(username) if !username.trim().is_empty() => username.trim().to_string(), 198 _ => return ( 199 StatusCode::BAD_REQUEST, 200 Json(json!({"error": "MissingTelegramUsername", "message": "Telegram username is required when using Telegram verification"})), 201 ).into_response(), 202 }, 203 "signal" => match &input.signal_number { 204 Some(number) if !number.trim().is_empty() => number.trim().to_string(), 205 _ => return ( 206 StatusCode::BAD_REQUEST, 207 Json(json!({"error": "MissingSignalNumber", "message": "Signal phone number is required when using Signal verification"})), 208 ).into_response(), 209 }, 210 _ => return ( 211 StatusCode::BAD_REQUEST, 212 Json(json!({"error": "InvalidVerificationChannel", "message": "Invalid verification channel"})), 213 ).into_response(), 214 }; 215 216 use k256::ecdsa::SigningKey; 217 use rand::rngs::OsRng; 218 219 let pds_endpoint = format!("https://{}", hostname); 220 let did_type = input.did_type.as_deref().unwrap_or("plc"); 221 222 let (secret_key_bytes, reserved_key_id): (Vec<u8>, Option<Uuid>) = 223 if let Some(signing_key_did) = &input.signing_key { 224 let reserved = sqlx::query!( 225 r#" 226 SELECT id, private_key_bytes 227 FROM reserved_signing_keys 228 WHERE public_key_did_key = $1 229 AND used_at IS NULL 230 AND expires_at > NOW() 231 FOR UPDATE 232 "#, 233 signing_key_did 234 ) 235 .fetch_optional(&state.db) 236 .await; 237 match reserved { 238 Ok(Some(row)) => (row.private_key_bytes, Some(row.id)), 239 Ok(None) => { 240 return ( 241 StatusCode::BAD_REQUEST, 242 Json(json!({ 243 "error": "InvalidSigningKey", 244 "message": "Signing key not found, already used, or expired" 245 })), 246 ) 247 .into_response(); 248 } 249 Err(e) => { 250 error!("Error looking up reserved signing key: {:?}", e); 251 return ( 252 StatusCode::INTERNAL_SERVER_ERROR, 253 Json(json!({"error": "InternalError"})), 254 ) 255 .into_response(); 256 } 257 } 258 } else { 259 let secret_key = k256::SecretKey::random(&mut OsRng); 260 (secret_key.to_bytes().to_vec(), None) 261 }; 262 263 let secret_key = match SigningKey::from_slice(&secret_key_bytes) { 264 Ok(k) => k, 265 Err(e) => { 266 error!("Error creating signing key: {:?}", e); 267 return ( 268 StatusCode::INTERNAL_SERVER_ERROR, 269 Json(json!({"error": "InternalError"})), 270 ) 271 .into_response(); 272 } 273 }; 274 275 let did = match did_type { 276 "web" => { 277 let subdomain_host = format!("{}.{}", input.handle, hostname); 278 let encoded_subdomain = subdomain_host.replace(':', "%3A"); 279 let self_hosted_did = format!("did:web:{}", encoded_subdomain); 280 info!(did = %self_hosted_did, "Creating self-hosted did:web passkey account"); 281 self_hosted_did 282 } 283 "web-external" => { 284 let d = match &input.did { 285 Some(d) if !d.trim().is_empty() => d.trim(), 286 _ => { 287 return ( 288 StatusCode::BAD_REQUEST, 289 Json(json!({"error": "InvalidRequest", "message": "External did:web requires the 'did' field to be provided"})), 290 ) 291 .into_response(); 292 } 293 }; 294 if !d.starts_with("did:web:") { 295 return ( 296 StatusCode::BAD_REQUEST, 297 Json( 298 json!({"error": "InvalidDid", "message": "External DID must be a did:web"}), 299 ), 300 ) 301 .into_response(); 302 } 303 if let Err(e) = crate::api::identity::did::verify_did_web( 304 d, 305 &hostname, 306 &input.handle, 307 input.signing_key.as_deref(), 308 ) 309 .await 310 { 311 return ( 312 StatusCode::BAD_REQUEST, 313 Json(json!({"error": "InvalidDid", "message": e})), 314 ) 315 .into_response(); 316 } 317 info!(did = %d, "Creating external did:web passkey account"); 318 d.to_string() 319 } 320 _ => { 321 let rotation_key = std::env::var("PLC_ROTATION_KEY") 322 .unwrap_or_else(|_| crate::plc::signing_key_to_did_key(&secret_key)); 323 324 let genesis_result = match crate::plc::create_genesis_operation( 325 &secret_key, 326 &rotation_key, 327 &handle, 328 &pds_endpoint, 329 ) { 330 Ok(r) => r, 331 Err(e) => { 332 error!("Error creating PLC genesis operation: {:?}", e); 333 return ( 334 StatusCode::INTERNAL_SERVER_ERROR, 335 Json(json!({"error": "InternalError", "message": "Failed to create PLC operation"})), 336 ) 337 .into_response(); 338 } 339 }; 340 341 let plc_client = crate::plc::PlcClient::new(None); 342 if let Err(e) = plc_client 343 .send_operation(&genesis_result.did, &genesis_result.signed_operation) 344 .await 345 { 346 error!("Failed to submit PLC genesis operation: {:?}", e); 347 return ( 348 StatusCode::BAD_GATEWAY, 349 Json(json!({ 350 "error": "UpstreamError", 351 "message": format!("Failed to register DID with PLC directory: {}", e) 352 })), 353 ) 354 .into_response(); 355 } 356 genesis_result.did 357 } 358 }; 359 360 info!(did = %did, handle = %handle, "Created DID for passkey-only account"); 361 362 let verification_code = format!( 363 "{:06}", 364 rand::Rng::gen_range(&mut rand::thread_rng(), 0..1_000_000u32) 365 ); 366 let verification_code_expires_at = Utc::now() + Duration::minutes(30); 367 368 let setup_token = generate_setup_token(); 369 let setup_token_hash = match hash(&setup_token, DEFAULT_COST) { 370 Ok(h) => h, 371 Err(e) => { 372 error!("Error hashing setup token: {:?}", e); 373 return ( 374 StatusCode::INTERNAL_SERVER_ERROR, 375 Json(json!({"error": "InternalError"})), 376 ) 377 .into_response(); 378 } 379 }; 380 let setup_expires_at = Utc::now() + Duration::hours(1); 381 382 let mut tx = match state.db.begin().await { 383 Ok(tx) => tx, 384 Err(e) => { 385 error!("Error starting transaction: {:?}", e); 386 return ( 387 StatusCode::INTERNAL_SERVER_ERROR, 388 Json(json!({"error": "InternalError"})), 389 ) 390 .into_response(); 391 } 392 }; 393 394 let user_insert: Result<(Uuid,), _> = sqlx::query_as( 395 r#"INSERT INTO users ( 396 handle, email, did, password_hash, password_required, 397 preferred_comms_channel, 398 discord_id, telegram_username, signal_number, 399 recovery_token, recovery_token_expires_at 400 ) VALUES ($1, $2, $3, NULL, FALSE, $4::comms_channel, $5, $6, $7, $8, $9) RETURNING id"#, 401 ) 402 .bind(&handle) 403 .bind(&email) 404 .bind(&did) 405 .bind(verification_channel) 406 .bind( 407 input 408 .discord_id 409 .as_deref() 410 .map(|s| s.trim()) 411 .filter(|s| !s.is_empty()), 412 ) 413 .bind( 414 input 415 .telegram_username 416 .as_deref() 417 .map(|s| s.trim()) 418 .filter(|s| !s.is_empty()), 419 ) 420 .bind( 421 input 422 .signal_number 423 .as_deref() 424 .map(|s| s.trim()) 425 .filter(|s| !s.is_empty()), 426 ) 427 .bind(&setup_token_hash) 428 .bind(setup_expires_at) 429 .fetch_one(&mut *tx) 430 .await; 431 432 let user_id = match user_insert { 433 Ok((id,)) => id, 434 Err(e) => { 435 if let Some(db_err) = e.as_database_error() 436 && db_err.code().as_deref() == Some("23505") 437 { 438 let constraint = db_err.constraint().unwrap_or(""); 439 if constraint.contains("handle") { 440 return ( 441 StatusCode::BAD_REQUEST, 442 Json(json!({"error": "HandleNotAvailable", "message": "Handle already taken"})), 443 ) 444 .into_response(); 445 } else if constraint.contains("email") { 446 return ( 447 StatusCode::BAD_REQUEST, 448 Json( 449 json!({"error": "InvalidEmail", "message": "Email already registered"}), 450 ), 451 ) 452 .into_response(); 453 } 454 } 455 error!("Error inserting user: {:?}", e); 456 return ( 457 StatusCode::INTERNAL_SERVER_ERROR, 458 Json(json!({"error": "InternalError"})), 459 ) 460 .into_response(); 461 } 462 }; 463 464 let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) { 465 Ok(bytes) => bytes, 466 Err(e) => { 467 error!("Error encrypting signing key: {:?}", e); 468 return ( 469 StatusCode::INTERNAL_SERVER_ERROR, 470 Json(json!({"error": "InternalError"})), 471 ) 472 .into_response(); 473 } 474 }; 475 476 if let Err(e) = sqlx::query!( 477 "INSERT INTO user_keys (user_id, key_bytes, encryption_version, encrypted_at) VALUES ($1, $2, $3, NOW())", 478 user_id, 479 &encrypted_key_bytes[..], 480 crate::config::ENCRYPTION_VERSION 481 ) 482 .execute(&mut *tx) 483 .await 484 { 485 error!("Error inserting user key: {:?}", e); 486 return ( 487 StatusCode::INTERNAL_SERVER_ERROR, 488 Json(json!({"error": "InternalError"})), 489 ) 490 .into_response(); 491 } 492 493 if let Some(key_id) = reserved_key_id 494 && let Err(e) = sqlx::query!( 495 "UPDATE reserved_signing_keys SET used_at = NOW() WHERE id = $1", 496 key_id 497 ) 498 .execute(&mut *tx) 499 .await 500 { 501 error!("Error marking reserved key as used: {:?}", e); 502 return ( 503 StatusCode::INTERNAL_SERVER_ERROR, 504 Json(json!({"error": "InternalError"})), 505 ) 506 .into_response(); 507 } 508 509 let mst = Mst::new(Arc::new(state.block_store.clone())); 510 let mst_root = match mst.persist().await { 511 Ok(c) => c, 512 Err(e) => { 513 error!("Error persisting MST: {:?}", e); 514 return ( 515 StatusCode::INTERNAL_SERVER_ERROR, 516 Json(json!({"error": "InternalError"})), 517 ) 518 .into_response(); 519 } 520 }; 521 let did_obj = match Did::new(&did) { 522 Ok(d) => d, 523 Err(_) => { 524 return ( 525 StatusCode::INTERNAL_SERVER_ERROR, 526 Json(json!({"error": "InternalError", "message": "Invalid DID"})), 527 ) 528 .into_response(); 529 } 530 }; 531 let rev = Tid::now(LimitedU32::MIN); 532 let unsigned_commit = Commit::new_unsigned(did_obj, mst_root, rev, None); 533 let signed_commit = match unsigned_commit.sign(&secret_key) { 534 Ok(c) => c, 535 Err(e) => { 536 error!("Error signing genesis commit: {:?}", e); 537 return ( 538 StatusCode::INTERNAL_SERVER_ERROR, 539 Json(json!({"error": "InternalError"})), 540 ) 541 .into_response(); 542 } 543 }; 544 let commit_bytes = match signed_commit.to_cbor() { 545 Ok(b) => b, 546 Err(e) => { 547 error!("Error serializing genesis commit: {:?}", e); 548 return ( 549 StatusCode::INTERNAL_SERVER_ERROR, 550 Json(json!({"error": "InternalError"})), 551 ) 552 .into_response(); 553 } 554 }; 555 let commit_cid: cid::Cid = match state.block_store.put(&commit_bytes).await { 556 Ok(c) => c, 557 Err(e) => { 558 error!("Error saving genesis commit: {:?}", e); 559 return ( 560 StatusCode::INTERNAL_SERVER_ERROR, 561 Json(json!({"error": "InternalError"})), 562 ) 563 .into_response(); 564 } 565 }; 566 let commit_cid_str = commit_cid.to_string(); 567 if let Err(e) = sqlx::query!( 568 "INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)", 569 user_id, 570 commit_cid_str 571 ) 572 .execute(&mut *tx) 573 .await 574 { 575 error!("Error inserting repo: {:?}", e); 576 return ( 577 StatusCode::INTERNAL_SERVER_ERROR, 578 Json(json!({"error": "InternalError"})), 579 ) 580 .into_response(); 581 } 582 583 if let Some(ref code) = input.invite_code { 584 let _ = sqlx::query!( 585 "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1", 586 code 587 ) 588 .execute(&mut *tx) 589 .await; 590 591 let _ = sqlx::query!( 592 "INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)", 593 code, 594 user_id 595 ) 596 .execute(&mut *tx) 597 .await; 598 } 599 600 if let Err(e) = sqlx::query!( 601 "INSERT INTO channel_verifications (user_id, channel, code, pending_identifier, expires_at) VALUES ($1, $2::comms_channel, $3, $4, $5)", 602 user_id, 603 verification_channel as _, 604 verification_code, 605 verification_recipient, 606 verification_code_expires_at 607 ) 608 .execute(&mut *tx) 609 .await 610 { 611 error!("Error inserting channel verification: {:?}", e); 612 return ( 613 StatusCode::INTERNAL_SERVER_ERROR, 614 Json(json!({"error": "InternalError"})), 615 ) 616 .into_response(); 617 } 618 619 if let Err(e) = tx.commit().await { 620 error!("Error committing transaction: {:?}", e); 621 return ( 622 StatusCode::INTERNAL_SERVER_ERROR, 623 Json(json!({"error": "InternalError"})), 624 ) 625 .into_response(); 626 } 627 628 if let Err(e) = 629 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle)).await 630 { 631 warn!("Failed to sequence identity event for {}: {}", did, e); 632 } 633 634 if let Err(e) = crate::comms::enqueue_signup_verification( 635 &state.db, 636 user_id, 637 verification_channel, 638 &verification_recipient, 639 &verification_code, 640 ) 641 .await 642 { 643 warn!("Failed to enqueue signup verification: {:?}", e); 644 } 645 646 info!(did = %did, handle = %handle, "Passkey-only account created, awaiting setup completion"); 647 648 Json(CreatePasskeyAccountResponse { 649 did, 650 handle, 651 setup_token, 652 setup_expires_at, 653 }) 654 .into_response() 655} 656 657#[derive(Deserialize)] 658#[serde(rename_all = "camelCase")] 659pub struct CompletePasskeySetupInput { 660 pub did: String, 661 pub setup_token: String, 662 pub passkey_credential: serde_json::Value, 663 pub passkey_friendly_name: Option<String>, 664} 665 666#[derive(Serialize)] 667#[serde(rename_all = "camelCase")] 668pub struct CompletePasskeySetupResponse { 669 pub did: String, 670 pub handle: String, 671 pub app_password: String, 672 pub app_password_name: String, 673} 674 675pub async fn complete_passkey_setup( 676 State(state): State<AppState>, 677 Json(input): Json<CompletePasskeySetupInput>, 678) -> Response { 679 let user = sqlx::query!( 680 r#"SELECT id, handle, recovery_token, recovery_token_expires_at, password_required 681 FROM users WHERE did = $1"#, 682 input.did 683 ) 684 .fetch_optional(&state.db) 685 .await; 686 687 let user = match user { 688 Ok(Some(u)) => u, 689 Ok(None) => { 690 return ( 691 StatusCode::NOT_FOUND, 692 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 693 ) 694 .into_response(); 695 } 696 Err(e) => { 697 error!("DB error: {:?}", e); 698 return ( 699 StatusCode::INTERNAL_SERVER_ERROR, 700 Json(json!({"error": "InternalError"})), 701 ) 702 .into_response(); 703 } 704 }; 705 706 if user.password_required { 707 return ( 708 StatusCode::BAD_REQUEST, 709 Json(json!({"error": "InvalidAccount", "message": "This account is not a passkey-only account"})), 710 ) 711 .into_response(); 712 } 713 714 let token_hash = match &user.recovery_token { 715 Some(h) => h, 716 None => { 717 return ( 718 StatusCode::BAD_REQUEST, 719 Json(json!({"error": "SetupExpired", "message": "Setup has already been completed or expired"})), 720 ) 721 .into_response(); 722 } 723 }; 724 725 if let Some(expires_at) = user.recovery_token_expires_at 726 && expires_at < Utc::now() 727 { 728 return ( 729 StatusCode::BAD_REQUEST, 730 Json(json!({"error": "SetupExpired", "message": "Setup token has expired"})), 731 ) 732 .into_response(); 733 } 734 735 if !bcrypt::verify(&input.setup_token, token_hash).unwrap_or(false) { 736 return ( 737 StatusCode::UNAUTHORIZED, 738 Json(json!({"error": "InvalidToken", "message": "Invalid setup token"})), 739 ) 740 .into_response(); 741 } 742 743 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 744 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 745 Ok(w) => w, 746 Err(e) => { 747 error!("Failed to create WebAuthn config: {:?}", e); 748 return ( 749 StatusCode::INTERNAL_SERVER_ERROR, 750 Json(json!({"error": "InternalError"})), 751 ) 752 .into_response(); 753 } 754 }; 755 756 let reg_state = match crate::auth::webauthn::load_registration_state(&state.db, &input.did) 757 .await 758 { 759 Ok(Some(s)) => s, 760 Ok(None) => { 761 return ( 762 StatusCode::BAD_REQUEST, 763 Json(json!({"error": "NoChallengeInProgress", "message": "Please start passkey registration first"})), 764 ) 765 .into_response(); 766 } 767 Err(e) => { 768 error!("Error loading registration state: {:?}", e); 769 return ( 770 StatusCode::INTERNAL_SERVER_ERROR, 771 Json(json!({"error": "InternalError"})), 772 ) 773 .into_response(); 774 } 775 }; 776 777 let credential: webauthn_rs::prelude::RegisterPublicKeyCredential = match serde_json::from_value( 778 input.passkey_credential, 779 ) { 780 Ok(c) => c, 781 Err(e) => { 782 warn!("Failed to parse credential: {:?}", e); 783 return ( 784 StatusCode::BAD_REQUEST, 785 Json( 786 json!({"error": "InvalidCredential", "message": "Failed to parse credential"}), 787 ), 788 ) 789 .into_response(); 790 } 791 }; 792 793 let security_key = match webauthn.finish_registration(&credential, &reg_state) { 794 Ok(sk) => sk, 795 Err(e) => { 796 warn!("Passkey registration failed: {:?}", e); 797 return ( 798 StatusCode::BAD_REQUEST, 799 Json(json!({"error": "RegistrationFailed", "message": "Passkey registration failed"})), 800 ) 801 .into_response(); 802 } 803 }; 804 805 if let Err(e) = crate::auth::webauthn::save_passkey( 806 &state.db, 807 &input.did, 808 &security_key, 809 input.passkey_friendly_name.as_deref(), 810 ) 811 .await 812 { 813 error!("Error saving passkey: {:?}", e); 814 return ( 815 StatusCode::INTERNAL_SERVER_ERROR, 816 Json(json!({"error": "InternalError"})), 817 ) 818 .into_response(); 819 } 820 821 let _ = crate::auth::webauthn::delete_registration_state(&state.db, &input.did).await; 822 823 let app_password = generate_app_password(); 824 let app_password_name = "bsky.app".to_string(); 825 let password_hash = match hash(&app_password, DEFAULT_COST) { 826 Ok(h) => h, 827 Err(e) => { 828 error!("Error hashing app password: {:?}", e); 829 return ( 830 StatusCode::INTERNAL_SERVER_ERROR, 831 Json(json!({"error": "InternalError"})), 832 ) 833 .into_response(); 834 } 835 }; 836 837 if let Err(e) = sqlx::query!( 838 "INSERT INTO app_passwords (user_id, name, password_hash, privileged) VALUES ($1, $2, $3, FALSE)", 839 user.id, 840 app_password_name, 841 password_hash 842 ) 843 .execute(&state.db) 844 .await 845 { 846 error!("Error creating app password: {:?}", e); 847 return ( 848 StatusCode::INTERNAL_SERVER_ERROR, 849 Json(json!({"error": "InternalError"})), 850 ) 851 .into_response(); 852 } 853 854 if let Err(e) = sqlx::query!( 855 "UPDATE users SET recovery_token = NULL, recovery_token_expires_at = NULL WHERE did = $1", 856 input.did 857 ) 858 .execute(&state.db) 859 .await 860 { 861 error!("Error clearing setup token: {:?}", e); 862 } 863 864 info!(did = %input.did, "Passkey-only account setup completed"); 865 866 Json(CompletePasskeySetupResponse { 867 did: input.did, 868 handle: user.handle, 869 app_password, 870 app_password_name, 871 }) 872 .into_response() 873} 874 875pub async fn start_passkey_registration_for_setup( 876 State(state): State<AppState>, 877 Json(input): Json<StartPasskeyRegistrationInput>, 878) -> Response { 879 let user = sqlx::query!( 880 r#"SELECT handle, recovery_token, recovery_token_expires_at, password_required 881 FROM users WHERE did = $1"#, 882 input.did 883 ) 884 .fetch_optional(&state.db) 885 .await; 886 887 let user = match user { 888 Ok(Some(u)) => u, 889 Ok(None) => { 890 return ( 891 StatusCode::NOT_FOUND, 892 Json(json!({"error": "AccountNotFound"})), 893 ) 894 .into_response(); 895 } 896 Err(e) => { 897 error!("DB error: {:?}", e); 898 return ( 899 StatusCode::INTERNAL_SERVER_ERROR, 900 Json(json!({"error": "InternalError"})), 901 ) 902 .into_response(); 903 } 904 }; 905 906 if user.password_required { 907 return ( 908 StatusCode::BAD_REQUEST, 909 Json(json!({"error": "InvalidAccount"})), 910 ) 911 .into_response(); 912 } 913 914 let token_hash = match &user.recovery_token { 915 Some(h) => h, 916 None => { 917 return ( 918 StatusCode::BAD_REQUEST, 919 Json(json!({"error": "SetupExpired"})), 920 ) 921 .into_response(); 922 } 923 }; 924 925 if let Some(expires_at) = user.recovery_token_expires_at 926 && expires_at < Utc::now() 927 { 928 return ( 929 StatusCode::BAD_REQUEST, 930 Json(json!({"error": "SetupExpired"})), 931 ) 932 .into_response(); 933 } 934 935 if !bcrypt::verify(&input.setup_token, token_hash).unwrap_or(false) { 936 return ( 937 StatusCode::UNAUTHORIZED, 938 Json(json!({"error": "InvalidToken"})), 939 ) 940 .into_response(); 941 } 942 943 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 944 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 945 Ok(w) => w, 946 Err(e) => { 947 error!("Failed to create WebAuthn config: {:?}", e); 948 return ( 949 StatusCode::INTERNAL_SERVER_ERROR, 950 Json(json!({"error": "InternalError"})), 951 ) 952 .into_response(); 953 } 954 }; 955 956 let existing_passkeys = crate::auth::webauthn::get_passkeys_for_user(&state.db, &input.did) 957 .await 958 .unwrap_or_default(); 959 960 let exclude_credentials: Vec<webauthn_rs::prelude::CredentialID> = existing_passkeys 961 .iter() 962 .map(|p| webauthn_rs::prelude::CredentialID::from(p.credential_id.clone())) 963 .collect(); 964 965 let display_name = input.friendly_name.as_deref().unwrap_or(&user.handle); 966 967 let (ccr, reg_state) = match webauthn.start_registration( 968 &input.did, 969 &user.handle, 970 display_name, 971 exclude_credentials, 972 ) { 973 Ok(result) => result, 974 Err(e) => { 975 error!("Failed to start passkey registration: {:?}", e); 976 return ( 977 StatusCode::INTERNAL_SERVER_ERROR, 978 Json(json!({"error": "InternalError"})), 979 ) 980 .into_response(); 981 } 982 }; 983 984 if let Err(e) = 985 crate::auth::webauthn::save_registration_state(&state.db, &input.did, &reg_state).await 986 { 987 error!("Failed to save registration state: {:?}", e); 988 return ( 989 StatusCode::INTERNAL_SERVER_ERROR, 990 Json(json!({"error": "InternalError"})), 991 ) 992 .into_response(); 993 } 994 995 let options = serde_json::to_value(&ccr).unwrap_or(json!({})); 996 Json(json!({"options": options})).into_response() 997} 998 999#[derive(Deserialize)] 1000#[serde(rename_all = "camelCase")] 1001pub struct StartPasskeyRegistrationInput { 1002 pub did: String, 1003 pub setup_token: String, 1004 pub friendly_name: Option<String>, 1005} 1006 1007#[derive(Deserialize)] 1008#[serde(rename_all = "camelCase")] 1009pub struct RequestPasskeyRecoveryInput { 1010 #[serde(alias = "identifier")] 1011 pub email: String, 1012} 1013 1014pub async fn request_passkey_recovery( 1015 State(state): State<AppState>, 1016 headers: HeaderMap, 1017 Json(input): Json<RequestPasskeyRecoveryInput>, 1018) -> Response { 1019 let client_ip = extract_client_ip(&headers); 1020 if !state 1021 .check_rate_limit(RateLimitKind::PasswordReset, &client_ip) 1022 .await 1023 { 1024 return ( 1025 StatusCode::TOO_MANY_REQUESTS, 1026 Json(json!({"error": "RateLimitExceeded"})), 1027 ) 1028 .into_response(); 1029 } 1030 1031 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1032 let identifier = input.email.trim().to_lowercase(); 1033 let identifier = identifier.strip_prefix('@').unwrap_or(&identifier); 1034 let normalized_handle = if identifier.contains('@') || identifier.contains('.') { 1035 identifier.to_string() 1036 } else { 1037 format!("{}.{}", identifier, pds_hostname) 1038 }; 1039 1040 let user = sqlx::query!( 1041 "SELECT id, did, handle, password_required FROM users WHERE LOWER(email) = $1 OR handle = $2", 1042 identifier, 1043 normalized_handle 1044 ) 1045 .fetch_optional(&state.db) 1046 .await; 1047 1048 let user = match user { 1049 Ok(Some(u)) if !u.password_required => u, 1050 _ => { 1051 return Json(json!({"success": true})).into_response(); 1052 } 1053 }; 1054 1055 let recovery_token = generate_setup_token(); 1056 let recovery_token_hash = match hash(&recovery_token, DEFAULT_COST) { 1057 Ok(h) => h, 1058 Err(_) => { 1059 return ( 1060 StatusCode::INTERNAL_SERVER_ERROR, 1061 Json(json!({"error": "InternalError"})), 1062 ) 1063 .into_response(); 1064 } 1065 }; 1066 let expires_at = Utc::now() + Duration::hours(1); 1067 1068 if let Err(e) = sqlx::query!( 1069 "UPDATE users SET recovery_token = $1, recovery_token_expires_at = $2 WHERE did = $3", 1070 recovery_token_hash, 1071 expires_at, 1072 user.did 1073 ) 1074 .execute(&state.db) 1075 .await 1076 { 1077 error!("Error updating recovery token: {:?}", e); 1078 return ( 1079 StatusCode::INTERNAL_SERVER_ERROR, 1080 Json(json!({"error": "InternalError"})), 1081 ) 1082 .into_response(); 1083 } 1084 1085 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1086 let recovery_url = format!( 1087 "https://{}/#/recover-passkey?did={}&token={}", 1088 hostname, 1089 urlencoding::encode(&user.did), 1090 urlencoding::encode(&recovery_token) 1091 ); 1092 1093 let _ = 1094 crate::comms::enqueue_passkey_recovery(&state.db, user.id, &recovery_url, &hostname).await; 1095 1096 info!(did = %user.did, "Passkey recovery requested"); 1097 Json(json!({"success": true})).into_response() 1098} 1099 1100#[derive(Deserialize)] 1101#[serde(rename_all = "camelCase")] 1102pub struct RecoverPasskeyAccountInput { 1103 pub did: String, 1104 pub recovery_token: String, 1105 pub new_password: String, 1106} 1107 1108pub async fn recover_passkey_account( 1109 State(state): State<AppState>, 1110 Json(input): Json<RecoverPasskeyAccountInput>, 1111) -> Response { 1112 if let Err(e) = validate_password(&input.new_password) { 1113 return ( 1114 StatusCode::BAD_REQUEST, 1115 Json(json!({ 1116 "error": "InvalidPassword", 1117 "message": e.to_string() 1118 })), 1119 ) 1120 .into_response(); 1121 } 1122 1123 let user = sqlx::query!( 1124 "SELECT id, did, recovery_token, recovery_token_expires_at FROM users WHERE did = $1", 1125 input.did 1126 ) 1127 .fetch_optional(&state.db) 1128 .await; 1129 1130 let user = match user { 1131 Ok(Some(u)) => u, 1132 _ => { 1133 return ( 1134 StatusCode::NOT_FOUND, 1135 Json(json!({"error": "InvalidRecoveryLink"})), 1136 ) 1137 .into_response(); 1138 } 1139 }; 1140 1141 let token_hash = match &user.recovery_token { 1142 Some(h) => h, 1143 None => { 1144 return ( 1145 StatusCode::BAD_REQUEST, 1146 Json(json!({"error": "InvalidRecoveryLink"})), 1147 ) 1148 .into_response(); 1149 } 1150 }; 1151 1152 if let Some(expires_at) = user.recovery_token_expires_at 1153 && expires_at < Utc::now() 1154 { 1155 return ( 1156 StatusCode::BAD_REQUEST, 1157 Json(json!({"error": "RecoveryLinkExpired"})), 1158 ) 1159 .into_response(); 1160 } 1161 1162 if !bcrypt::verify(&input.recovery_token, token_hash).unwrap_or(false) { 1163 return ( 1164 StatusCode::UNAUTHORIZED, 1165 Json(json!({"error": "InvalidRecoveryLink"})), 1166 ) 1167 .into_response(); 1168 } 1169 1170 let password_hash = match hash(&input.new_password, DEFAULT_COST) { 1171 Ok(h) => h, 1172 Err(_) => { 1173 return ( 1174 StatusCode::INTERNAL_SERVER_ERROR, 1175 Json(json!({"error": "InternalError"})), 1176 ) 1177 .into_response(); 1178 } 1179 }; 1180 1181 if let Err(e) = sqlx::query!( 1182 "UPDATE users SET password_hash = $1, password_required = TRUE, recovery_token = NULL, recovery_token_expires_at = NULL WHERE did = $2", 1183 password_hash, 1184 input.did 1185 ) 1186 .execute(&state.db) 1187 .await 1188 { 1189 error!("Error updating password: {:?}", e); 1190 return ( 1191 StatusCode::INTERNAL_SERVER_ERROR, 1192 Json(json!({"error": "InternalError"})), 1193 ) 1194 .into_response(); 1195 } 1196 1197 let deleted = sqlx::query!("DELETE FROM passkeys WHERE did = $1", input.did) 1198 .execute(&state.db) 1199 .await; 1200 match deleted { 1201 Ok(result) => { 1202 if result.rows_affected() > 0 { 1203 info!(did = %input.did, count = result.rows_affected(), "Deleted lost passkeys during account recovery"); 1204 } 1205 } 1206 Err(e) => { 1207 warn!(did = %input.did, "Failed to delete passkeys during recovery: {:?}", e); 1208 } 1209 } 1210 1211 info!(did = %input.did, "Passkey-only account recovered with temporary password"); 1212 Json(json!({"success": true})).into_response() 1213}