this repo has no description
1use crate::comms::{CommsChannel, channel_display_name, enqueue_2fa_code}; 2use crate::oauth::{ 3 Code, DeviceData, DeviceId, OAuthError, SessionId, client::ClientMetadataCache, db, 4}; 5use crate::state::{AppState, RateLimitKind}; 6use axum::{ 7 Json, 8 extract::{Query, State}, 9 http::{ 10 HeaderMap, StatusCode, 11 header::{LOCATION, SET_COOKIE}, 12 }, 13 response::{IntoResponse, Response}, 14}; 15use chrono::Utc; 16use serde::{Deserialize, Serialize}; 17use subtle::ConstantTimeEq; 18use urlencoding::encode as url_encode; 19 20const DEVICE_COOKIE_NAME: &str = "oauth_device_id"; 21 22fn redirect_see_other(uri: &str) -> Response { 23 (StatusCode::SEE_OTHER, [(LOCATION, uri.to_string())]).into_response() 24} 25 26fn redirect_to_frontend_error(error: &str, description: &str) -> Response { 27 redirect_see_other(&format!( 28 "/#/oauth/error?error={}&error_description={}", 29 url_encode(error), 30 url_encode(description) 31 )) 32} 33 34fn extract_device_cookie(headers: &HeaderMap) -> Option<String> { 35 headers 36 .get("cookie") 37 .and_then(|v| v.to_str().ok()) 38 .and_then(|cookie_str| { 39 for cookie in cookie_str.split(';') { 40 let cookie = cookie.trim(); 41 if let Some(value) = cookie.strip_prefix(&format!("{}=", DEVICE_COOKIE_NAME)) { 42 return crate::config::AuthConfig::get().verify_device_cookie(value); 43 } 44 } 45 None 46 }) 47} 48 49fn extract_client_ip(headers: &HeaderMap) -> String { 50 if let Some(forwarded) = headers.get("x-forwarded-for") 51 && let Ok(value) = forwarded.to_str() 52 && let Some(first_ip) = value.split(',').next() 53 { 54 return first_ip.trim().to_string(); 55 } 56 if let Some(real_ip) = headers.get("x-real-ip") 57 && let Ok(value) = real_ip.to_str() 58 { 59 return value.trim().to_string(); 60 } 61 "0.0.0.0".to_string() 62} 63 64fn extract_user_agent(headers: &HeaderMap) -> Option<String> { 65 headers 66 .get("user-agent") 67 .and_then(|v| v.to_str().ok()) 68 .map(|s| s.to_string()) 69} 70 71fn make_device_cookie(device_id: &str) -> String { 72 let signed_value = crate::config::AuthConfig::get().sign_device_cookie(device_id); 73 format!( 74 "{}={}; Path=/oauth; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000", 75 DEVICE_COOKIE_NAME, signed_value 76 ) 77} 78 79#[derive(Debug, Deserialize)] 80pub struct AuthorizeQuery { 81 pub request_uri: Option<String>, 82 pub client_id: Option<String>, 83 pub new_account: Option<bool>, 84} 85 86#[derive(Debug, Serialize)] 87pub struct AuthorizeResponse { 88 pub client_id: String, 89 pub client_name: Option<String>, 90 pub scope: Option<String>, 91 pub redirect_uri: String, 92 pub state: Option<String>, 93 pub login_hint: Option<String>, 94} 95 96#[derive(Debug, Deserialize)] 97pub struct AuthorizeSubmit { 98 pub request_uri: String, 99 pub username: String, 100 pub password: String, 101 #[serde(default)] 102 pub remember_device: bool, 103} 104 105#[derive(Debug, Deserialize)] 106pub struct AuthorizeSelectSubmit { 107 pub request_uri: String, 108 pub did: String, 109} 110 111fn wants_json(headers: &HeaderMap) -> bool { 112 headers 113 .get("accept") 114 .and_then(|v| v.to_str().ok()) 115 .map(|accept| accept.contains("application/json")) 116 .unwrap_or(false) 117} 118 119pub async fn authorize_get( 120 State(state): State<AppState>, 121 headers: HeaderMap, 122 Query(query): Query<AuthorizeQuery>, 123) -> Response { 124 let request_uri = match query.request_uri { 125 Some(uri) => uri, 126 None => { 127 if wants_json(&headers) { 128 return ( 129 StatusCode::BAD_REQUEST, 130 Json(serde_json::json!({ 131 "error": "invalid_request", 132 "error_description": "Missing request_uri parameter. Use PAR to initiate authorization." 133 })), 134 ).into_response(); 135 } 136 return redirect_to_frontend_error( 137 "invalid_request", 138 "Missing request_uri parameter. Use PAR to initiate authorization.", 139 ); 140 } 141 }; 142 let request_data = match db::get_authorization_request(&state.db, &request_uri).await { 143 Ok(Some(data)) => data, 144 Ok(None) => { 145 if wants_json(&headers) { 146 return ( 147 StatusCode::BAD_REQUEST, 148 Json(serde_json::json!({ 149 "error": "invalid_request", 150 "error_description": "Invalid or expired request_uri. Please start a new authorization request." 151 })), 152 ).into_response(); 153 } 154 return redirect_to_frontend_error( 155 "invalid_request", 156 "Invalid or expired request_uri. Please start a new authorization request.", 157 ); 158 } 159 Err(e) => { 160 if wants_json(&headers) { 161 return ( 162 StatusCode::INTERNAL_SERVER_ERROR, 163 Json(serde_json::json!({ 164 "error": "server_error", 165 "error_description": format!("Database error: {:?}", e) 166 })), 167 ) 168 .into_response(); 169 } 170 return redirect_to_frontend_error("server_error", "A database error occurred."); 171 } 172 }; 173 if request_data.expires_at < Utc::now() { 174 let _ = db::delete_authorization_request(&state.db, &request_uri).await; 175 if wants_json(&headers) { 176 return ( 177 StatusCode::BAD_REQUEST, 178 Json(serde_json::json!({ 179 "error": "invalid_request", 180 "error_description": "Authorization request has expired. Please start a new request." 181 })), 182 ).into_response(); 183 } 184 return redirect_to_frontend_error( 185 "invalid_request", 186 "Authorization request has expired. Please start a new request.", 187 ); 188 } 189 let client_cache = ClientMetadataCache::new(3600); 190 let client_name = client_cache 191 .get(&request_data.parameters.client_id) 192 .await 193 .ok() 194 .and_then(|m| m.client_name); 195 if wants_json(&headers) { 196 return Json(AuthorizeResponse { 197 client_id: request_data.parameters.client_id.clone(), 198 client_name: client_name.clone(), 199 scope: request_data.parameters.scope.clone(), 200 redirect_uri: request_data.parameters.redirect_uri.clone(), 201 state: request_data.parameters.state.clone(), 202 login_hint: request_data.parameters.login_hint.clone(), 203 }) 204 .into_response(); 205 } 206 let force_new_account = query.new_account.unwrap_or(false); 207 if !force_new_account 208 && let Some(device_id) = extract_device_cookie(&headers) 209 && let Ok(accounts) = db::get_device_accounts(&state.db, &device_id).await 210 && !accounts.is_empty() 211 { 212 return redirect_see_other(&format!( 213 "/#/oauth/accounts?request_uri={}", 214 url_encode(&request_uri) 215 )); 216 } 217 redirect_see_other(&format!( 218 "/#/oauth/login?request_uri={}", 219 url_encode(&request_uri) 220 )) 221} 222 223pub async fn authorize_get_json( 224 State(state): State<AppState>, 225 Query(query): Query<AuthorizeQuery>, 226) -> Result<Json<AuthorizeResponse>, OAuthError> { 227 let request_uri = query 228 .request_uri 229 .ok_or_else(|| OAuthError::InvalidRequest("request_uri is required".to_string()))?; 230 let request_data = db::get_authorization_request(&state.db, &request_uri) 231 .await? 232 .ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?; 233 if request_data.expires_at < Utc::now() { 234 db::delete_authorization_request(&state.db, &request_uri).await?; 235 return Err(OAuthError::InvalidRequest( 236 "request_uri has expired".to_string(), 237 )); 238 } 239 Ok(Json(AuthorizeResponse { 240 client_id: request_data.parameters.client_id.clone(), 241 client_name: None, 242 scope: request_data.parameters.scope.clone(), 243 redirect_uri: request_data.parameters.redirect_uri.clone(), 244 state: request_data.parameters.state.clone(), 245 login_hint: request_data.parameters.login_hint.clone(), 246 })) 247} 248 249#[derive(Debug, Serialize)] 250pub struct AccountInfo { 251 pub did: String, 252 pub handle: String, 253 #[serde(skip_serializing_if = "Option::is_none")] 254 pub email: Option<String>, 255} 256 257#[derive(Debug, Serialize)] 258pub struct AccountsResponse { 259 pub accounts: Vec<AccountInfo>, 260 pub request_uri: String, 261} 262 263fn mask_email(email: &str) -> String { 264 if let Some(at_pos) = email.find('@') { 265 let local = &email[..at_pos]; 266 let domain = &email[at_pos..]; 267 if local.len() <= 2 { 268 format!("{}***{}", local.chars().next().unwrap_or('*'), domain) 269 } else { 270 let first = local.chars().next().unwrap_or('*'); 271 let last = local.chars().last().unwrap_or('*'); 272 format!("{}***{}{}", first, last, domain) 273 } 274 } else { 275 "***".to_string() 276 } 277} 278 279pub async fn authorize_accounts( 280 State(state): State<AppState>, 281 headers: HeaderMap, 282 Query(query): Query<AuthorizeQuery>, 283) -> Response { 284 let request_uri = match query.request_uri { 285 Some(uri) => uri, 286 None => { 287 return ( 288 StatusCode::BAD_REQUEST, 289 Json(serde_json::json!({ 290 "error": "invalid_request", 291 "error_description": "Missing request_uri parameter" 292 })), 293 ) 294 .into_response(); 295 } 296 }; 297 let device_id = match extract_device_cookie(&headers) { 298 Some(id) => id, 299 None => { 300 return Json(AccountsResponse { 301 accounts: vec![], 302 request_uri, 303 }) 304 .into_response(); 305 } 306 }; 307 let accounts = match db::get_device_accounts(&state.db, &device_id).await { 308 Ok(accts) => accts, 309 Err(_) => { 310 return Json(AccountsResponse { 311 accounts: vec![], 312 request_uri, 313 }) 314 .into_response(); 315 } 316 }; 317 let account_infos: Vec<AccountInfo> = accounts 318 .into_iter() 319 .map(|row| AccountInfo { 320 did: row.did, 321 handle: row.handle, 322 email: row.email.map(|e| mask_email(&e)), 323 }) 324 .collect(); 325 Json(AccountsResponse { 326 accounts: account_infos, 327 request_uri, 328 }) 329 .into_response() 330} 331 332pub async fn authorize_post( 333 State(state): State<AppState>, 334 headers: HeaderMap, 335 Json(form): Json<AuthorizeSubmit>, 336) -> Response { 337 let json_response = wants_json(&headers); 338 let client_ip = extract_client_ip(&headers); 339 if !state 340 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip) 341 .await 342 { 343 tracing::warn!(ip = %client_ip, "OAuth authorize rate limit exceeded"); 344 if json_response { 345 return ( 346 axum::http::StatusCode::TOO_MANY_REQUESTS, 347 Json(serde_json::json!({ 348 "error": "RateLimitExceeded", 349 "error_description": "Too many login attempts. Please try again later." 350 })), 351 ) 352 .into_response(); 353 } 354 return redirect_to_frontend_error( 355 "RateLimitExceeded", 356 "Too many login attempts. Please try again later.", 357 ); 358 } 359 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 360 Ok(Some(data)) => data, 361 Ok(None) => { 362 if json_response { 363 return ( 364 axum::http::StatusCode::BAD_REQUEST, 365 Json(serde_json::json!({ 366 "error": "invalid_request", 367 "error_description": "Invalid or expired request_uri." 368 })), 369 ) 370 .into_response(); 371 } 372 return redirect_to_frontend_error( 373 "invalid_request", 374 "Invalid or expired request_uri. Please start a new authorization request.", 375 ); 376 } 377 Err(e) => { 378 if json_response { 379 return ( 380 axum::http::StatusCode::INTERNAL_SERVER_ERROR, 381 Json(serde_json::json!({ 382 "error": "server_error", 383 "error_description": format!("Database error: {:?}", e) 384 })), 385 ) 386 .into_response(); 387 } 388 return redirect_to_frontend_error("server_error", &format!("Database error: {:?}", e)); 389 } 390 }; 391 if request_data.expires_at < Utc::now() { 392 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 393 if json_response { 394 return ( 395 axum::http::StatusCode::BAD_REQUEST, 396 Json(serde_json::json!({ 397 "error": "invalid_request", 398 "error_description": "Authorization request has expired." 399 })), 400 ) 401 .into_response(); 402 } 403 return redirect_to_frontend_error( 404 "invalid_request", 405 "Authorization request has expired. Please start a new request.", 406 ); 407 } 408 let show_login_error = |error_msg: &str, json: bool| -> Response { 409 if json { 410 return ( 411 axum::http::StatusCode::FORBIDDEN, 412 Json(serde_json::json!({ 413 "error": "access_denied", 414 "error_description": error_msg 415 })), 416 ) 417 .into_response(); 418 } 419 redirect_see_other(&format!( 420 "/#/oauth/login?request_uri={}&error={}", 421 url_encode(&form.request_uri), 422 url_encode(error_msg) 423 )) 424 }; 425 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 426 let normalized_username = form.username.trim(); 427 let normalized_username = normalized_username 428 .strip_prefix('@') 429 .unwrap_or(normalized_username); 430 let normalized_username = if normalized_username.contains('@') { 431 normalized_username.to_string() 432 } else if !normalized_username.contains('.') { 433 format!("{}.{}", normalized_username, pds_hostname) 434 } else { 435 normalized_username.to_string() 436 }; 437 tracing::debug!( 438 original_username = %form.username, 439 normalized_username = %normalized_username, 440 pds_hostname = %pds_hostname, 441 "Normalized username for lookup" 442 ); 443 let user = match sqlx::query!( 444 r#" 445 SELECT id, did, email, password_hash, password_required, two_factor_enabled, 446 preferred_comms_channel as "preferred_comms_channel: CommsChannel", 447 deactivated_at, takedown_ref, 448 email_verified, discord_verified, telegram_verified, signal_verified 449 FROM users 450 WHERE handle = $1 OR email = $1 451 "#, 452 normalized_username 453 ) 454 .fetch_optional(&state.db) 455 .await 456 { 457 Ok(Some(u)) => u, 458 Ok(None) => { 459 let _ = bcrypt::verify( 460 &form.password, 461 "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK", 462 ); 463 return show_login_error("Invalid handle/email or password.", json_response); 464 } 465 Err(_) => return show_login_error("An error occurred. Please try again.", json_response), 466 }; 467 if user.deactivated_at.is_some() { 468 return show_login_error("This account has been deactivated.", json_response); 469 } 470 if user.takedown_ref.is_some() { 471 return show_login_error("This account has been taken down.", json_response); 472 } 473 let is_verified = user.email_verified 474 || user.discord_verified 475 || user.telegram_verified 476 || user.signal_verified; 477 if !is_verified { 478 return show_login_error( 479 "Please verify your account before logging in.", 480 json_response, 481 ); 482 } 483 484 if !user.password_required { 485 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None) 486 .await 487 .is_err() 488 { 489 return show_login_error("An error occurred. Please try again.", json_response); 490 } 491 let redirect_url = format!( 492 "/#/oauth/passkey?request_uri={}", 493 url_encode(&form.request_uri) 494 ); 495 if json_response { 496 return ( 497 StatusCode::OK, 498 Json(serde_json::json!({ 499 "next": "passkey", 500 "redirect": redirect_url 501 })), 502 ) 503 .into_response(); 504 } 505 return redirect_see_other(&redirect_url); 506 } 507 508 let password_valid = match &user.password_hash { 509 Some(hash) => match bcrypt::verify(&form.password, hash) { 510 Ok(valid) => valid, 511 Err(_) => { 512 return show_login_error("An error occurred. Please try again.", json_response); 513 } 514 }, 515 None => false, 516 }; 517 if !password_valid { 518 return show_login_error("Invalid handle/email or password.", json_response); 519 } 520 let has_totp = crate::api::server::has_totp_enabled(&state, &user.did).await; 521 if has_totp { 522 let device_cookie = extract_device_cookie(&headers); 523 let device_is_trusted = if let Some(ref dev_id) = device_cookie { 524 crate::api::server::is_device_trusted(&state.db, dev_id, &user.did).await 525 } else { 526 false 527 }; 528 529 if device_is_trusted { 530 if let Some(ref dev_id) = device_cookie { 531 let _ = crate::api::server::extend_device_trust(&state.db, dev_id).await; 532 } 533 } else { 534 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None) 535 .await 536 .is_err() 537 { 538 return show_login_error("An error occurred. Please try again.", json_response); 539 } 540 if json_response { 541 return Json(serde_json::json!({ 542 "needs_totp": true 543 })) 544 .into_response(); 545 } 546 return redirect_see_other(&format!( 547 "/#/oauth/totp?request_uri={}", 548 url_encode(&form.request_uri) 549 )); 550 } 551 } 552 if user.two_factor_enabled { 553 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 554 match db::create_2fa_challenge(&state.db, &user.did, &form.request_uri).await { 555 Ok(challenge) => { 556 let hostname = 557 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 558 if let Err(e) = 559 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await 560 { 561 tracing::warn!( 562 did = %user.did, 563 error = %e, 564 "Failed to enqueue 2FA notification" 565 ); 566 } 567 let channel_name = channel_display_name(user.preferred_comms_channel); 568 if json_response { 569 return Json(serde_json::json!({ 570 "needs_2fa": true, 571 "channel": channel_name 572 })) 573 .into_response(); 574 } 575 return redirect_see_other(&format!( 576 "/#/oauth/2fa?request_uri={}&channel={}", 577 url_encode(&form.request_uri), 578 url_encode(channel_name) 579 )); 580 } 581 Err(_) => { 582 return show_login_error("An error occurred. Please try again.", json_response); 583 } 584 } 585 } 586 let mut device_id: Option<String> = extract_device_cookie(&headers); 587 let mut new_cookie: Option<String> = None; 588 if form.remember_device { 589 let final_device_id = if let Some(existing_id) = &device_id { 590 existing_id.clone() 591 } else { 592 let new_id = DeviceId::generate(); 593 let device_data = DeviceData { 594 session_id: SessionId::generate().0, 595 user_agent: extract_user_agent(&headers), 596 ip_address: extract_client_ip(&headers), 597 last_seen_at: Utc::now(), 598 }; 599 if db::create_device(&state.db, &new_id.0, &device_data) 600 .await 601 .is_ok() 602 { 603 new_cookie = Some(make_device_cookie(&new_id.0)); 604 device_id = Some(new_id.0.clone()); 605 } 606 new_id.0 607 }; 608 let _ = db::upsert_account_device(&state.db, &user.did, &final_device_id).await; 609 } 610 if db::set_authorization_did( 611 &state.db, 612 &form.request_uri, 613 &user.did, 614 device_id.as_deref(), 615 ) 616 .await 617 .is_err() 618 { 619 return show_login_error("An error occurred. Please try again.", json_response); 620 } 621 let requested_scope_str = request_data 622 .parameters 623 .scope 624 .as_deref() 625 .unwrap_or("atproto"); 626 let requested_scopes: Vec<String> = requested_scope_str 627 .split_whitespace() 628 .map(|s| s.to_string()) 629 .collect(); 630 let needs_consent = db::should_show_consent( 631 &state.db, 632 &user.did, 633 &request_data.parameters.client_id, 634 &requested_scopes, 635 ) 636 .await 637 .unwrap_or(true); 638 if needs_consent { 639 let consent_url = format!( 640 "/#/oauth/consent?request_uri={}", 641 url_encode(&form.request_uri) 642 ); 643 if json_response { 644 if let Some(cookie) = new_cookie { 645 return ( 646 StatusCode::OK, 647 [(SET_COOKIE, cookie)], 648 Json(serde_json::json!({"redirect_uri": consent_url})), 649 ) 650 .into_response(); 651 } 652 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 653 } 654 if let Some(cookie) = new_cookie { 655 return ( 656 StatusCode::SEE_OTHER, 657 [(SET_COOKIE, cookie), (LOCATION, consent_url)], 658 ) 659 .into_response(); 660 } 661 return redirect_see_other(&consent_url); 662 } 663 let code = Code::generate(); 664 if db::update_authorization_request( 665 &state.db, 666 &form.request_uri, 667 &user.did, 668 device_id.as_deref(), 669 &code.0, 670 ) 671 .await 672 .is_err() 673 { 674 return show_login_error("An error occurred. Please try again.", json_response); 675 } 676 let redirect_url = build_success_redirect( 677 &request_data.parameters.redirect_uri, 678 &code.0, 679 request_data.parameters.state.as_deref(), 680 request_data.parameters.response_mode.as_deref(), 681 ); 682 if json_response { 683 if let Some(cookie) = new_cookie { 684 ( 685 StatusCode::OK, 686 [(SET_COOKIE, cookie)], 687 Json(serde_json::json!({"redirect_uri": redirect_url})), 688 ) 689 .into_response() 690 } else { 691 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 692 } 693 } else if let Some(cookie) = new_cookie { 694 ( 695 StatusCode::SEE_OTHER, 696 [(SET_COOKIE, cookie), (LOCATION, redirect_url)], 697 ) 698 .into_response() 699 } else { 700 redirect_see_other(&redirect_url) 701 } 702} 703 704pub async fn authorize_select( 705 State(state): State<AppState>, 706 headers: HeaderMap, 707 Json(form): Json<AuthorizeSelectSubmit>, 708) -> Response { 709 let json_error = |status: StatusCode, error: &str, description: &str| -> Response { 710 ( 711 status, 712 Json(serde_json::json!({ 713 "error": error, 714 "error_description": description 715 })), 716 ) 717 .into_response() 718 }; 719 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 720 Ok(Some(data)) => data, 721 Ok(None) => { 722 return json_error( 723 StatusCode::BAD_REQUEST, 724 "invalid_request", 725 "Invalid or expired request_uri. Please start a new authorization request.", 726 ); 727 } 728 Err(_) => { 729 return json_error( 730 StatusCode::INTERNAL_SERVER_ERROR, 731 "server_error", 732 "An error occurred. Please try again.", 733 ); 734 } 735 }; 736 if request_data.expires_at < Utc::now() { 737 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 738 return json_error( 739 StatusCode::BAD_REQUEST, 740 "invalid_request", 741 "Authorization request has expired. Please start a new request.", 742 ); 743 } 744 let device_id = match extract_device_cookie(&headers) { 745 Some(id) => id, 746 None => { 747 return json_error( 748 StatusCode::BAD_REQUEST, 749 "invalid_request", 750 "No device session found. Please sign in.", 751 ); 752 } 753 }; 754 let account_valid = match db::verify_account_on_device(&state.db, &device_id, &form.did).await { 755 Ok(valid) => valid, 756 Err(_) => { 757 return json_error( 758 StatusCode::INTERNAL_SERVER_ERROR, 759 "server_error", 760 "An error occurred. Please try again.", 761 ); 762 } 763 }; 764 if !account_valid { 765 return json_error( 766 StatusCode::FORBIDDEN, 767 "access_denied", 768 "This account is not available on this device. Please sign in.", 769 ); 770 } 771 let user = match sqlx::query!( 772 r#" 773 SELECT id, two_factor_enabled, 774 preferred_comms_channel as "preferred_comms_channel: CommsChannel", 775 email_verified, discord_verified, telegram_verified, signal_verified 776 FROM users 777 WHERE did = $1 778 "#, 779 form.did 780 ) 781 .fetch_optional(&state.db) 782 .await 783 { 784 Ok(Some(u)) => u, 785 Ok(None) => { 786 return json_error( 787 StatusCode::FORBIDDEN, 788 "access_denied", 789 "Account not found. Please sign in.", 790 ); 791 } 792 Err(_) => { 793 return json_error( 794 StatusCode::INTERNAL_SERVER_ERROR, 795 "server_error", 796 "An error occurred. Please try again.", 797 ); 798 } 799 }; 800 let is_verified = user.email_verified 801 || user.discord_verified 802 || user.telegram_verified 803 || user.signal_verified; 804 if !is_verified { 805 return json_error( 806 StatusCode::FORBIDDEN, 807 "access_denied", 808 "Please verify your account before logging in.", 809 ); 810 } 811 let has_totp = crate::api::server::has_totp_enabled(&state, &form.did).await; 812 if has_totp { 813 if db::set_authorization_did(&state.db, &form.request_uri, &form.did, Some(&device_id)) 814 .await 815 .is_err() 816 { 817 return json_error( 818 StatusCode::INTERNAL_SERVER_ERROR, 819 "server_error", 820 "An error occurred. Please try again.", 821 ); 822 } 823 return Json(serde_json::json!({ 824 "needs_totp": true 825 })) 826 .into_response(); 827 } 828 if user.two_factor_enabled { 829 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 830 match db::create_2fa_challenge(&state.db, &form.did, &form.request_uri).await { 831 Ok(challenge) => { 832 let hostname = 833 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 834 if let Err(e) = 835 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await 836 { 837 tracing::warn!( 838 did = %form.did, 839 error = %e, 840 "Failed to enqueue 2FA notification" 841 ); 842 } 843 let channel_name = channel_display_name(user.preferred_comms_channel); 844 return Json(serde_json::json!({ 845 "needs_2fa": true, 846 "channel": channel_name 847 })) 848 .into_response(); 849 } 850 Err(_) => { 851 return json_error( 852 StatusCode::INTERNAL_SERVER_ERROR, 853 "server_error", 854 "An error occurred. Please try again.", 855 ); 856 } 857 } 858 } 859 let _ = db::upsert_account_device(&state.db, &form.did, &device_id).await; 860 let code = Code::generate(); 861 if db::update_authorization_request( 862 &state.db, 863 &form.request_uri, 864 &form.did, 865 Some(&device_id), 866 &code.0, 867 ) 868 .await 869 .is_err() 870 { 871 return json_error( 872 StatusCode::INTERNAL_SERVER_ERROR, 873 "server_error", 874 "An error occurred. Please try again.", 875 ); 876 } 877 let redirect_url = build_success_redirect( 878 &request_data.parameters.redirect_uri, 879 &code.0, 880 request_data.parameters.state.as_deref(), 881 request_data.parameters.response_mode.as_deref(), 882 ); 883 Json(serde_json::json!({ 884 "redirect_uri": redirect_url 885 })) 886 .into_response() 887} 888 889fn build_success_redirect( 890 redirect_uri: &str, 891 code: &str, 892 state: Option<&str>, 893 response_mode: Option<&str>, 894) -> String { 895 let mut redirect_url = redirect_uri.to_string(); 896 let use_fragment = response_mode == Some("fragment"); 897 let separator = if use_fragment { 898 '#' 899 } else if redirect_url.contains('?') { 900 '&' 901 } else { 902 '?' 903 }; 904 redirect_url.push(separator); 905 redirect_url.push_str(&format!("code={}", url_encode(code))); 906 if let Some(req_state) = state { 907 redirect_url.push_str(&format!("&state={}", url_encode(req_state))); 908 } 909 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 910 redirect_url.push_str(&format!( 911 "&iss={}", 912 url_encode(&format!("https://{}", pds_hostname)) 913 )); 914 redirect_url 915} 916 917#[derive(Debug, Serialize)] 918pub struct AuthorizeDenyResponse { 919 pub error: String, 920 pub error_description: String, 921} 922 923pub async fn authorize_deny( 924 State(state): State<AppState>, 925 Json(form): Json<AuthorizeDenyForm>, 926) -> Response { 927 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 928 Ok(Some(data)) => data, 929 Ok(None) => { 930 return ( 931 StatusCode::BAD_REQUEST, 932 Json(serde_json::json!({ 933 "error": "invalid_request", 934 "error_description": "Invalid request_uri" 935 })), 936 ) 937 .into_response(); 938 } 939 Err(_) => { 940 return ( 941 StatusCode::INTERNAL_SERVER_ERROR, 942 Json(serde_json::json!({ 943 "error": "server_error", 944 "error_description": "An error occurred" 945 })), 946 ) 947 .into_response(); 948 } 949 }; 950 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 951 let redirect_uri = &request_data.parameters.redirect_uri; 952 let mut redirect_url = redirect_uri.to_string(); 953 let separator = if redirect_url.contains('?') { '&' } else { '?' }; 954 redirect_url.push(separator); 955 redirect_url.push_str("error=access_denied"); 956 redirect_url.push_str("&error_description=User%20denied%20the%20request"); 957 if let Some(state) = &request_data.parameters.state { 958 redirect_url.push_str(&format!("&state={}", url_encode(state))); 959 } 960 Json(serde_json::json!({ 961 "redirect_uri": redirect_url 962 })) 963 .into_response() 964} 965 966#[derive(Debug, Deserialize)] 967pub struct AuthorizeDenyForm { 968 pub request_uri: String, 969} 970 971#[derive(Debug, Deserialize)] 972pub struct Authorize2faQuery { 973 pub request_uri: String, 974 pub channel: Option<String>, 975} 976 977#[derive(Debug, Deserialize)] 978pub struct Authorize2faSubmit { 979 pub request_uri: String, 980 pub code: String, 981 #[serde(default)] 982 pub trust_device: bool, 983} 984 985const MAX_2FA_ATTEMPTS: i32 = 5; 986 987pub async fn authorize_2fa_get( 988 State(state): State<AppState>, 989 Query(query): Query<Authorize2faQuery>, 990) -> Response { 991 let challenge = match db::get_2fa_challenge(&state.db, &query.request_uri).await { 992 Ok(Some(c)) => c, 993 Ok(None) => { 994 return redirect_to_frontend_error( 995 "invalid_request", 996 "No 2FA challenge found. Please start over.", 997 ); 998 } 999 Err(_) => { 1000 return redirect_to_frontend_error( 1001 "server_error", 1002 "An error occurred. Please try again.", 1003 ); 1004 } 1005 }; 1006 if challenge.expires_at < Utc::now() { 1007 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 1008 return redirect_to_frontend_error( 1009 "invalid_request", 1010 "2FA code has expired. Please start over.", 1011 ); 1012 } 1013 let _request_data = match db::get_authorization_request(&state.db, &query.request_uri).await { 1014 Ok(Some(d)) => d, 1015 Ok(None) => { 1016 return redirect_to_frontend_error( 1017 "invalid_request", 1018 "Authorization request not found. Please start over.", 1019 ); 1020 } 1021 Err(_) => { 1022 return redirect_to_frontend_error( 1023 "server_error", 1024 "An error occurred. Please try again.", 1025 ); 1026 } 1027 }; 1028 let channel = query.channel.as_deref().unwrap_or("email"); 1029 redirect_see_other(&format!( 1030 "/#/oauth/2fa?request_uri={}&channel={}", 1031 url_encode(&query.request_uri), 1032 url_encode(channel) 1033 )) 1034} 1035 1036#[derive(Debug, Serialize)] 1037pub struct ScopeInfo { 1038 pub scope: String, 1039 pub category: String, 1040 pub required: bool, 1041 pub description: String, 1042 pub display_name: String, 1043 pub granted: Option<bool>, 1044} 1045 1046#[derive(Debug, Serialize)] 1047pub struct ConsentResponse { 1048 pub request_uri: String, 1049 pub client_id: String, 1050 pub client_name: Option<String>, 1051 pub client_uri: Option<String>, 1052 pub logo_uri: Option<String>, 1053 pub scopes: Vec<ScopeInfo>, 1054 pub show_consent: bool, 1055 pub did: String, 1056} 1057 1058#[derive(Debug, Deserialize)] 1059pub struct ConsentQuery { 1060 pub request_uri: String, 1061} 1062 1063#[derive(Debug, Deserialize)] 1064pub struct ConsentSubmit { 1065 pub request_uri: String, 1066 pub approved_scopes: Vec<String>, 1067 pub remember: bool, 1068} 1069 1070pub async fn consent_get( 1071 State(state): State<AppState>, 1072 Query(query): Query<ConsentQuery>, 1073) -> Response { 1074 let request_data = match db::get_authorization_request(&state.db, &query.request_uri).await { 1075 Ok(Some(data)) => data, 1076 Ok(None) => { 1077 return ( 1078 StatusCode::BAD_REQUEST, 1079 Json(serde_json::json!({ 1080 "error": "invalid_request", 1081 "error_description": "Invalid or expired request_uri" 1082 })), 1083 ) 1084 .into_response(); 1085 } 1086 Err(e) => { 1087 return ( 1088 StatusCode::INTERNAL_SERVER_ERROR, 1089 Json(serde_json::json!({ 1090 "error": "server_error", 1091 "error_description": format!("Database error: {:?}", e) 1092 })), 1093 ) 1094 .into_response(); 1095 } 1096 }; 1097 if request_data.expires_at < Utc::now() { 1098 let _ = db::delete_authorization_request(&state.db, &query.request_uri).await; 1099 return ( 1100 StatusCode::BAD_REQUEST, 1101 Json(serde_json::json!({ 1102 "error": "invalid_request", 1103 "error_description": "Authorization request has expired" 1104 })), 1105 ) 1106 .into_response(); 1107 } 1108 let did = match &request_data.did { 1109 Some(d) => d.clone(), 1110 None => { 1111 return ( 1112 StatusCode::FORBIDDEN, 1113 Json(serde_json::json!({ 1114 "error": "access_denied", 1115 "error_description": "Not authenticated" 1116 })), 1117 ) 1118 .into_response(); 1119 } 1120 }; 1121 let client_cache = ClientMetadataCache::new(3600); 1122 let client_metadata = client_cache 1123 .get(&request_data.parameters.client_id) 1124 .await 1125 .ok(); 1126 let requested_scope_str = request_data 1127 .parameters 1128 .scope 1129 .as_deref() 1130 .unwrap_or("atproto"); 1131 let requested_scopes: Vec<&str> = requested_scope_str.split_whitespace().collect(); 1132 let preferences = 1133 db::get_scope_preferences(&state.db, &did, &request_data.parameters.client_id) 1134 .await 1135 .unwrap_or_default(); 1136 let pref_map: std::collections::HashMap<_, _> = preferences 1137 .iter() 1138 .map(|p| (p.scope.as_str(), p.granted)) 1139 .collect(); 1140 let requested_scope_strings: Vec<String> = 1141 requested_scopes.iter().map(|s| s.to_string()).collect(); 1142 let show_consent = db::should_show_consent( 1143 &state.db, 1144 &did, 1145 &request_data.parameters.client_id, 1146 &requested_scope_strings, 1147 ) 1148 .await 1149 .unwrap_or(true); 1150 let mut scopes = Vec::new(); 1151 for scope in &requested_scopes { 1152 let (category, required, description, display_name) = 1153 if let Some(def) = crate::oauth::scopes::SCOPE_DEFINITIONS.get(*scope) { 1154 ( 1155 def.category.display_name().to_string(), 1156 def.required, 1157 def.description.to_string(), 1158 def.display_name.to_string(), 1159 ) 1160 } else if scope.starts_with("ref:") { 1161 ( 1162 "Reference".to_string(), 1163 false, 1164 "Referenced scope".to_string(), 1165 scope.to_string(), 1166 ) 1167 } else { 1168 ( 1169 "Other".to_string(), 1170 false, 1171 format!("Access to {}", scope), 1172 scope.to_string(), 1173 ) 1174 }; 1175 let granted = pref_map.get(*scope).copied(); 1176 scopes.push(ScopeInfo { 1177 scope: scope.to_string(), 1178 category, 1179 required, 1180 description, 1181 display_name, 1182 granted, 1183 }); 1184 } 1185 Json(ConsentResponse { 1186 request_uri: query.request_uri.clone(), 1187 client_id: request_data.parameters.client_id.clone(), 1188 client_name: client_metadata.as_ref().and_then(|m| m.client_name.clone()), 1189 client_uri: client_metadata.as_ref().and_then(|m| m.client_uri.clone()), 1190 logo_uri: client_metadata.as_ref().and_then(|m| m.logo_uri.clone()), 1191 scopes, 1192 show_consent, 1193 did, 1194 }) 1195 .into_response() 1196} 1197 1198pub async fn consent_post( 1199 State(state): State<AppState>, 1200 Json(form): Json<ConsentSubmit>, 1201) -> Response { 1202 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 1203 Ok(Some(data)) => data, 1204 Ok(None) => { 1205 return ( 1206 StatusCode::BAD_REQUEST, 1207 Json(serde_json::json!({ 1208 "error": "invalid_request", 1209 "error_description": "Invalid or expired request_uri" 1210 })), 1211 ) 1212 .into_response(); 1213 } 1214 Err(e) => { 1215 return ( 1216 StatusCode::INTERNAL_SERVER_ERROR, 1217 Json(serde_json::json!({ 1218 "error": "server_error", 1219 "error_description": format!("Database error: {:?}", e) 1220 })), 1221 ) 1222 .into_response(); 1223 } 1224 }; 1225 if request_data.expires_at < Utc::now() { 1226 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1227 return ( 1228 StatusCode::BAD_REQUEST, 1229 Json(serde_json::json!({ 1230 "error": "invalid_request", 1231 "error_description": "Authorization request has expired" 1232 })), 1233 ) 1234 .into_response(); 1235 } 1236 let did = match &request_data.did { 1237 Some(d) => d.clone(), 1238 None => { 1239 return ( 1240 StatusCode::FORBIDDEN, 1241 Json(serde_json::json!({ 1242 "error": "access_denied", 1243 "error_description": "Not authenticated" 1244 })), 1245 ) 1246 .into_response(); 1247 } 1248 }; 1249 let requested_scope_str = request_data 1250 .parameters 1251 .scope 1252 .as_deref() 1253 .unwrap_or("atproto"); 1254 let requested_scopes: Vec<&str> = requested_scope_str.split_whitespace().collect(); 1255 let has_granular_scopes = requested_scopes.iter().any(|s| { 1256 s.starts_with("repo:") 1257 || s.starts_with("blob:") 1258 || s.starts_with("rpc:") 1259 || s.starts_with("account:") 1260 || s.starts_with("identity:") 1261 }); 1262 let user_denied_some_granular = has_granular_scopes 1263 && requested_scopes 1264 .iter() 1265 .filter(|s| { 1266 s.starts_with("repo:") 1267 || s.starts_with("blob:") 1268 || s.starts_with("rpc:") 1269 || s.starts_with("account:") 1270 || s.starts_with("identity:") 1271 }) 1272 .any(|s| !form.approved_scopes.contains(&s.to_string())); 1273 let atproto_was_requested = requested_scopes.contains(&"atproto"); 1274 if atproto_was_requested 1275 && !has_granular_scopes 1276 && !form.approved_scopes.contains(&"atproto".to_string()) 1277 { 1278 return ( 1279 StatusCode::BAD_REQUEST, 1280 Json(serde_json::json!({ 1281 "error": "invalid_request", 1282 "error_description": "The atproto scope was requested and must be approved" 1283 })), 1284 ) 1285 .into_response(); 1286 } 1287 let final_approved: Vec<String> = if user_denied_some_granular { 1288 form.approved_scopes 1289 .iter() 1290 .filter(|s| *s != "atproto") 1291 .cloned() 1292 .collect() 1293 } else { 1294 form.approved_scopes.clone() 1295 }; 1296 if final_approved.is_empty() { 1297 return ( 1298 StatusCode::BAD_REQUEST, 1299 Json(serde_json::json!({ 1300 "error": "invalid_request", 1301 "error_description": "At least one scope must be approved" 1302 })), 1303 ) 1304 .into_response(); 1305 } 1306 let approved_scope_str = final_approved.join(" "); 1307 let has_valid_scope = final_approved.iter().all(|s| { 1308 s == "atproto" 1309 || s == "transition:generic" 1310 || s == "transition:chat.bsky" 1311 || s == "transition:email" 1312 || s.starts_with("repo:") 1313 || s.starts_with("blob:") 1314 || s.starts_with("rpc:") 1315 || s.starts_with("account:") 1316 || s.starts_with("include:") 1317 }); 1318 if !has_valid_scope { 1319 return ( 1320 StatusCode::BAD_REQUEST, 1321 Json(serde_json::json!({ 1322 "error": "invalid_request", 1323 "error_description": "Invalid scope format" 1324 })), 1325 ) 1326 .into_response(); 1327 } 1328 if form.remember { 1329 let preferences: Vec<db::ScopePreference> = requested_scopes 1330 .iter() 1331 .map(|s| db::ScopePreference { 1332 scope: s.to_string(), 1333 granted: form.approved_scopes.contains(&s.to_string()), 1334 }) 1335 .collect(); 1336 let _ = db::upsert_scope_preferences( 1337 &state.db, 1338 &did, 1339 &request_data.parameters.client_id, 1340 &preferences, 1341 ) 1342 .await; 1343 } 1344 if let Err(e) = 1345 db::update_request_scope(&state.db, &form.request_uri, &approved_scope_str).await 1346 { 1347 tracing::warn!("Failed to update request scope: {:?}", e); 1348 } 1349 let code = Code::generate(); 1350 if db::update_authorization_request( 1351 &state.db, 1352 &form.request_uri, 1353 &did, 1354 request_data.device_id.as_deref(), 1355 &code.0, 1356 ) 1357 .await 1358 .is_err() 1359 { 1360 return ( 1361 StatusCode::INTERNAL_SERVER_ERROR, 1362 Json(serde_json::json!({ 1363 "error": "server_error", 1364 "error_description": "Failed to complete authorization" 1365 })), 1366 ) 1367 .into_response(); 1368 } 1369 let redirect_url = build_success_redirect( 1370 &request_data.parameters.redirect_uri, 1371 &code.0, 1372 request_data.parameters.state.as_deref(), 1373 request_data.parameters.response_mode.as_deref(), 1374 ); 1375 Json(serde_json::json!({ 1376 "redirect_uri": redirect_url 1377 })) 1378 .into_response() 1379} 1380 1381pub async fn authorize_2fa_post( 1382 State(state): State<AppState>, 1383 headers: HeaderMap, 1384 Json(form): Json<Authorize2faSubmit>, 1385) -> Response { 1386 let json_error = |status: StatusCode, error: &str, description: &str| -> Response { 1387 ( 1388 status, 1389 Json(serde_json::json!({ 1390 "error": error, 1391 "error_description": description 1392 })), 1393 ) 1394 .into_response() 1395 }; 1396 let client_ip = extract_client_ip(&headers); 1397 if !state 1398 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip) 1399 .await 1400 { 1401 tracing::warn!(ip = %client_ip, "OAuth 2FA rate limit exceeded"); 1402 return json_error( 1403 StatusCode::TOO_MANY_REQUESTS, 1404 "RateLimitExceeded", 1405 "Too many attempts. Please try again later.", 1406 ); 1407 } 1408 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 1409 Ok(Some(d)) => d, 1410 Ok(None) => { 1411 return json_error( 1412 StatusCode::BAD_REQUEST, 1413 "invalid_request", 1414 "Authorization request not found.", 1415 ); 1416 } 1417 Err(_) => { 1418 return json_error( 1419 StatusCode::INTERNAL_SERVER_ERROR, 1420 "server_error", 1421 "An error occurred.", 1422 ); 1423 } 1424 }; 1425 if request_data.expires_at < Utc::now() { 1426 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1427 return json_error( 1428 StatusCode::BAD_REQUEST, 1429 "invalid_request", 1430 "Authorization request has expired.", 1431 ); 1432 } 1433 let challenge = db::get_2fa_challenge(&state.db, &form.request_uri) 1434 .await 1435 .ok() 1436 .flatten(); 1437 if let Some(challenge) = challenge { 1438 if challenge.expires_at < Utc::now() { 1439 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 1440 return json_error( 1441 StatusCode::BAD_REQUEST, 1442 "invalid_request", 1443 "2FA code has expired. Please start over.", 1444 ); 1445 } 1446 if challenge.attempts >= MAX_2FA_ATTEMPTS { 1447 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 1448 return json_error( 1449 StatusCode::FORBIDDEN, 1450 "access_denied", 1451 "Too many failed attempts. Please start over.", 1452 ); 1453 } 1454 let code_valid: bool = form 1455 .code 1456 .trim() 1457 .as_bytes() 1458 .ct_eq(challenge.code.as_bytes()) 1459 .into(); 1460 if !code_valid { 1461 let _ = db::increment_2fa_attempts(&state.db, challenge.id).await; 1462 return json_error( 1463 StatusCode::FORBIDDEN, 1464 "invalid_code", 1465 "Invalid verification code. Please try again.", 1466 ); 1467 } 1468 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 1469 let code = Code::generate(); 1470 let device_id = extract_device_cookie(&headers); 1471 if db::update_authorization_request( 1472 &state.db, 1473 &form.request_uri, 1474 &challenge.did, 1475 device_id.as_deref(), 1476 &code.0, 1477 ) 1478 .await 1479 .is_err() 1480 { 1481 return json_error( 1482 StatusCode::INTERNAL_SERVER_ERROR, 1483 "server_error", 1484 "An error occurred. Please try again.", 1485 ); 1486 } 1487 let redirect_url = build_success_redirect( 1488 &request_data.parameters.redirect_uri, 1489 &code.0, 1490 request_data.parameters.state.as_deref(), 1491 request_data.parameters.response_mode.as_deref(), 1492 ); 1493 return Json(serde_json::json!({ 1494 "redirect_uri": redirect_url 1495 })) 1496 .into_response(); 1497 } 1498 let did = match &request_data.did { 1499 Some(d) => d.clone(), 1500 None => { 1501 return json_error( 1502 StatusCode::BAD_REQUEST, 1503 "invalid_request", 1504 "No 2FA challenge found. Please start over.", 1505 ); 1506 } 1507 }; 1508 if !crate::api::server::has_totp_enabled(&state, &did).await { 1509 return json_error( 1510 StatusCode::BAD_REQUEST, 1511 "invalid_request", 1512 "No 2FA challenge found. Please start over.", 1513 ); 1514 } 1515 if !state 1516 .check_rate_limit(RateLimitKind::TotpVerify, &did) 1517 .await 1518 { 1519 tracing::warn!(did = %did, "TOTP verification rate limit exceeded"); 1520 return json_error( 1521 StatusCode::TOO_MANY_REQUESTS, 1522 "RateLimitExceeded", 1523 "Too many verification attempts. Please try again in a few minutes.", 1524 ); 1525 } 1526 let totp_valid = 1527 crate::api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await; 1528 if !totp_valid { 1529 return json_error( 1530 StatusCode::FORBIDDEN, 1531 "invalid_code", 1532 "Invalid verification code. Please try again.", 1533 ); 1534 } 1535 let device_id = extract_device_cookie(&headers); 1536 if form.trust_device 1537 && let Some(ref dev_id) = device_id 1538 { 1539 let _ = crate::api::server::trust_device(&state.db, dev_id).await; 1540 } 1541 let requested_scope_str = request_data 1542 .parameters 1543 .scope 1544 .as_deref() 1545 .unwrap_or("atproto"); 1546 let requested_scopes: Vec<String> = requested_scope_str 1547 .split_whitespace() 1548 .map(|s| s.to_string()) 1549 .collect(); 1550 let needs_consent = db::should_show_consent( 1551 &state.db, 1552 &did, 1553 &request_data.parameters.client_id, 1554 &requested_scopes, 1555 ) 1556 .await 1557 .unwrap_or(true); 1558 if needs_consent { 1559 let consent_url = format!( 1560 "/#/oauth/consent?request_uri={}", 1561 url_encode(&form.request_uri) 1562 ); 1563 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 1564 } 1565 let code = Code::generate(); 1566 if db::update_authorization_request( 1567 &state.db, 1568 &form.request_uri, 1569 &did, 1570 device_id.as_deref(), 1571 &code.0, 1572 ) 1573 .await 1574 .is_err() 1575 { 1576 return json_error( 1577 StatusCode::INTERNAL_SERVER_ERROR, 1578 "server_error", 1579 "An error occurred. Please try again.", 1580 ); 1581 } 1582 let redirect_url = build_success_redirect( 1583 &request_data.parameters.redirect_uri, 1584 &code.0, 1585 request_data.parameters.state.as_deref(), 1586 request_data.parameters.response_mode.as_deref(), 1587 ); 1588 Json(serde_json::json!({ 1589 "redirect_uri": redirect_url 1590 })) 1591 .into_response() 1592} 1593 1594#[derive(Debug, Deserialize)] 1595#[serde(rename_all = "camelCase")] 1596pub struct CheckPasskeysQuery { 1597 pub identifier: String, 1598} 1599 1600#[derive(Debug, Serialize)] 1601#[serde(rename_all = "camelCase")] 1602pub struct CheckPasskeysResponse { 1603 pub has_passkeys: bool, 1604} 1605 1606pub async fn check_user_has_passkeys( 1607 State(state): State<AppState>, 1608 Query(query): Query<CheckPasskeysQuery>, 1609) -> Response { 1610 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1611 let normalized_identifier = query.identifier.trim(); 1612 let normalized_identifier = normalized_identifier 1613 .strip_prefix('@') 1614 .unwrap_or(normalized_identifier); 1615 let normalized_identifier = if let Some(bare_handle) = 1616 normalized_identifier.strip_suffix(&format!(".{}", pds_hostname)) 1617 { 1618 bare_handle.to_string() 1619 } else { 1620 normalized_identifier.to_string() 1621 }; 1622 1623 let user = sqlx::query!( 1624 "SELECT did FROM users WHERE handle = $1 OR email = $1", 1625 normalized_identifier 1626 ) 1627 .fetch_optional(&state.db) 1628 .await; 1629 1630 let has_passkeys = match user { 1631 Ok(Some(u)) => crate::api::server::has_passkeys_for_user(&state, &u.did).await, 1632 _ => false, 1633 }; 1634 1635 Json(CheckPasskeysResponse { has_passkeys }).into_response() 1636} 1637 1638#[derive(Debug, Serialize)] 1639#[serde(rename_all = "camelCase")] 1640pub struct SecurityStatusResponse { 1641 pub has_passkeys: bool, 1642 pub has_totp: bool, 1643} 1644 1645pub async fn check_user_security_status( 1646 State(state): State<AppState>, 1647 Query(query): Query<CheckPasskeysQuery>, 1648) -> Response { 1649 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1650 let identifier = query.identifier.trim(); 1651 let identifier = identifier.strip_prefix('@').unwrap_or(identifier); 1652 let normalized_identifier = if identifier.contains('@') || identifier.starts_with("did:") { 1653 identifier.to_string() 1654 } else if !identifier.contains('.') { 1655 format!("{}.{}", identifier.to_lowercase(), pds_hostname) 1656 } else { 1657 identifier.to_lowercase() 1658 }; 1659 1660 let user = sqlx::query!( 1661 "SELECT did FROM users WHERE handle = $1 OR email = $1", 1662 normalized_identifier 1663 ) 1664 .fetch_optional(&state.db) 1665 .await; 1666 1667 let (has_passkeys, has_totp) = match user { 1668 Ok(Some(u)) => { 1669 let passkeys = crate::api::server::has_passkeys_for_user(&state, &u.did).await; 1670 let totp = crate::api::server::has_totp_enabled(&state, &u.did).await; 1671 (passkeys, totp) 1672 } 1673 _ => (false, false), 1674 }; 1675 1676 Json(SecurityStatusResponse { 1677 has_passkeys, 1678 has_totp, 1679 }) 1680 .into_response() 1681} 1682 1683#[derive(Debug, Deserialize)] 1684pub struct PasskeyStartInput { 1685 pub request_uri: String, 1686 pub identifier: String, 1687} 1688 1689#[derive(Debug, Serialize)] 1690#[serde(rename_all = "camelCase")] 1691pub struct PasskeyStartResponse { 1692 pub options: serde_json::Value, 1693} 1694 1695pub async fn passkey_start( 1696 State(state): State<AppState>, 1697 headers: HeaderMap, 1698 Json(form): Json<PasskeyStartInput>, 1699) -> Response { 1700 let client_ip = extract_client_ip(&headers); 1701 1702 if !state 1703 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip) 1704 .await 1705 { 1706 tracing::warn!(ip = %client_ip, "OAuth passkey rate limit exceeded"); 1707 return ( 1708 StatusCode::TOO_MANY_REQUESTS, 1709 Json(serde_json::json!({ 1710 "error": "RateLimitExceeded", 1711 "error_description": "Too many login attempts. Please try again later." 1712 })), 1713 ) 1714 .into_response(); 1715 } 1716 1717 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 1718 Ok(Some(data)) => data, 1719 Ok(None) => { 1720 return ( 1721 StatusCode::BAD_REQUEST, 1722 Json(serde_json::json!({ 1723 "error": "invalid_request", 1724 "error_description": "Invalid or expired request_uri." 1725 })), 1726 ) 1727 .into_response(); 1728 } 1729 Err(_) => { 1730 return ( 1731 StatusCode::INTERNAL_SERVER_ERROR, 1732 Json(serde_json::json!({ 1733 "error": "server_error", 1734 "error_description": "An error occurred." 1735 })), 1736 ) 1737 .into_response(); 1738 } 1739 }; 1740 1741 if request_data.expires_at < Utc::now() { 1742 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1743 return ( 1744 StatusCode::BAD_REQUEST, 1745 Json(serde_json::json!({ 1746 "error": "invalid_request", 1747 "error_description": "Authorization request has expired." 1748 })), 1749 ) 1750 .into_response(); 1751 } 1752 1753 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1754 let normalized_username = form.identifier.trim(); 1755 let normalized_username = normalized_username 1756 .strip_prefix('@') 1757 .unwrap_or(normalized_username); 1758 let normalized_username = if normalized_username.contains('@') { 1759 normalized_username.to_string() 1760 } else if !normalized_username.contains('.') { 1761 format!("{}.{}", normalized_username, pds_hostname) 1762 } else { 1763 normalized_username.to_string() 1764 }; 1765 1766 let user = match sqlx::query!( 1767 r#" 1768 SELECT did, deactivated_at, takedown_ref, 1769 email_verified, discord_verified, telegram_verified, signal_verified 1770 FROM users 1771 WHERE handle = $1 OR email = $1 1772 "#, 1773 normalized_username 1774 ) 1775 .fetch_optional(&state.db) 1776 .await 1777 { 1778 Ok(Some(u)) => u, 1779 Ok(None) => { 1780 return ( 1781 StatusCode::FORBIDDEN, 1782 Json(serde_json::json!({ 1783 "error": "access_denied", 1784 "error_description": "User not found or has no passkeys." 1785 })), 1786 ) 1787 .into_response(); 1788 } 1789 Err(_) => { 1790 return ( 1791 StatusCode::INTERNAL_SERVER_ERROR, 1792 Json(serde_json::json!({ 1793 "error": "server_error", 1794 "error_description": "An error occurred." 1795 })), 1796 ) 1797 .into_response(); 1798 } 1799 }; 1800 1801 if user.deactivated_at.is_some() { 1802 return ( 1803 StatusCode::FORBIDDEN, 1804 Json(serde_json::json!({ 1805 "error": "access_denied", 1806 "error_description": "This account has been deactivated." 1807 })), 1808 ) 1809 .into_response(); 1810 } 1811 1812 if user.takedown_ref.is_some() { 1813 return ( 1814 StatusCode::FORBIDDEN, 1815 Json(serde_json::json!({ 1816 "error": "access_denied", 1817 "error_description": "This account has been taken down." 1818 })), 1819 ) 1820 .into_response(); 1821 } 1822 1823 let is_verified = user.email_verified 1824 || user.discord_verified 1825 || user.telegram_verified 1826 || user.signal_verified; 1827 1828 if !is_verified { 1829 return ( 1830 StatusCode::FORBIDDEN, 1831 Json(serde_json::json!({ 1832 "error": "access_denied", 1833 "error_description": "Please verify your account before logging in." 1834 })), 1835 ) 1836 .into_response(); 1837 } 1838 1839 let stored_passkeys = 1840 match crate::auth::webauthn::get_passkeys_for_user(&state.db, &user.did).await { 1841 Ok(pks) => pks, 1842 Err(e) => { 1843 tracing::error!(error = %e, "Failed to get passkeys"); 1844 return ( 1845 StatusCode::INTERNAL_SERVER_ERROR, 1846 Json(serde_json::json!({ 1847 "error": "server_error", 1848 "error_description": "An error occurred." 1849 })), 1850 ) 1851 .into_response(); 1852 } 1853 }; 1854 1855 if stored_passkeys.is_empty() { 1856 return ( 1857 StatusCode::FORBIDDEN, 1858 Json(serde_json::json!({ 1859 "error": "access_denied", 1860 "error_description": "User not found or has no passkeys." 1861 })), 1862 ) 1863 .into_response(); 1864 } 1865 1866 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys 1867 .iter() 1868 .filter_map(|sp| sp.to_security_key().ok()) 1869 .collect(); 1870 1871 if passkeys.is_empty() { 1872 return ( 1873 StatusCode::INTERNAL_SERVER_ERROR, 1874 Json(serde_json::json!({ 1875 "error": "server_error", 1876 "error_description": "Failed to load passkeys." 1877 })), 1878 ) 1879 .into_response(); 1880 } 1881 1882 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 1883 Ok(w) => w, 1884 Err(e) => { 1885 tracing::error!(error = %e, "Failed to create WebAuthn config"); 1886 return ( 1887 StatusCode::INTERNAL_SERVER_ERROR, 1888 Json(serde_json::json!({ 1889 "error": "server_error", 1890 "error_description": "WebAuthn configuration failed." 1891 })), 1892 ) 1893 .into_response(); 1894 } 1895 }; 1896 1897 let (rcr, auth_state) = match webauthn.start_authentication(passkeys) { 1898 Ok(result) => result, 1899 Err(e) => { 1900 tracing::error!(error = %e, "Failed to start passkey authentication"); 1901 return ( 1902 StatusCode::INTERNAL_SERVER_ERROR, 1903 Json(serde_json::json!({ 1904 "error": "server_error", 1905 "error_description": "Failed to start authentication." 1906 })), 1907 ) 1908 .into_response(); 1909 } 1910 }; 1911 1912 if let Err(e) = 1913 crate::auth::webauthn::save_authentication_state(&state.db, &user.did, &auth_state).await 1914 { 1915 tracing::error!(error = %e, "Failed to save authentication state"); 1916 return ( 1917 StatusCode::INTERNAL_SERVER_ERROR, 1918 Json(serde_json::json!({ 1919 "error": "server_error", 1920 "error_description": "An error occurred." 1921 })), 1922 ) 1923 .into_response(); 1924 } 1925 1926 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None) 1927 .await 1928 .is_err() 1929 { 1930 return ( 1931 StatusCode::INTERNAL_SERVER_ERROR, 1932 Json(serde_json::json!({ 1933 "error": "server_error", 1934 "error_description": "An error occurred." 1935 })), 1936 ) 1937 .into_response(); 1938 } 1939 1940 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 1941 1942 Json(PasskeyStartResponse { options }).into_response() 1943} 1944 1945#[derive(Debug, Deserialize)] 1946pub struct PasskeyFinishInput { 1947 pub request_uri: String, 1948 pub credential: serde_json::Value, 1949} 1950 1951pub async fn passkey_finish( 1952 State(state): State<AppState>, 1953 headers: HeaderMap, 1954 Json(form): Json<PasskeyFinishInput>, 1955) -> Response { 1956 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 1957 Ok(Some(data)) => data, 1958 Ok(None) => { 1959 return ( 1960 StatusCode::BAD_REQUEST, 1961 Json(serde_json::json!({ 1962 "error": "invalid_request", 1963 "error_description": "Invalid or expired request_uri." 1964 })), 1965 ) 1966 .into_response(); 1967 } 1968 Err(_) => { 1969 return ( 1970 StatusCode::INTERNAL_SERVER_ERROR, 1971 Json(serde_json::json!({ 1972 "error": "server_error", 1973 "error_description": "An error occurred." 1974 })), 1975 ) 1976 .into_response(); 1977 } 1978 }; 1979 1980 if request_data.expires_at < Utc::now() { 1981 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1982 return ( 1983 StatusCode::BAD_REQUEST, 1984 Json(serde_json::json!({ 1985 "error": "invalid_request", 1986 "error_description": "Authorization request has expired." 1987 })), 1988 ) 1989 .into_response(); 1990 } 1991 1992 let did = match request_data.did { 1993 Some(d) => d, 1994 None => { 1995 return ( 1996 StatusCode::BAD_REQUEST, 1997 Json(serde_json::json!({ 1998 "error": "invalid_request", 1999 "error_description": "No passkey authentication in progress." 2000 })), 2001 ) 2002 .into_response(); 2003 } 2004 }; 2005 2006 let auth_state = match crate::auth::webauthn::load_authentication_state(&state.db, &did).await { 2007 Ok(Some(s)) => s, 2008 Ok(None) => { 2009 return ( 2010 StatusCode::BAD_REQUEST, 2011 Json(serde_json::json!({ 2012 "error": "invalid_request", 2013 "error_description": "No passkey authentication in progress or challenge expired." 2014 })), 2015 ) 2016 .into_response(); 2017 } 2018 Err(e) => { 2019 tracing::error!(error = %e, "Failed to load authentication state"); 2020 return ( 2021 StatusCode::INTERNAL_SERVER_ERROR, 2022 Json(serde_json::json!({ 2023 "error": "server_error", 2024 "error_description": "An error occurred." 2025 })), 2026 ) 2027 .into_response(); 2028 } 2029 }; 2030 2031 let credential: webauthn_rs::prelude::PublicKeyCredential = 2032 match serde_json::from_value(form.credential) { 2033 Ok(c) => c, 2034 Err(e) => { 2035 tracing::warn!(error = %e, "Failed to parse credential"); 2036 return ( 2037 StatusCode::BAD_REQUEST, 2038 Json(serde_json::json!({ 2039 "error": "invalid_request", 2040 "error_description": "Failed to parse credential response." 2041 })), 2042 ) 2043 .into_response(); 2044 } 2045 }; 2046 2047 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2048 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 2049 Ok(w) => w, 2050 Err(e) => { 2051 tracing::error!(error = %e, "Failed to create WebAuthn config"); 2052 return ( 2053 StatusCode::INTERNAL_SERVER_ERROR, 2054 Json(serde_json::json!({ 2055 "error": "server_error", 2056 "error_description": "WebAuthn configuration failed." 2057 })), 2058 ) 2059 .into_response(); 2060 } 2061 }; 2062 2063 let auth_result = match webauthn.finish_authentication(&credential, &auth_state) { 2064 Ok(r) => r, 2065 Err(e) => { 2066 tracing::warn!(error = %e, did = %did, "Failed to verify passkey authentication"); 2067 return ( 2068 StatusCode::FORBIDDEN, 2069 Json(serde_json::json!({ 2070 "error": "access_denied", 2071 "error_description": "Passkey verification failed." 2072 })), 2073 ) 2074 .into_response(); 2075 } 2076 }; 2077 2078 if let Err(e) = crate::auth::webauthn::delete_authentication_state(&state.db, &did).await { 2079 tracing::warn!(error = %e, "Failed to delete authentication state"); 2080 } 2081 2082 if auth_result.needs_update() { 2083 match crate::auth::webauthn::update_passkey_counter( 2084 &state.db, 2085 auth_result.cred_id(), 2086 auth_result.counter(), 2087 ) 2088 .await 2089 { 2090 Ok(false) => { 2091 tracing::warn!(did = %did, "Passkey counter anomaly detected - possible cloned key"); 2092 return ( 2093 StatusCode::FORBIDDEN, 2094 Json(serde_json::json!({ 2095 "error": "access_denied", 2096 "error_description": "Security key counter anomaly detected. This may indicate a cloned key." 2097 })), 2098 ) 2099 .into_response(); 2100 } 2101 Err(e) => { 2102 tracing::warn!(error = %e, "Failed to update passkey counter"); 2103 } 2104 Ok(true) => {} 2105 } 2106 } 2107 2108 tracing::info!(did = %did, "Passkey authentication successful"); 2109 2110 let has_totp = crate::api::server::has_totp_enabled(&state, &did).await; 2111 if has_totp { 2112 return Json(serde_json::json!({ 2113 "needs_totp": true 2114 })) 2115 .into_response(); 2116 } 2117 2118 let user = sqlx::query!( 2119 "SELECT two_factor_enabled, preferred_comms_channel as \"preferred_comms_channel: CommsChannel\", id FROM users WHERE did = $1", 2120 did 2121 ) 2122 .fetch_optional(&state.db) 2123 .await; 2124 2125 if let Ok(Some(user)) = user 2126 && user.two_factor_enabled 2127 { 2128 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 2129 match db::create_2fa_challenge(&state.db, &did, &form.request_uri).await { 2130 Ok(challenge) => { 2131 let hostname = 2132 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2133 if let Err(e) = 2134 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await 2135 { 2136 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification"); 2137 } 2138 let channel_name = channel_display_name(user.preferred_comms_channel); 2139 return Json(serde_json::json!({ 2140 "needs_2fa": true, 2141 "channel": channel_name 2142 })) 2143 .into_response(); 2144 } 2145 Err(_) => { 2146 return ( 2147 StatusCode::INTERNAL_SERVER_ERROR, 2148 Json(serde_json::json!({ 2149 "error": "server_error", 2150 "error_description": "An error occurred." 2151 })), 2152 ) 2153 .into_response(); 2154 } 2155 } 2156 } 2157 2158 let device_id = extract_device_cookie(&headers); 2159 let requested_scope_str = request_data 2160 .parameters 2161 .scope 2162 .as_deref() 2163 .unwrap_or("atproto"); 2164 let requested_scopes: Vec<String> = requested_scope_str 2165 .split_whitespace() 2166 .map(|s| s.to_string()) 2167 .collect(); 2168 2169 let needs_consent = db::should_show_consent( 2170 &state.db, 2171 &did, 2172 &request_data.parameters.client_id, 2173 &requested_scopes, 2174 ) 2175 .await 2176 .unwrap_or(true); 2177 2178 if needs_consent { 2179 let consent_url = format!( 2180 "/#/oauth/consent?request_uri={}", 2181 url_encode(&form.request_uri) 2182 ); 2183 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 2184 } 2185 2186 let code = Code::generate(); 2187 if db::update_authorization_request( 2188 &state.db, 2189 &form.request_uri, 2190 &did, 2191 device_id.as_deref(), 2192 &code.0, 2193 ) 2194 .await 2195 .is_err() 2196 { 2197 return ( 2198 StatusCode::INTERNAL_SERVER_ERROR, 2199 Json(serde_json::json!({ 2200 "error": "server_error", 2201 "error_description": "An error occurred." 2202 })), 2203 ) 2204 .into_response(); 2205 } 2206 2207 let redirect_url = build_success_redirect( 2208 &request_data.parameters.redirect_uri, 2209 &code.0, 2210 request_data.parameters.state.as_deref(), 2211 request_data.parameters.response_mode.as_deref(), 2212 ); 2213 2214 Json(serde_json::json!({ 2215 "redirect_uri": redirect_url 2216 })) 2217 .into_response() 2218} 2219 2220#[derive(Debug, Deserialize)] 2221pub struct AuthorizePasskeyQuery { 2222 pub request_uri: String, 2223} 2224 2225#[derive(Debug, Serialize)] 2226#[serde(rename_all = "camelCase")] 2227pub struct PasskeyAuthResponse { 2228 pub options: serde_json::Value, 2229 pub request_uri: String, 2230} 2231 2232pub async fn authorize_passkey_start( 2233 State(state): State<AppState>, 2234 Query(query): Query<AuthorizePasskeyQuery>, 2235) -> Response { 2236 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2237 2238 let request_data = match db::get_authorization_request(&state.db, &query.request_uri).await { 2239 Ok(Some(d)) => d, 2240 Ok(None) => { 2241 return ( 2242 StatusCode::BAD_REQUEST, 2243 Json(serde_json::json!({ 2244 "error": "invalid_request", 2245 "error_description": "Authorization request not found." 2246 })), 2247 ) 2248 .into_response(); 2249 } 2250 Err(_) => { 2251 return ( 2252 StatusCode::INTERNAL_SERVER_ERROR, 2253 Json(serde_json::json!({ 2254 "error": "server_error", 2255 "error_description": "An error occurred." 2256 })), 2257 ) 2258 .into_response(); 2259 } 2260 }; 2261 2262 if request_data.expires_at < Utc::now() { 2263 let _ = db::delete_authorization_request(&state.db, &query.request_uri).await; 2264 return ( 2265 StatusCode::BAD_REQUEST, 2266 Json(serde_json::json!({ 2267 "error": "invalid_request", 2268 "error_description": "Authorization request has expired." 2269 })), 2270 ) 2271 .into_response(); 2272 } 2273 2274 let did = match &request_data.did { 2275 Some(d) => d.clone(), 2276 None => { 2277 return ( 2278 StatusCode::BAD_REQUEST, 2279 Json(serde_json::json!({ 2280 "error": "invalid_request", 2281 "error_description": "User not authenticated yet." 2282 })), 2283 ) 2284 .into_response(); 2285 } 2286 }; 2287 2288 let stored_passkeys = match crate::auth::webauthn::get_passkeys_for_user(&state.db, &did).await 2289 { 2290 Ok(pks) => pks, 2291 Err(e) => { 2292 tracing::error!("Failed to get passkeys: {:?}", e); 2293 return ( 2294 StatusCode::INTERNAL_SERVER_ERROR, 2295 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2296 ) 2297 .into_response(); 2298 } 2299 }; 2300 2301 if stored_passkeys.is_empty() { 2302 return ( 2303 StatusCode::BAD_REQUEST, 2304 Json(serde_json::json!({ 2305 "error": "invalid_request", 2306 "error_description": "No passkeys registered for this account." 2307 })), 2308 ) 2309 .into_response(); 2310 } 2311 2312 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys 2313 .iter() 2314 .filter_map(|sp| sp.to_security_key().ok()) 2315 .collect(); 2316 2317 if passkeys.is_empty() { 2318 return ( 2319 StatusCode::INTERNAL_SERVER_ERROR, 2320 Json(serde_json::json!({"error": "server_error", "error_description": "Failed to load passkeys."})), 2321 ) 2322 .into_response(); 2323 } 2324 2325 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 2326 Ok(w) => w, 2327 Err(e) => { 2328 tracing::error!("Failed to create WebAuthn config: {:?}", e); 2329 return ( 2330 StatusCode::INTERNAL_SERVER_ERROR, 2331 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2332 ) 2333 .into_response(); 2334 } 2335 }; 2336 2337 let (rcr, auth_state) = match webauthn.start_authentication(passkeys) { 2338 Ok(result) => result, 2339 Err(e) => { 2340 tracing::error!("Failed to start passkey authentication: {:?}", e); 2341 return ( 2342 StatusCode::INTERNAL_SERVER_ERROR, 2343 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2344 ) 2345 .into_response(); 2346 } 2347 }; 2348 2349 if let Err(e) = 2350 crate::auth::webauthn::save_authentication_state(&state.db, &did, &auth_state).await 2351 { 2352 tracing::error!("Failed to save authentication state: {:?}", e); 2353 return ( 2354 StatusCode::INTERNAL_SERVER_ERROR, 2355 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2356 ) 2357 .into_response(); 2358 } 2359 2360 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 2361 Json(PasskeyAuthResponse { 2362 options, 2363 request_uri: query.request_uri, 2364 }) 2365 .into_response() 2366} 2367 2368#[derive(Debug, Deserialize)] 2369#[serde(rename_all = "camelCase")] 2370pub struct AuthorizePasskeySubmit { 2371 pub request_uri: String, 2372 pub credential: serde_json::Value, 2373} 2374 2375pub async fn authorize_passkey_finish( 2376 State(state): State<AppState>, 2377 headers: HeaderMap, 2378 Json(form): Json<AuthorizePasskeySubmit>, 2379) -> Response { 2380 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2381 2382 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 2383 Ok(Some(d)) => d, 2384 Ok(None) => { 2385 return ( 2386 StatusCode::BAD_REQUEST, 2387 Json(serde_json::json!({ 2388 "error": "invalid_request", 2389 "error_description": "Authorization request not found." 2390 })), 2391 ) 2392 .into_response(); 2393 } 2394 Err(_) => { 2395 return ( 2396 StatusCode::INTERNAL_SERVER_ERROR, 2397 Json(serde_json::json!({ 2398 "error": "server_error", 2399 "error_description": "An error occurred." 2400 })), 2401 ) 2402 .into_response(); 2403 } 2404 }; 2405 2406 if request_data.expires_at < Utc::now() { 2407 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 2408 return ( 2409 StatusCode::BAD_REQUEST, 2410 Json(serde_json::json!({ 2411 "error": "invalid_request", 2412 "error_description": "Authorization request has expired." 2413 })), 2414 ) 2415 .into_response(); 2416 } 2417 2418 let did = match &request_data.did { 2419 Some(d) => d.clone(), 2420 None => { 2421 return ( 2422 StatusCode::BAD_REQUEST, 2423 Json(serde_json::json!({ 2424 "error": "invalid_request", 2425 "error_description": "User not authenticated yet." 2426 })), 2427 ) 2428 .into_response(); 2429 } 2430 }; 2431 2432 let auth_state = match crate::auth::webauthn::load_authentication_state(&state.db, &did).await { 2433 Ok(Some(s)) => s, 2434 Ok(None) => { 2435 return ( 2436 StatusCode::BAD_REQUEST, 2437 Json(serde_json::json!({ 2438 "error": "invalid_request", 2439 "error_description": "No passkey challenge found. Please start over." 2440 })), 2441 ) 2442 .into_response(); 2443 } 2444 Err(e) => { 2445 tracing::error!("Failed to load authentication state: {:?}", e); 2446 return ( 2447 StatusCode::INTERNAL_SERVER_ERROR, 2448 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2449 ) 2450 .into_response(); 2451 } 2452 }; 2453 2454 let credential: webauthn_rs::prelude::PublicKeyCredential = 2455 match serde_json::from_value(form.credential.clone()) { 2456 Ok(c) => c, 2457 Err(e) => { 2458 tracing::error!("Failed to parse credential: {:?}", e); 2459 return ( 2460 StatusCode::BAD_REQUEST, 2461 Json(serde_json::json!({ 2462 "error": "invalid_request", 2463 "error_description": "Invalid credential format." 2464 })), 2465 ) 2466 .into_response(); 2467 } 2468 }; 2469 2470 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 2471 Ok(w) => w, 2472 Err(e) => { 2473 tracing::error!("Failed to create WebAuthn config: {:?}", e); 2474 return ( 2475 StatusCode::INTERNAL_SERVER_ERROR, 2476 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2477 ) 2478 .into_response(); 2479 } 2480 }; 2481 2482 let auth_result = match webauthn.finish_authentication(&credential, &auth_state) { 2483 Ok(r) => r, 2484 Err(e) => { 2485 tracing::warn!("Passkey authentication failed: {:?}", e); 2486 return ( 2487 StatusCode::FORBIDDEN, 2488 Json(serde_json::json!({ 2489 "error": "access_denied", 2490 "error_description": "Passkey authentication failed." 2491 })), 2492 ) 2493 .into_response(); 2494 } 2495 }; 2496 2497 let _ = crate::auth::webauthn::delete_authentication_state(&state.db, &did).await; 2498 2499 match crate::auth::webauthn::update_passkey_counter( 2500 &state.db, 2501 credential.id.as_ref(), 2502 auth_result.counter(), 2503 ) 2504 .await 2505 { 2506 Ok(false) => { 2507 tracing::warn!(did = %did, "Passkey counter anomaly detected - possible cloned key"); 2508 return ( 2509 StatusCode::FORBIDDEN, 2510 Json(serde_json::json!({ 2511 "error": "access_denied", 2512 "error_description": "Security key counter anomaly detected. This may indicate a cloned key." 2513 })), 2514 ) 2515 .into_response(); 2516 } 2517 Err(e) => { 2518 tracing::warn!("Failed to update passkey counter: {:?}", e); 2519 } 2520 Ok(true) => {} 2521 } 2522 2523 let has_totp = crate::api::server::has_totp_enabled_db(&state.db, &did).await; 2524 if has_totp { 2525 let device_cookie = extract_device_cookie(&headers); 2526 let device_is_trusted = if let Some(ref dev_id) = device_cookie { 2527 crate::api::server::is_device_trusted(&state.db, dev_id, &did).await 2528 } else { 2529 false 2530 }; 2531 2532 if device_is_trusted { 2533 if let Some(ref dev_id) = device_cookie { 2534 let _ = crate::api::server::extend_device_trust(&state.db, dev_id).await; 2535 } 2536 } else { 2537 let user = match sqlx::query!( 2538 r#"SELECT id, preferred_comms_channel as "preferred_comms_channel: CommsChannel" FROM users WHERE did = $1"#, 2539 did 2540 ) 2541 .fetch_optional(&state.db) 2542 .await 2543 { 2544 Ok(Some(u)) => u, 2545 _ => { 2546 return ( 2547 StatusCode::INTERNAL_SERVER_ERROR, 2548 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2549 ) 2550 .into_response(); 2551 } 2552 }; 2553 2554 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 2555 match db::create_2fa_challenge(&state.db, &did, &form.request_uri).await { 2556 Ok(challenge) => { 2557 if let Err(e) = 2558 enqueue_2fa_code(&state.db, user.id, &challenge.code, &pds_hostname).await 2559 { 2560 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification"); 2561 } 2562 let channel_name = channel_display_name(user.preferred_comms_channel); 2563 let redirect_url = format!( 2564 "/#/oauth/2fa?request_uri={}&channel={}", 2565 url_encode(&form.request_uri), 2566 url_encode(channel_name) 2567 ); 2568 return ( 2569 StatusCode::OK, 2570 Json(serde_json::json!({ 2571 "next": "2fa", 2572 "redirect": redirect_url 2573 })), 2574 ) 2575 .into_response(); 2576 } 2577 Err(_) => { 2578 return ( 2579 StatusCode::INTERNAL_SERVER_ERROR, 2580 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2581 ) 2582 .into_response(); 2583 } 2584 } 2585 } 2586 } 2587 2588 let redirect_url = format!( 2589 "/#/oauth/consent?request_uri={}", 2590 url_encode(&form.request_uri) 2591 ); 2592 ( 2593 StatusCode::OK, 2594 Json(serde_json::json!({ 2595 "next": "consent", 2596 "redirect": redirect_url 2597 })), 2598 ) 2599 .into_response() 2600}