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