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 let rev_str = rev.as_ref().to_string(); 616 if let Err(e) = sqlx::query!( 617 "INSERT INTO repos (user_id, repo_root_cid, repo_rev) VALUES ($1, $2, $3)", 618 user_id, 619 commit_cid_str, 620 rev_str 621 ) 622 .execute(&mut *tx) 623 .await 624 { 625 error!("Error inserting repo: {:?}", e); 626 return ( 627 StatusCode::INTERNAL_SERVER_ERROR, 628 Json(json!({"error": "InternalError"})), 629 ) 630 .into_response(); 631 } 632 let genesis_block_cids = vec![mst_root.to_bytes(), commit_cid.to_bytes()]; 633 if let Err(e) = sqlx::query!( 634 r#" 635 INSERT INTO user_blocks (user_id, block_cid) 636 SELECT $1, block_cid FROM UNNEST($2::bytea[]) AS t(block_cid) 637 ON CONFLICT (user_id, block_cid) DO NOTHING 638 "#, 639 user_id, 640 &genesis_block_cids 641 ) 642 .execute(&mut *tx) 643 .await 644 { 645 error!("Error inserting user_blocks: {:?}", e); 646 return ( 647 StatusCode::INTERNAL_SERVER_ERROR, 648 Json(json!({"error": "InternalError"})), 649 ) 650 .into_response(); 651 } 652 653 if let Some(ref code) = input.invite_code { 654 let _ = sqlx::query!( 655 "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1", 656 code 657 ) 658 .execute(&mut *tx) 659 .await; 660 661 let _ = sqlx::query!( 662 "INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)", 663 code, 664 user_id 665 ) 666 .execute(&mut *tx) 667 .await; 668 } 669 670 if let Err(e) = tx.commit().await { 671 error!("Error committing transaction: {:?}", e); 672 return ( 673 StatusCode::INTERNAL_SERVER_ERROR, 674 Json(json!({"error": "InternalError"})), 675 ) 676 .into_response(); 677 } 678 679 if !is_byod_did_web { 680 if let Err(e) = 681 crate::api::repo::record::sequence_identity_event(&state, &did, Some(&handle)).await 682 { 683 warn!("Failed to sequence identity event for {}: {}", did, e); 684 } 685 if let Err(e) = 686 crate::api::repo::record::sequence_account_event(&state, &did, true, None).await 687 { 688 warn!("Failed to sequence account event for {}: {}", did, e); 689 } 690 let profile_record = serde_json::json!({ 691 "$type": "app.bsky.actor.profile", 692 "displayName": handle 693 }); 694 if let Err(e) = crate::api::repo::record::create_record_internal( 695 &state, 696 &did, 697 "app.bsky.actor.profile", 698 "self", 699 &profile_record, 700 ) 701 .await 702 { 703 warn!("Failed to create default profile for {}: {}", did, e); 704 } 705 } 706 707 let verification_token = crate::auth::verification_token::generate_signup_token( 708 &did, 709 verification_channel, 710 &verification_recipient, 711 ); 712 let formatted_token = 713 crate::auth::verification_token::format_token_for_display(&verification_token); 714 if let Err(e) = crate::comms::enqueue_signup_verification( 715 &state.db, 716 user_id, 717 verification_channel, 718 &verification_recipient, 719 &formatted_token, 720 None, 721 ) 722 .await 723 { 724 warn!("Failed to enqueue signup verification: {:?}", e); 725 } 726 727 info!(did = %did, handle = %handle, "Passkey-only account created, awaiting setup completion"); 728 729 Json(CreatePasskeyAccountResponse { 730 did, 731 handle, 732 setup_token, 733 setup_expires_at, 734 }) 735 .into_response() 736} 737 738#[derive(Deserialize)] 739#[serde(rename_all = "camelCase")] 740pub struct CompletePasskeySetupInput { 741 pub did: String, 742 pub setup_token: String, 743 pub passkey_credential: serde_json::Value, 744 pub passkey_friendly_name: Option<String>, 745} 746 747#[derive(Serialize)] 748#[serde(rename_all = "camelCase")] 749pub struct CompletePasskeySetupResponse { 750 pub did: String, 751 pub handle: String, 752 pub app_password: String, 753 pub app_password_name: String, 754} 755 756pub async fn complete_passkey_setup( 757 State(state): State<AppState>, 758 Json(input): Json<CompletePasskeySetupInput>, 759) -> Response { 760 let user = sqlx::query!( 761 r#"SELECT id, handle, recovery_token, recovery_token_expires_at, password_required 762 FROM users WHERE did = $1"#, 763 input.did 764 ) 765 .fetch_optional(&state.db) 766 .await; 767 768 let user = match user { 769 Ok(Some(u)) => u, 770 Ok(None) => { 771 return ( 772 StatusCode::NOT_FOUND, 773 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 774 ) 775 .into_response(); 776 } 777 Err(e) => { 778 error!("DB error: {:?}", e); 779 return ( 780 StatusCode::INTERNAL_SERVER_ERROR, 781 Json(json!({"error": "InternalError"})), 782 ) 783 .into_response(); 784 } 785 }; 786 787 if user.password_required { 788 return ( 789 StatusCode::BAD_REQUEST, 790 Json(json!({"error": "InvalidAccount", "message": "This account is not a passkey-only account"})), 791 ) 792 .into_response(); 793 } 794 795 let token_hash = match &user.recovery_token { 796 Some(h) => h, 797 None => { 798 return ( 799 StatusCode::BAD_REQUEST, 800 Json(json!({"error": "SetupExpired", "message": "Setup has already been completed or expired"})), 801 ) 802 .into_response(); 803 } 804 }; 805 806 if let Some(expires_at) = user.recovery_token_expires_at 807 && expires_at < Utc::now() 808 { 809 return ( 810 StatusCode::BAD_REQUEST, 811 Json(json!({"error": "SetupExpired", "message": "Setup token has expired"})), 812 ) 813 .into_response(); 814 } 815 816 if !bcrypt::verify(&input.setup_token, token_hash).unwrap_or(false) { 817 return ( 818 StatusCode::UNAUTHORIZED, 819 Json(json!({"error": "InvalidToken", "message": "Invalid setup token"})), 820 ) 821 .into_response(); 822 } 823 824 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 825 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 826 Ok(w) => w, 827 Err(e) => { 828 error!("Failed to create WebAuthn config: {:?}", e); 829 return ( 830 StatusCode::INTERNAL_SERVER_ERROR, 831 Json(json!({"error": "InternalError"})), 832 ) 833 .into_response(); 834 } 835 }; 836 837 let reg_state = match crate::auth::webauthn::load_registration_state(&state.db, &input.did) 838 .await 839 { 840 Ok(Some(s)) => s, 841 Ok(None) => { 842 return ( 843 StatusCode::BAD_REQUEST, 844 Json(json!({"error": "NoChallengeInProgress", "message": "Please start passkey registration first"})), 845 ) 846 .into_response(); 847 } 848 Err(e) => { 849 error!("Error loading registration state: {:?}", e); 850 return ( 851 StatusCode::INTERNAL_SERVER_ERROR, 852 Json(json!({"error": "InternalError"})), 853 ) 854 .into_response(); 855 } 856 }; 857 858 let credential: webauthn_rs::prelude::RegisterPublicKeyCredential = 859 match serde_json::from_value(input.passkey_credential) { 860 Ok(c) => c, 861 Err(e) => { 862 warn!("Failed to parse credential: {:?}", e); 863 return ( 864 StatusCode::BAD_REQUEST, 865 Json( 866 json!({"error": "InvalidCredential", "message": "Failed to parse credential"}), 867 ), 868 ) 869 .into_response(); 870 } 871 }; 872 873 let security_key = match webauthn.finish_registration(&credential, &reg_state) { 874 Ok(sk) => sk, 875 Err(e) => { 876 warn!("Passkey registration failed: {:?}", e); 877 return ( 878 StatusCode::BAD_REQUEST, 879 Json(json!({"error": "RegistrationFailed", "message": "Passkey registration failed"})), 880 ) 881 .into_response(); 882 } 883 }; 884 885 if let Err(e) = crate::auth::webauthn::save_passkey( 886 &state.db, 887 &input.did, 888 &security_key, 889 input.passkey_friendly_name.as_deref(), 890 ) 891 .await 892 { 893 error!("Error saving passkey: {:?}", e); 894 return ( 895 StatusCode::INTERNAL_SERVER_ERROR, 896 Json(json!({"error": "InternalError"})), 897 ) 898 .into_response(); 899 } 900 901 let _ = crate::auth::webauthn::delete_registration_state(&state.db, &input.did).await; 902 903 let app_password = generate_app_password(); 904 let app_password_name = "bsky.app".to_string(); 905 let password_hash = match hash(&app_password, DEFAULT_COST) { 906 Ok(h) => h, 907 Err(e) => { 908 error!("Error hashing app password: {:?}", e); 909 return ( 910 StatusCode::INTERNAL_SERVER_ERROR, 911 Json(json!({"error": "InternalError"})), 912 ) 913 .into_response(); 914 } 915 }; 916 917 if let Err(e) = sqlx::query!( 918 "INSERT INTO app_passwords (user_id, name, password_hash, privileged) VALUES ($1, $2, $3, FALSE)", 919 user.id, 920 app_password_name, 921 password_hash 922 ) 923 .execute(&state.db) 924 .await 925 { 926 error!("Error creating app password: {:?}", e); 927 return ( 928 StatusCode::INTERNAL_SERVER_ERROR, 929 Json(json!({"error": "InternalError"})), 930 ) 931 .into_response(); 932 } 933 934 if let Err(e) = sqlx::query!( 935 "UPDATE users SET recovery_token = NULL, recovery_token_expires_at = NULL WHERE did = $1", 936 input.did 937 ) 938 .execute(&state.db) 939 .await 940 { 941 error!("Error clearing setup token: {:?}", e); 942 } 943 944 info!(did = %input.did, "Passkey-only account setup completed"); 945 946 Json(CompletePasskeySetupResponse { 947 did: input.did, 948 handle: user.handle, 949 app_password, 950 app_password_name, 951 }) 952 .into_response() 953} 954 955pub async fn start_passkey_registration_for_setup( 956 State(state): State<AppState>, 957 Json(input): Json<StartPasskeyRegistrationInput>, 958) -> Response { 959 let user = sqlx::query!( 960 r#"SELECT handle, recovery_token, recovery_token_expires_at, password_required 961 FROM users WHERE did = $1"#, 962 input.did 963 ) 964 .fetch_optional(&state.db) 965 .await; 966 967 let user = match user { 968 Ok(Some(u)) => u, 969 Ok(None) => { 970 return ( 971 StatusCode::NOT_FOUND, 972 Json(json!({"error": "AccountNotFound"})), 973 ) 974 .into_response(); 975 } 976 Err(e) => { 977 error!("DB error: {:?}", e); 978 return ( 979 StatusCode::INTERNAL_SERVER_ERROR, 980 Json(json!({"error": "InternalError"})), 981 ) 982 .into_response(); 983 } 984 }; 985 986 if user.password_required { 987 return ( 988 StatusCode::BAD_REQUEST, 989 Json(json!({"error": "InvalidAccount"})), 990 ) 991 .into_response(); 992 } 993 994 let token_hash = match &user.recovery_token { 995 Some(h) => h, 996 None => { 997 return ( 998 StatusCode::BAD_REQUEST, 999 Json(json!({"error": "SetupExpired"})), 1000 ) 1001 .into_response(); 1002 } 1003 }; 1004 1005 if let Some(expires_at) = user.recovery_token_expires_at 1006 && expires_at < Utc::now() 1007 { 1008 return ( 1009 StatusCode::BAD_REQUEST, 1010 Json(json!({"error": "SetupExpired"})), 1011 ) 1012 .into_response(); 1013 } 1014 1015 if !bcrypt::verify(&input.setup_token, token_hash).unwrap_or(false) { 1016 return ( 1017 StatusCode::UNAUTHORIZED, 1018 Json(json!({"error": "InvalidToken"})), 1019 ) 1020 .into_response(); 1021 } 1022 1023 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1024 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 1025 Ok(w) => w, 1026 Err(e) => { 1027 error!("Failed to create WebAuthn config: {:?}", e); 1028 return ( 1029 StatusCode::INTERNAL_SERVER_ERROR, 1030 Json(json!({"error": "InternalError"})), 1031 ) 1032 .into_response(); 1033 } 1034 }; 1035 1036 let existing_passkeys = crate::auth::webauthn::get_passkeys_for_user(&state.db, &input.did) 1037 .await 1038 .unwrap_or_default(); 1039 1040 let exclude_credentials: Vec<webauthn_rs::prelude::CredentialID> = existing_passkeys 1041 .iter() 1042 .map(|p| webauthn_rs::prelude::CredentialID::from(p.credential_id.clone())) 1043 .collect(); 1044 1045 let display_name = input.friendly_name.as_deref().unwrap_or(&user.handle); 1046 1047 let (ccr, reg_state) = match webauthn.start_registration( 1048 &input.did, 1049 &user.handle, 1050 display_name, 1051 exclude_credentials, 1052 ) { 1053 Ok(result) => result, 1054 Err(e) => { 1055 error!("Failed to start passkey registration: {:?}", e); 1056 return ( 1057 StatusCode::INTERNAL_SERVER_ERROR, 1058 Json(json!({"error": "InternalError"})), 1059 ) 1060 .into_response(); 1061 } 1062 }; 1063 1064 if let Err(e) = 1065 crate::auth::webauthn::save_registration_state(&state.db, &input.did, &reg_state).await 1066 { 1067 error!("Failed to save registration state: {:?}", e); 1068 return ( 1069 StatusCode::INTERNAL_SERVER_ERROR, 1070 Json(json!({"error": "InternalError"})), 1071 ) 1072 .into_response(); 1073 } 1074 1075 let options = serde_json::to_value(&ccr).unwrap_or(json!({})); 1076 Json(json!({"options": options})).into_response() 1077} 1078 1079#[derive(Deserialize)] 1080#[serde(rename_all = "camelCase")] 1081pub struct StartPasskeyRegistrationInput { 1082 pub did: String, 1083 pub setup_token: String, 1084 pub friendly_name: Option<String>, 1085} 1086 1087#[derive(Deserialize)] 1088#[serde(rename_all = "camelCase")] 1089pub struct RequestPasskeyRecoveryInput { 1090 #[serde(alias = "identifier")] 1091 pub email: String, 1092} 1093 1094pub async fn request_passkey_recovery( 1095 State(state): State<AppState>, 1096 headers: HeaderMap, 1097 Json(input): Json<RequestPasskeyRecoveryInput>, 1098) -> Response { 1099 let client_ip = extract_client_ip(&headers); 1100 if !state 1101 .check_rate_limit(RateLimitKind::PasswordReset, &client_ip) 1102 .await 1103 { 1104 return ( 1105 StatusCode::TOO_MANY_REQUESTS, 1106 Json(json!({"error": "RateLimitExceeded"})), 1107 ) 1108 .into_response(); 1109 } 1110 1111 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1112 let identifier = input.email.trim().to_lowercase(); 1113 let identifier = identifier.strip_prefix('@').unwrap_or(&identifier); 1114 let normalized_handle = if identifier.contains('@') || identifier.contains('.') { 1115 identifier.to_string() 1116 } else { 1117 format!("{}.{}", identifier, pds_hostname) 1118 }; 1119 1120 let user = sqlx::query!( 1121 "SELECT id, did, handle, password_required FROM users WHERE LOWER(email) = $1 OR handle = $2", 1122 identifier, 1123 normalized_handle 1124 ) 1125 .fetch_optional(&state.db) 1126 .await; 1127 1128 let user = match user { 1129 Ok(Some(u)) if !u.password_required => u, 1130 _ => { 1131 return Json(json!({"success": true})).into_response(); 1132 } 1133 }; 1134 1135 let recovery_token = generate_setup_token(); 1136 let recovery_token_hash = match hash(&recovery_token, DEFAULT_COST) { 1137 Ok(h) => h, 1138 Err(_) => { 1139 return ( 1140 StatusCode::INTERNAL_SERVER_ERROR, 1141 Json(json!({"error": "InternalError"})), 1142 ) 1143 .into_response(); 1144 } 1145 }; 1146 let expires_at = Utc::now() + Duration::hours(1); 1147 1148 if let Err(e) = sqlx::query!( 1149 "UPDATE users SET recovery_token = $1, recovery_token_expires_at = $2 WHERE did = $3", 1150 recovery_token_hash, 1151 expires_at, 1152 user.did 1153 ) 1154 .execute(&state.db) 1155 .await 1156 { 1157 error!("Error updating recovery token: {:?}", e); 1158 return ( 1159 StatusCode::INTERNAL_SERVER_ERROR, 1160 Json(json!({"error": "InternalError"})), 1161 ) 1162 .into_response(); 1163 } 1164 1165 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1166 let recovery_url = format!( 1167 "https://{}/#/recover-passkey?did={}&token={}", 1168 hostname, 1169 urlencoding::encode(&user.did), 1170 urlencoding::encode(&recovery_token) 1171 ); 1172 1173 let _ = 1174 crate::comms::enqueue_passkey_recovery(&state.db, user.id, &recovery_url, &hostname).await; 1175 1176 info!(did = %user.did, "Passkey recovery requested"); 1177 Json(json!({"success": true})).into_response() 1178} 1179 1180#[derive(Deserialize)] 1181#[serde(rename_all = "camelCase")] 1182pub struct RecoverPasskeyAccountInput { 1183 pub did: String, 1184 pub recovery_token: String, 1185 pub new_password: String, 1186} 1187 1188pub async fn recover_passkey_account( 1189 State(state): State<AppState>, 1190 Json(input): Json<RecoverPasskeyAccountInput>, 1191) -> Response { 1192 if let Err(e) = validate_password(&input.new_password) { 1193 return ( 1194 StatusCode::BAD_REQUEST, 1195 Json(json!({ 1196 "error": "InvalidPassword", 1197 "message": e.to_string() 1198 })), 1199 ) 1200 .into_response(); 1201 } 1202 1203 let user = sqlx::query!( 1204 "SELECT id, did, recovery_token, recovery_token_expires_at FROM users WHERE did = $1", 1205 input.did 1206 ) 1207 .fetch_optional(&state.db) 1208 .await; 1209 1210 let user = match user { 1211 Ok(Some(u)) => u, 1212 _ => { 1213 return ( 1214 StatusCode::NOT_FOUND, 1215 Json(json!({"error": "InvalidRecoveryLink"})), 1216 ) 1217 .into_response(); 1218 } 1219 }; 1220 1221 let token_hash = match &user.recovery_token { 1222 Some(h) => h, 1223 None => { 1224 return ( 1225 StatusCode::BAD_REQUEST, 1226 Json(json!({"error": "InvalidRecoveryLink"})), 1227 ) 1228 .into_response(); 1229 } 1230 }; 1231 1232 if let Some(expires_at) = user.recovery_token_expires_at 1233 && expires_at < Utc::now() 1234 { 1235 return ( 1236 StatusCode::BAD_REQUEST, 1237 Json(json!({"error": "RecoveryLinkExpired"})), 1238 ) 1239 .into_response(); 1240 } 1241 1242 if !bcrypt::verify(&input.recovery_token, token_hash).unwrap_or(false) { 1243 return ( 1244 StatusCode::UNAUTHORIZED, 1245 Json(json!({"error": "InvalidRecoveryLink"})), 1246 ) 1247 .into_response(); 1248 } 1249 1250 let password_hash = match hash(&input.new_password, DEFAULT_COST) { 1251 Ok(h) => h, 1252 Err(_) => { 1253 return ( 1254 StatusCode::INTERNAL_SERVER_ERROR, 1255 Json(json!({"error": "InternalError"})), 1256 ) 1257 .into_response(); 1258 } 1259 }; 1260 1261 if let Err(e) = sqlx::query!( 1262 "UPDATE users SET password_hash = $1, password_required = TRUE, recovery_token = NULL, recovery_token_expires_at = NULL WHERE did = $2", 1263 password_hash, 1264 input.did 1265 ) 1266 .execute(&state.db) 1267 .await 1268 { 1269 error!("Error updating password: {:?}", e); 1270 return ( 1271 StatusCode::INTERNAL_SERVER_ERROR, 1272 Json(json!({"error": "InternalError"})), 1273 ) 1274 .into_response(); 1275 } 1276 1277 let deleted = sqlx::query!("DELETE FROM passkeys WHERE did = $1", input.did) 1278 .execute(&state.db) 1279 .await; 1280 match deleted { 1281 Ok(result) => { 1282 if result.rows_affected() > 0 { 1283 info!(did = %input.did, count = result.rows_affected(), "Deleted lost passkeys during account recovery"); 1284 } 1285 } 1286 Err(e) => { 1287 warn!(did = %input.did, "Failed to delete passkeys during recovery: {:?}", e); 1288 } 1289 } 1290 1291 info!(did = %input.did, "Passkey-only account recovered with temporary password"); 1292 Json(json!({"success": true})).into_response() 1293}