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