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