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