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 let Some(bare_handle) = 430 normalized_username.strip_suffix(&format!(".{}", pds_hostname)) 431 { 432 bare_handle.to_string() 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, 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 let password_valid = match bcrypt::verify(&form.password, &user.password_hash) { 483 Ok(valid) => valid, 484 Err(_) => return show_login_error("An error occurred. Please try again.", json_response), 485 }; 486 if !password_valid { 487 return show_login_error("Invalid handle/email or password.", json_response); 488 } 489 let has_totp = crate::api::server::has_totp_enabled(&state, &user.did).await; 490 if has_totp { 491 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None) 492 .await 493 .is_err() 494 { 495 return show_login_error("An error occurred. Please try again.", json_response); 496 } 497 if json_response { 498 return Json(serde_json::json!({ 499 "needs_totp": true 500 })) 501 .into_response(); 502 } 503 return redirect_see_other(&format!( 504 "/#/oauth/totp?request_uri={}", 505 url_encode(&form.request_uri) 506 )); 507 } 508 if user.two_factor_enabled { 509 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 510 match db::create_2fa_challenge(&state.db, &user.did, &form.request_uri).await { 511 Ok(challenge) => { 512 let hostname = 513 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 514 if let Err(e) = 515 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await 516 { 517 tracing::warn!( 518 did = %user.did, 519 error = %e, 520 "Failed to enqueue 2FA notification" 521 ); 522 } 523 let channel_name = channel_display_name(user.preferred_comms_channel); 524 if json_response { 525 return Json(serde_json::json!({ 526 "needs_2fa": true, 527 "channel": channel_name 528 })) 529 .into_response(); 530 } 531 return redirect_see_other(&format!( 532 "/#/oauth/2fa?request_uri={}&channel={}", 533 url_encode(&form.request_uri), 534 url_encode(channel_name) 535 )); 536 } 537 Err(_) => { 538 return show_login_error("An error occurred. Please try again.", json_response); 539 } 540 } 541 } 542 let mut device_id: Option<String> = extract_device_cookie(&headers); 543 let mut new_cookie: Option<String> = None; 544 if form.remember_device { 545 let final_device_id = if let Some(existing_id) = &device_id { 546 existing_id.clone() 547 } else { 548 let new_id = DeviceId::generate(); 549 let device_data = DeviceData { 550 session_id: SessionId::generate().0, 551 user_agent: extract_user_agent(&headers), 552 ip_address: extract_client_ip(&headers), 553 last_seen_at: Utc::now(), 554 }; 555 if db::create_device(&state.db, &new_id.0, &device_data) 556 .await 557 .is_ok() 558 { 559 new_cookie = Some(make_device_cookie(&new_id.0)); 560 device_id = Some(new_id.0.clone()); 561 } 562 new_id.0 563 }; 564 let _ = db::upsert_account_device(&state.db, &user.did, &final_device_id).await; 565 } 566 if db::set_authorization_did( 567 &state.db, 568 &form.request_uri, 569 &user.did, 570 device_id.as_deref(), 571 ) 572 .await 573 .is_err() 574 { 575 return show_login_error("An error occurred. Please try again.", json_response); 576 } 577 let requested_scope_str = request_data 578 .parameters 579 .scope 580 .as_deref() 581 .unwrap_or("atproto"); 582 let requested_scopes: Vec<String> = requested_scope_str 583 .split_whitespace() 584 .map(|s| s.to_string()) 585 .collect(); 586 let needs_consent = db::should_show_consent( 587 &state.db, 588 &user.did, 589 &request_data.parameters.client_id, 590 &requested_scopes, 591 ) 592 .await 593 .unwrap_or(true); 594 if needs_consent { 595 let consent_url = format!( 596 "/#/oauth/consent?request_uri={}", 597 url_encode(&form.request_uri) 598 ); 599 if json_response { 600 if let Some(cookie) = new_cookie { 601 return ( 602 StatusCode::OK, 603 [(SET_COOKIE, cookie)], 604 Json(serde_json::json!({"redirect_uri": consent_url})), 605 ) 606 .into_response(); 607 } 608 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 609 } 610 if let Some(cookie) = new_cookie { 611 return ( 612 StatusCode::SEE_OTHER, 613 [(SET_COOKIE, cookie), (LOCATION, consent_url)], 614 ) 615 .into_response(); 616 } 617 return redirect_see_other(&consent_url); 618 } 619 let code = Code::generate(); 620 if db::update_authorization_request( 621 &state.db, 622 &form.request_uri, 623 &user.did, 624 device_id.as_deref(), 625 &code.0, 626 ) 627 .await 628 .is_err() 629 { 630 return show_login_error("An error occurred. Please try again.", json_response); 631 } 632 let redirect_url = build_success_redirect( 633 &request_data.parameters.redirect_uri, 634 &code.0, 635 request_data.parameters.state.as_deref(), 636 request_data.parameters.response_mode.as_deref(), 637 ); 638 if json_response { 639 if let Some(cookie) = new_cookie { 640 ( 641 StatusCode::OK, 642 [(SET_COOKIE, cookie)], 643 Json(serde_json::json!({"redirect_uri": redirect_url})), 644 ) 645 .into_response() 646 } else { 647 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 648 } 649 } else if let Some(cookie) = new_cookie { 650 ( 651 StatusCode::SEE_OTHER, 652 [(SET_COOKIE, cookie), (LOCATION, redirect_url)], 653 ) 654 .into_response() 655 } else { 656 redirect_see_other(&redirect_url) 657 } 658} 659 660pub async fn authorize_select( 661 State(state): State<AppState>, 662 headers: HeaderMap, 663 Json(form): Json<AuthorizeSelectSubmit>, 664) -> Response { 665 let json_error = |status: StatusCode, error: &str, description: &str| -> Response { 666 ( 667 status, 668 Json(serde_json::json!({ 669 "error": error, 670 "error_description": description 671 })), 672 ) 673 .into_response() 674 }; 675 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 676 Ok(Some(data)) => data, 677 Ok(None) => { 678 return json_error( 679 StatusCode::BAD_REQUEST, 680 "invalid_request", 681 "Invalid or expired request_uri. Please start a new authorization request.", 682 ); 683 } 684 Err(_) => { 685 return json_error( 686 StatusCode::INTERNAL_SERVER_ERROR, 687 "server_error", 688 "An error occurred. Please try again.", 689 ); 690 } 691 }; 692 if request_data.expires_at < Utc::now() { 693 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 694 return json_error( 695 StatusCode::BAD_REQUEST, 696 "invalid_request", 697 "Authorization request has expired. Please start a new request.", 698 ); 699 } 700 let device_id = match extract_device_cookie(&headers) { 701 Some(id) => id, 702 None => { 703 return json_error( 704 StatusCode::BAD_REQUEST, 705 "invalid_request", 706 "No device session found. Please sign in.", 707 ); 708 } 709 }; 710 let account_valid = match db::verify_account_on_device(&state.db, &device_id, &form.did).await { 711 Ok(valid) => valid, 712 Err(_) => { 713 return json_error( 714 StatusCode::INTERNAL_SERVER_ERROR, 715 "server_error", 716 "An error occurred. Please try again.", 717 ); 718 } 719 }; 720 if !account_valid { 721 return json_error( 722 StatusCode::FORBIDDEN, 723 "access_denied", 724 "This account is not available on this device. Please sign in.", 725 ); 726 } 727 let user = match sqlx::query!( 728 r#" 729 SELECT id, two_factor_enabled, 730 preferred_comms_channel as "preferred_comms_channel: CommsChannel", 731 email_verified, discord_verified, telegram_verified, signal_verified 732 FROM users 733 WHERE did = $1 734 "#, 735 form.did 736 ) 737 .fetch_optional(&state.db) 738 .await 739 { 740 Ok(Some(u)) => u, 741 Ok(None) => { 742 return json_error( 743 StatusCode::FORBIDDEN, 744 "access_denied", 745 "Account not found. Please sign in.", 746 ); 747 } 748 Err(_) => { 749 return json_error( 750 StatusCode::INTERNAL_SERVER_ERROR, 751 "server_error", 752 "An error occurred. Please try again.", 753 ); 754 } 755 }; 756 let is_verified = user.email_verified 757 || user.discord_verified 758 || user.telegram_verified 759 || user.signal_verified; 760 if !is_verified { 761 return json_error( 762 StatusCode::FORBIDDEN, 763 "access_denied", 764 "Please verify your account before logging in.", 765 ); 766 } 767 let has_totp = crate::api::server::has_totp_enabled(&state, &form.did).await; 768 if has_totp { 769 if db::set_authorization_did(&state.db, &form.request_uri, &form.did, Some(&device_id)) 770 .await 771 .is_err() 772 { 773 return json_error( 774 StatusCode::INTERNAL_SERVER_ERROR, 775 "server_error", 776 "An error occurred. Please try again.", 777 ); 778 } 779 return Json(serde_json::json!({ 780 "needs_totp": true 781 })) 782 .into_response(); 783 } 784 if user.two_factor_enabled { 785 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 786 match db::create_2fa_challenge(&state.db, &form.did, &form.request_uri).await { 787 Ok(challenge) => { 788 let hostname = 789 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 790 if let Err(e) = 791 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await 792 { 793 tracing::warn!( 794 did = %form.did, 795 error = %e, 796 "Failed to enqueue 2FA notification" 797 ); 798 } 799 let channel_name = channel_display_name(user.preferred_comms_channel); 800 return Json(serde_json::json!({ 801 "needs_2fa": true, 802 "channel": channel_name 803 })) 804 .into_response(); 805 } 806 Err(_) => { 807 return json_error( 808 StatusCode::INTERNAL_SERVER_ERROR, 809 "server_error", 810 "An error occurred. Please try again.", 811 ); 812 } 813 } 814 } 815 let _ = db::upsert_account_device(&state.db, &form.did, &device_id).await; 816 let code = Code::generate(); 817 if db::update_authorization_request( 818 &state.db, 819 &form.request_uri, 820 &form.did, 821 Some(&device_id), 822 &code.0, 823 ) 824 .await 825 .is_err() 826 { 827 return json_error( 828 StatusCode::INTERNAL_SERVER_ERROR, 829 "server_error", 830 "An error occurred. Please try again.", 831 ); 832 } 833 let redirect_url = build_success_redirect( 834 &request_data.parameters.redirect_uri, 835 &code.0, 836 request_data.parameters.state.as_deref(), 837 request_data.parameters.response_mode.as_deref(), 838 ); 839 Json(serde_json::json!({ 840 "redirect_uri": redirect_url 841 })) 842 .into_response() 843} 844 845fn build_success_redirect( 846 redirect_uri: &str, 847 code: &str, 848 state: Option<&str>, 849 response_mode: Option<&str>, 850) -> String { 851 let mut redirect_url = redirect_uri.to_string(); 852 let use_fragment = response_mode == Some("fragment"); 853 let separator = if use_fragment { 854 '#' 855 } else if redirect_url.contains('?') { 856 '&' 857 } else { 858 '?' 859 }; 860 redirect_url.push(separator); 861 redirect_url.push_str(&format!("code={}", url_encode(code))); 862 if let Some(req_state) = state { 863 redirect_url.push_str(&format!("&state={}", url_encode(req_state))); 864 } 865 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 866 redirect_url.push_str(&format!( 867 "&iss={}", 868 url_encode(&format!("https://{}", pds_hostname)) 869 )); 870 redirect_url 871} 872 873#[derive(Debug, Serialize)] 874pub struct AuthorizeDenyResponse { 875 pub error: String, 876 pub error_description: String, 877} 878 879pub async fn authorize_deny( 880 State(state): State<AppState>, 881 Json(form): Json<AuthorizeDenyForm>, 882) -> Response { 883 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 884 Ok(Some(data)) => data, 885 Ok(None) => { 886 return ( 887 StatusCode::BAD_REQUEST, 888 Json(serde_json::json!({ 889 "error": "invalid_request", 890 "error_description": "Invalid request_uri" 891 })), 892 ) 893 .into_response(); 894 } 895 Err(_) => { 896 return ( 897 StatusCode::INTERNAL_SERVER_ERROR, 898 Json(serde_json::json!({ 899 "error": "server_error", 900 "error_description": "An error occurred" 901 })), 902 ) 903 .into_response(); 904 } 905 }; 906 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 907 let redirect_uri = &request_data.parameters.redirect_uri; 908 let mut redirect_url = redirect_uri.to_string(); 909 let separator = if redirect_url.contains('?') { '&' } else { '?' }; 910 redirect_url.push(separator); 911 redirect_url.push_str("error=access_denied"); 912 redirect_url.push_str("&error_description=User%20denied%20the%20request"); 913 if let Some(state) = &request_data.parameters.state { 914 redirect_url.push_str(&format!("&state={}", url_encode(state))); 915 } 916 Json(serde_json::json!({ 917 "redirect_uri": redirect_url 918 })) 919 .into_response() 920} 921 922#[derive(Debug, Deserialize)] 923pub struct AuthorizeDenyForm { 924 pub request_uri: String, 925} 926 927#[derive(Debug, Deserialize)] 928pub struct Authorize2faQuery { 929 pub request_uri: String, 930 pub channel: Option<String>, 931} 932 933#[derive(Debug, Deserialize)] 934pub struct Authorize2faSubmit { 935 pub request_uri: String, 936 pub code: String, 937} 938 939const MAX_2FA_ATTEMPTS: i32 = 5; 940 941pub async fn authorize_2fa_get( 942 State(state): State<AppState>, 943 Query(query): Query<Authorize2faQuery>, 944) -> Response { 945 let challenge = match db::get_2fa_challenge(&state.db, &query.request_uri).await { 946 Ok(Some(c)) => c, 947 Ok(None) => { 948 return redirect_to_frontend_error( 949 "invalid_request", 950 "No 2FA challenge found. Please start over.", 951 ); 952 } 953 Err(_) => { 954 return redirect_to_frontend_error( 955 "server_error", 956 "An error occurred. Please try again.", 957 ); 958 } 959 }; 960 if challenge.expires_at < Utc::now() { 961 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 962 return redirect_to_frontend_error( 963 "invalid_request", 964 "2FA code has expired. Please start over.", 965 ); 966 } 967 let _request_data = match db::get_authorization_request(&state.db, &query.request_uri).await { 968 Ok(Some(d)) => d, 969 Ok(None) => { 970 return redirect_to_frontend_error( 971 "invalid_request", 972 "Authorization request not found. Please start over.", 973 ); 974 } 975 Err(_) => { 976 return redirect_to_frontend_error( 977 "server_error", 978 "An error occurred. Please try again.", 979 ); 980 } 981 }; 982 let channel = query.channel.as_deref().unwrap_or("email"); 983 redirect_see_other(&format!( 984 "/#/oauth/2fa?request_uri={}&channel={}", 985 url_encode(&query.request_uri), 986 url_encode(channel) 987 )) 988} 989 990#[derive(Debug, Serialize)] 991pub struct ScopeInfo { 992 pub scope: String, 993 pub category: String, 994 pub required: bool, 995 pub description: String, 996 pub display_name: String, 997 pub granted: Option<bool>, 998} 999 1000#[derive(Debug, Serialize)] 1001pub struct ConsentResponse { 1002 pub request_uri: String, 1003 pub client_id: String, 1004 pub client_name: Option<String>, 1005 pub client_uri: Option<String>, 1006 pub logo_uri: Option<String>, 1007 pub scopes: Vec<ScopeInfo>, 1008 pub show_consent: bool, 1009 pub did: String, 1010} 1011 1012#[derive(Debug, Deserialize)] 1013pub struct ConsentQuery { 1014 pub request_uri: String, 1015} 1016 1017#[derive(Debug, Deserialize)] 1018pub struct ConsentSubmit { 1019 pub request_uri: String, 1020 pub approved_scopes: Vec<String>, 1021 pub remember: bool, 1022} 1023 1024pub async fn consent_get( 1025 State(state): State<AppState>, 1026 Query(query): Query<ConsentQuery>, 1027) -> Response { 1028 let request_data = match db::get_authorization_request(&state.db, &query.request_uri).await { 1029 Ok(Some(data)) => data, 1030 Ok(None) => { 1031 return ( 1032 StatusCode::BAD_REQUEST, 1033 Json(serde_json::json!({ 1034 "error": "invalid_request", 1035 "error_description": "Invalid or expired request_uri" 1036 })), 1037 ) 1038 .into_response(); 1039 } 1040 Err(e) => { 1041 return ( 1042 StatusCode::INTERNAL_SERVER_ERROR, 1043 Json(serde_json::json!({ 1044 "error": "server_error", 1045 "error_description": format!("Database error: {:?}", e) 1046 })), 1047 ) 1048 .into_response(); 1049 } 1050 }; 1051 if request_data.expires_at < Utc::now() { 1052 let _ = db::delete_authorization_request(&state.db, &query.request_uri).await; 1053 return ( 1054 StatusCode::BAD_REQUEST, 1055 Json(serde_json::json!({ 1056 "error": "invalid_request", 1057 "error_description": "Authorization request has expired" 1058 })), 1059 ) 1060 .into_response(); 1061 } 1062 let did = match &request_data.did { 1063 Some(d) => d.clone(), 1064 None => { 1065 return ( 1066 StatusCode::FORBIDDEN, 1067 Json(serde_json::json!({ 1068 "error": "access_denied", 1069 "error_description": "Not authenticated" 1070 })), 1071 ) 1072 .into_response(); 1073 } 1074 }; 1075 let client_cache = ClientMetadataCache::new(3600); 1076 let client_metadata = client_cache 1077 .get(&request_data.parameters.client_id) 1078 .await 1079 .ok(); 1080 let requested_scope_str = request_data 1081 .parameters 1082 .scope 1083 .as_deref() 1084 .unwrap_or("atproto"); 1085 let requested_scopes: Vec<&str> = requested_scope_str.split_whitespace().collect(); 1086 let preferences = 1087 db::get_scope_preferences(&state.db, &did, &request_data.parameters.client_id) 1088 .await 1089 .unwrap_or_default(); 1090 let pref_map: std::collections::HashMap<_, _> = preferences 1091 .iter() 1092 .map(|p| (p.scope.as_str(), p.granted)) 1093 .collect(); 1094 let requested_scope_strings: Vec<String> = 1095 requested_scopes.iter().map(|s| s.to_string()).collect(); 1096 let show_consent = db::should_show_consent( 1097 &state.db, 1098 &did, 1099 &request_data.parameters.client_id, 1100 &requested_scope_strings, 1101 ) 1102 .await 1103 .unwrap_or(true); 1104 let mut scopes = Vec::new(); 1105 for scope in &requested_scopes { 1106 let (category, required, description, display_name) = 1107 if let Some(def) = crate::oauth::scopes::SCOPE_DEFINITIONS.get(*scope) { 1108 ( 1109 def.category.display_name().to_string(), 1110 def.required, 1111 def.description.to_string(), 1112 def.display_name.to_string(), 1113 ) 1114 } else if scope.starts_with("ref:") { 1115 ( 1116 "Reference".to_string(), 1117 false, 1118 "Referenced scope".to_string(), 1119 scope.to_string(), 1120 ) 1121 } else { 1122 ( 1123 "Other".to_string(), 1124 false, 1125 format!("Access to {}", scope), 1126 scope.to_string(), 1127 ) 1128 }; 1129 let granted = pref_map.get(*scope).copied(); 1130 scopes.push(ScopeInfo { 1131 scope: scope.to_string(), 1132 category, 1133 required, 1134 description, 1135 display_name, 1136 granted, 1137 }); 1138 } 1139 Json(ConsentResponse { 1140 request_uri: query.request_uri.clone(), 1141 client_id: request_data.parameters.client_id.clone(), 1142 client_name: client_metadata.as_ref().and_then(|m| m.client_name.clone()), 1143 client_uri: client_metadata.as_ref().and_then(|m| m.client_uri.clone()), 1144 logo_uri: client_metadata.as_ref().and_then(|m| m.logo_uri.clone()), 1145 scopes, 1146 show_consent, 1147 did, 1148 }) 1149 .into_response() 1150} 1151 1152pub async fn consent_post( 1153 State(state): State<AppState>, 1154 Json(form): Json<ConsentSubmit>, 1155) -> Response { 1156 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 1157 Ok(Some(data)) => data, 1158 Ok(None) => { 1159 return ( 1160 StatusCode::BAD_REQUEST, 1161 Json(serde_json::json!({ 1162 "error": "invalid_request", 1163 "error_description": "Invalid or expired request_uri" 1164 })), 1165 ) 1166 .into_response(); 1167 } 1168 Err(e) => { 1169 return ( 1170 StatusCode::INTERNAL_SERVER_ERROR, 1171 Json(serde_json::json!({ 1172 "error": "server_error", 1173 "error_description": format!("Database error: {:?}", e) 1174 })), 1175 ) 1176 .into_response(); 1177 } 1178 }; 1179 if request_data.expires_at < Utc::now() { 1180 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1181 return ( 1182 StatusCode::BAD_REQUEST, 1183 Json(serde_json::json!({ 1184 "error": "invalid_request", 1185 "error_description": "Authorization request has expired" 1186 })), 1187 ) 1188 .into_response(); 1189 } 1190 let did = match &request_data.did { 1191 Some(d) => d.clone(), 1192 None => { 1193 return ( 1194 StatusCode::FORBIDDEN, 1195 Json(serde_json::json!({ 1196 "error": "access_denied", 1197 "error_description": "Not authenticated" 1198 })), 1199 ) 1200 .into_response(); 1201 } 1202 }; 1203 let requested_scope_str = request_data 1204 .parameters 1205 .scope 1206 .as_deref() 1207 .unwrap_or("atproto"); 1208 let requested_scopes: Vec<&str> = requested_scope_str.split_whitespace().collect(); 1209 let has_granular_scopes = requested_scopes.iter().any(|s| { 1210 s.starts_with("repo:") 1211 || s.starts_with("blob:") 1212 || s.starts_with("rpc:") 1213 || s.starts_with("account:") 1214 || s.starts_with("identity:") 1215 }); 1216 let user_denied_some_granular = has_granular_scopes 1217 && requested_scopes 1218 .iter() 1219 .filter(|s| { 1220 s.starts_with("repo:") 1221 || s.starts_with("blob:") 1222 || s.starts_with("rpc:") 1223 || s.starts_with("account:") 1224 || s.starts_with("identity:") 1225 }) 1226 .any(|s| !form.approved_scopes.contains(&s.to_string())); 1227 let atproto_was_requested = requested_scopes.contains(&"atproto"); 1228 if atproto_was_requested 1229 && !has_granular_scopes 1230 && !form.approved_scopes.contains(&"atproto".to_string()) 1231 { 1232 return ( 1233 StatusCode::BAD_REQUEST, 1234 Json(serde_json::json!({ 1235 "error": "invalid_request", 1236 "error_description": "The atproto scope was requested and must be approved" 1237 })), 1238 ) 1239 .into_response(); 1240 } 1241 let final_approved: Vec<String> = if user_denied_some_granular { 1242 form.approved_scopes 1243 .iter() 1244 .filter(|s| *s != "atproto") 1245 .cloned() 1246 .collect() 1247 } else { 1248 form.approved_scopes.clone() 1249 }; 1250 if final_approved.is_empty() { 1251 return ( 1252 StatusCode::BAD_REQUEST, 1253 Json(serde_json::json!({ 1254 "error": "invalid_request", 1255 "error_description": "At least one scope must be approved" 1256 })), 1257 ) 1258 .into_response(); 1259 } 1260 let approved_scope_str = final_approved.join(" "); 1261 let has_valid_scope = final_approved.iter().all(|s| { 1262 s == "atproto" 1263 || s == "transition:generic" 1264 || s == "transition:chat.bsky" 1265 || s == "transition:email" 1266 || s.starts_with("repo:") 1267 || s.starts_with("blob:") 1268 || s.starts_with("rpc:") 1269 || s.starts_with("account:") 1270 || s.starts_with("include:") 1271 }); 1272 if !has_valid_scope { 1273 return ( 1274 StatusCode::BAD_REQUEST, 1275 Json(serde_json::json!({ 1276 "error": "invalid_request", 1277 "error_description": "Invalid scope format" 1278 })), 1279 ) 1280 .into_response(); 1281 } 1282 if form.remember { 1283 let preferences: Vec<db::ScopePreference> = requested_scopes 1284 .iter() 1285 .map(|s| db::ScopePreference { 1286 scope: s.to_string(), 1287 granted: form.approved_scopes.contains(&s.to_string()), 1288 }) 1289 .collect(); 1290 let _ = db::upsert_scope_preferences( 1291 &state.db, 1292 &did, 1293 &request_data.parameters.client_id, 1294 &preferences, 1295 ) 1296 .await; 1297 } 1298 if let Err(e) = 1299 db::update_request_scope(&state.db, &form.request_uri, &approved_scope_str).await 1300 { 1301 tracing::warn!("Failed to update request scope: {:?}", e); 1302 } 1303 let code = Code::generate(); 1304 if db::update_authorization_request( 1305 &state.db, 1306 &form.request_uri, 1307 &did, 1308 request_data.device_id.as_deref(), 1309 &code.0, 1310 ) 1311 .await 1312 .is_err() 1313 { 1314 return ( 1315 StatusCode::INTERNAL_SERVER_ERROR, 1316 Json(serde_json::json!({ 1317 "error": "server_error", 1318 "error_description": "Failed to complete authorization" 1319 })), 1320 ) 1321 .into_response(); 1322 } 1323 let redirect_url = build_success_redirect( 1324 &request_data.parameters.redirect_uri, 1325 &code.0, 1326 request_data.parameters.state.as_deref(), 1327 request_data.parameters.response_mode.as_deref(), 1328 ); 1329 Json(serde_json::json!({ 1330 "redirect_uri": redirect_url 1331 })) 1332 .into_response() 1333} 1334 1335pub async fn authorize_2fa_post( 1336 State(state): State<AppState>, 1337 headers: HeaderMap, 1338 Json(form): Json<Authorize2faSubmit>, 1339) -> Response { 1340 let json_error = |status: StatusCode, error: &str, description: &str| -> Response { 1341 ( 1342 status, 1343 Json(serde_json::json!({ 1344 "error": error, 1345 "error_description": description 1346 })), 1347 ) 1348 .into_response() 1349 }; 1350 let client_ip = extract_client_ip(&headers); 1351 if !state 1352 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip) 1353 .await 1354 { 1355 tracing::warn!(ip = %client_ip, "OAuth 2FA rate limit exceeded"); 1356 return json_error( 1357 StatusCode::TOO_MANY_REQUESTS, 1358 "RateLimitExceeded", 1359 "Too many attempts. Please try again later.", 1360 ); 1361 } 1362 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 1363 Ok(Some(d)) => d, 1364 Ok(None) => { 1365 return json_error( 1366 StatusCode::BAD_REQUEST, 1367 "invalid_request", 1368 "Authorization request not found.", 1369 ); 1370 } 1371 Err(_) => { 1372 return json_error( 1373 StatusCode::INTERNAL_SERVER_ERROR, 1374 "server_error", 1375 "An error occurred.", 1376 ); 1377 } 1378 }; 1379 if request_data.expires_at < Utc::now() { 1380 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1381 return json_error( 1382 StatusCode::BAD_REQUEST, 1383 "invalid_request", 1384 "Authorization request has expired.", 1385 ); 1386 } 1387 let challenge = db::get_2fa_challenge(&state.db, &form.request_uri) 1388 .await 1389 .ok() 1390 .flatten(); 1391 if let Some(challenge) = challenge { 1392 if challenge.expires_at < Utc::now() { 1393 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 1394 return json_error( 1395 StatusCode::BAD_REQUEST, 1396 "invalid_request", 1397 "2FA code has expired. Please start over.", 1398 ); 1399 } 1400 if challenge.attempts >= MAX_2FA_ATTEMPTS { 1401 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 1402 return json_error( 1403 StatusCode::FORBIDDEN, 1404 "access_denied", 1405 "Too many failed attempts. Please start over.", 1406 ); 1407 } 1408 let code_valid: bool = form 1409 .code 1410 .trim() 1411 .as_bytes() 1412 .ct_eq(challenge.code.as_bytes()) 1413 .into(); 1414 if !code_valid { 1415 let _ = db::increment_2fa_attempts(&state.db, challenge.id).await; 1416 return json_error( 1417 StatusCode::FORBIDDEN, 1418 "invalid_code", 1419 "Invalid verification code. Please try again.", 1420 ); 1421 } 1422 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 1423 let code = Code::generate(); 1424 let device_id = extract_device_cookie(&headers); 1425 if db::update_authorization_request( 1426 &state.db, 1427 &form.request_uri, 1428 &challenge.did, 1429 device_id.as_deref(), 1430 &code.0, 1431 ) 1432 .await 1433 .is_err() 1434 { 1435 return json_error( 1436 StatusCode::INTERNAL_SERVER_ERROR, 1437 "server_error", 1438 "An error occurred. Please try again.", 1439 ); 1440 } 1441 let redirect_url = build_success_redirect( 1442 &request_data.parameters.redirect_uri, 1443 &code.0, 1444 request_data.parameters.state.as_deref(), 1445 request_data.parameters.response_mode.as_deref(), 1446 ); 1447 return Json(serde_json::json!({ 1448 "redirect_uri": redirect_url 1449 })) 1450 .into_response(); 1451 } 1452 let did = match &request_data.did { 1453 Some(d) => d.clone(), 1454 None => { 1455 return json_error( 1456 StatusCode::BAD_REQUEST, 1457 "invalid_request", 1458 "No 2FA challenge found. Please start over.", 1459 ); 1460 } 1461 }; 1462 if !crate::api::server::has_totp_enabled(&state, &did).await { 1463 return json_error( 1464 StatusCode::BAD_REQUEST, 1465 "invalid_request", 1466 "No 2FA challenge found. Please start over.", 1467 ); 1468 } 1469 let totp_valid = 1470 crate::api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await; 1471 if !totp_valid { 1472 return json_error( 1473 StatusCode::FORBIDDEN, 1474 "invalid_code", 1475 "Invalid verification code. Please try again.", 1476 ); 1477 } 1478 let requested_scope_str = request_data 1479 .parameters 1480 .scope 1481 .as_deref() 1482 .unwrap_or("atproto"); 1483 let requested_scopes: Vec<String> = requested_scope_str 1484 .split_whitespace() 1485 .map(|s| s.to_string()) 1486 .collect(); 1487 let needs_consent = db::should_show_consent( 1488 &state.db, 1489 &did, 1490 &request_data.parameters.client_id, 1491 &requested_scopes, 1492 ) 1493 .await 1494 .unwrap_or(true); 1495 if needs_consent { 1496 let consent_url = format!( 1497 "/#/oauth/consent?request_uri={}", 1498 url_encode(&form.request_uri) 1499 ); 1500 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 1501 } 1502 let code = Code::generate(); 1503 let device_id = extract_device_cookie(&headers); 1504 if db::update_authorization_request( 1505 &state.db, 1506 &form.request_uri, 1507 &did, 1508 device_id.as_deref(), 1509 &code.0, 1510 ) 1511 .await 1512 .is_err() 1513 { 1514 return json_error( 1515 StatusCode::INTERNAL_SERVER_ERROR, 1516 "server_error", 1517 "An error occurred. Please try again.", 1518 ); 1519 } 1520 let redirect_url = build_success_redirect( 1521 &request_data.parameters.redirect_uri, 1522 &code.0, 1523 request_data.parameters.state.as_deref(), 1524 request_data.parameters.response_mode.as_deref(), 1525 ); 1526 Json(serde_json::json!({ 1527 "redirect_uri": redirect_url 1528 })) 1529 .into_response() 1530} 1531 1532#[derive(Debug, Deserialize)] 1533#[serde(rename_all = "camelCase")] 1534pub struct CheckPasskeysQuery { 1535 pub identifier: String, 1536} 1537 1538#[derive(Debug, Serialize)] 1539#[serde(rename_all = "camelCase")] 1540pub struct CheckPasskeysResponse { 1541 pub has_passkeys: bool, 1542} 1543 1544pub async fn check_user_has_passkeys( 1545 State(state): State<AppState>, 1546 Query(query): Query<CheckPasskeysQuery>, 1547) -> Response { 1548 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1549 let normalized_identifier = query.identifier.trim(); 1550 let normalized_identifier = normalized_identifier 1551 .strip_prefix('@') 1552 .unwrap_or(normalized_identifier); 1553 let normalized_identifier = if let Some(bare_handle) = 1554 normalized_identifier.strip_suffix(&format!(".{}", pds_hostname)) 1555 { 1556 bare_handle.to_string() 1557 } else { 1558 normalized_identifier.to_string() 1559 }; 1560 1561 let user = sqlx::query!( 1562 "SELECT did FROM users WHERE handle = $1 OR email = $1", 1563 normalized_identifier 1564 ) 1565 .fetch_optional(&state.db) 1566 .await; 1567 1568 let has_passkeys = match user { 1569 Ok(Some(u)) => crate::api::server::has_passkeys_for_user(&state, &u.did).await, 1570 _ => false, 1571 }; 1572 1573 Json(CheckPasskeysResponse { has_passkeys }).into_response() 1574} 1575 1576#[derive(Debug, Serialize)] 1577#[serde(rename_all = "camelCase")] 1578pub struct SecurityStatusResponse { 1579 pub has_passkeys: bool, 1580 pub has_totp: bool, 1581} 1582 1583pub async fn check_user_security_status( 1584 State(state): State<AppState>, 1585 Query(query): Query<CheckPasskeysQuery>, 1586) -> Response { 1587 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1588 let normalized_identifier = query.identifier.trim(); 1589 let normalized_identifier = normalized_identifier 1590 .strip_prefix('@') 1591 .unwrap_or(normalized_identifier); 1592 let normalized_identifier = if let Some(bare_handle) = 1593 normalized_identifier.strip_suffix(&format!(".{}", pds_hostname)) 1594 { 1595 bare_handle.to_string() 1596 } else { 1597 normalized_identifier.to_string() 1598 }; 1599 1600 let user = sqlx::query!( 1601 "SELECT did FROM users WHERE handle = $1 OR email = $1", 1602 normalized_identifier 1603 ) 1604 .fetch_optional(&state.db) 1605 .await; 1606 1607 let (has_passkeys, has_totp) = match user { 1608 Ok(Some(u)) => { 1609 let passkeys = crate::api::server::has_passkeys_for_user(&state, &u.did).await; 1610 let totp = crate::api::server::has_totp_enabled(&state, &u.did).await; 1611 (passkeys, totp) 1612 } 1613 _ => (false, false), 1614 }; 1615 1616 Json(SecurityStatusResponse { 1617 has_passkeys, 1618 has_totp, 1619 }) 1620 .into_response() 1621} 1622 1623#[derive(Debug, Deserialize)] 1624pub struct PasskeyStartInput { 1625 pub request_uri: String, 1626 pub identifier: String, 1627} 1628 1629#[derive(Debug, Serialize)] 1630#[serde(rename_all = "camelCase")] 1631pub struct PasskeyStartResponse { 1632 pub options: serde_json::Value, 1633} 1634 1635pub async fn passkey_start( 1636 State(state): State<AppState>, 1637 headers: HeaderMap, 1638 Json(form): Json<PasskeyStartInput>, 1639) -> Response { 1640 let client_ip = extract_client_ip(&headers); 1641 1642 if !state 1643 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip) 1644 .await 1645 { 1646 tracing::warn!(ip = %client_ip, "OAuth passkey rate limit exceeded"); 1647 return ( 1648 StatusCode::TOO_MANY_REQUESTS, 1649 Json(serde_json::json!({ 1650 "error": "RateLimitExceeded", 1651 "error_description": "Too many login attempts. Please try again later." 1652 })), 1653 ) 1654 .into_response(); 1655 } 1656 1657 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 1658 Ok(Some(data)) => data, 1659 Ok(None) => { 1660 return ( 1661 StatusCode::BAD_REQUEST, 1662 Json(serde_json::json!({ 1663 "error": "invalid_request", 1664 "error_description": "Invalid or expired request_uri." 1665 })), 1666 ) 1667 .into_response(); 1668 } 1669 Err(_) => { 1670 return ( 1671 StatusCode::INTERNAL_SERVER_ERROR, 1672 Json(serde_json::json!({ 1673 "error": "server_error", 1674 "error_description": "An error occurred." 1675 })), 1676 ) 1677 .into_response(); 1678 } 1679 }; 1680 1681 if request_data.expires_at < Utc::now() { 1682 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1683 return ( 1684 StatusCode::BAD_REQUEST, 1685 Json(serde_json::json!({ 1686 "error": "invalid_request", 1687 "error_description": "Authorization request has expired." 1688 })), 1689 ) 1690 .into_response(); 1691 } 1692 1693 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1694 let normalized_username = form.identifier.trim(); 1695 let normalized_username = normalized_username 1696 .strip_prefix('@') 1697 .unwrap_or(normalized_username); 1698 let normalized_username = if let Some(bare_handle) = 1699 normalized_username.strip_suffix(&format!(".{}", pds_hostname)) 1700 { 1701 bare_handle.to_string() 1702 } else { 1703 normalized_username.to_string() 1704 }; 1705 1706 let user = match sqlx::query!( 1707 r#" 1708 SELECT did, deactivated_at, takedown_ref, 1709 email_verified, discord_verified, telegram_verified, signal_verified 1710 FROM users 1711 WHERE handle = $1 OR email = $1 1712 "#, 1713 normalized_username 1714 ) 1715 .fetch_optional(&state.db) 1716 .await 1717 { 1718 Ok(Some(u)) => u, 1719 Ok(None) => { 1720 return ( 1721 StatusCode::FORBIDDEN, 1722 Json(serde_json::json!({ 1723 "error": "access_denied", 1724 "error_description": "User not found or has no passkeys." 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 user.deactivated_at.is_some() { 1742 return ( 1743 StatusCode::FORBIDDEN, 1744 Json(serde_json::json!({ 1745 "error": "access_denied", 1746 "error_description": "This account has been deactivated." 1747 })), 1748 ) 1749 .into_response(); 1750 } 1751 1752 if user.takedown_ref.is_some() { 1753 return ( 1754 StatusCode::FORBIDDEN, 1755 Json(serde_json::json!({ 1756 "error": "access_denied", 1757 "error_description": "This account has been taken down." 1758 })), 1759 ) 1760 .into_response(); 1761 } 1762 1763 let is_verified = user.email_verified 1764 || user.discord_verified 1765 || user.telegram_verified 1766 || user.signal_verified; 1767 1768 if !is_verified { 1769 return ( 1770 StatusCode::FORBIDDEN, 1771 Json(serde_json::json!({ 1772 "error": "access_denied", 1773 "error_description": "Please verify your account before logging in." 1774 })), 1775 ) 1776 .into_response(); 1777 } 1778 1779 let stored_passkeys = 1780 match crate::auth::webauthn::get_passkeys_for_user(&state.db, &user.did).await { 1781 Ok(pks) => pks, 1782 Err(e) => { 1783 tracing::error!(error = %e, "Failed to get passkeys"); 1784 return ( 1785 StatusCode::INTERNAL_SERVER_ERROR, 1786 Json(serde_json::json!({ 1787 "error": "server_error", 1788 "error_description": "An error occurred." 1789 })), 1790 ) 1791 .into_response(); 1792 } 1793 }; 1794 1795 if stored_passkeys.is_empty() { 1796 return ( 1797 StatusCode::FORBIDDEN, 1798 Json(serde_json::json!({ 1799 "error": "access_denied", 1800 "error_description": "User not found or has no passkeys." 1801 })), 1802 ) 1803 .into_response(); 1804 } 1805 1806 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys 1807 .iter() 1808 .filter_map(|sp| sp.to_security_key().ok()) 1809 .collect(); 1810 1811 if passkeys.is_empty() { 1812 return ( 1813 StatusCode::INTERNAL_SERVER_ERROR, 1814 Json(serde_json::json!({ 1815 "error": "server_error", 1816 "error_description": "Failed to load passkeys." 1817 })), 1818 ) 1819 .into_response(); 1820 } 1821 1822 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 1823 Ok(w) => w, 1824 Err(e) => { 1825 tracing::error!(error = %e, "Failed to create WebAuthn config"); 1826 return ( 1827 StatusCode::INTERNAL_SERVER_ERROR, 1828 Json(serde_json::json!({ 1829 "error": "server_error", 1830 "error_description": "WebAuthn configuration failed." 1831 })), 1832 ) 1833 .into_response(); 1834 } 1835 }; 1836 1837 let (rcr, auth_state) = match webauthn.start_authentication(passkeys) { 1838 Ok(result) => result, 1839 Err(e) => { 1840 tracing::error!(error = %e, "Failed to start passkey authentication"); 1841 return ( 1842 StatusCode::INTERNAL_SERVER_ERROR, 1843 Json(serde_json::json!({ 1844 "error": "server_error", 1845 "error_description": "Failed to start authentication." 1846 })), 1847 ) 1848 .into_response(); 1849 } 1850 }; 1851 1852 if let Err(e) = 1853 crate::auth::webauthn::save_authentication_state(&state.db, &user.did, &auth_state).await 1854 { 1855 tracing::error!(error = %e, "Failed to save authentication state"); 1856 return ( 1857 StatusCode::INTERNAL_SERVER_ERROR, 1858 Json(serde_json::json!({ 1859 "error": "server_error", 1860 "error_description": "An error occurred." 1861 })), 1862 ) 1863 .into_response(); 1864 } 1865 1866 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None) 1867 .await 1868 .is_err() 1869 { 1870 return ( 1871 StatusCode::INTERNAL_SERVER_ERROR, 1872 Json(serde_json::json!({ 1873 "error": "server_error", 1874 "error_description": "An error occurred." 1875 })), 1876 ) 1877 .into_response(); 1878 } 1879 1880 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 1881 1882 Json(PasskeyStartResponse { options }).into_response() 1883} 1884 1885#[derive(Debug, Deserialize)] 1886pub struct PasskeyFinishInput { 1887 pub request_uri: String, 1888 pub credential: serde_json::Value, 1889} 1890 1891pub async fn passkey_finish( 1892 State(state): State<AppState>, 1893 headers: HeaderMap, 1894 Json(form): Json<PasskeyFinishInput>, 1895) -> Response { 1896 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 1897 Ok(Some(data)) => data, 1898 Ok(None) => { 1899 return ( 1900 StatusCode::BAD_REQUEST, 1901 Json(serde_json::json!({ 1902 "error": "invalid_request", 1903 "error_description": "Invalid or expired request_uri." 1904 })), 1905 ) 1906 .into_response(); 1907 } 1908 Err(_) => { 1909 return ( 1910 StatusCode::INTERNAL_SERVER_ERROR, 1911 Json(serde_json::json!({ 1912 "error": "server_error", 1913 "error_description": "An error occurred." 1914 })), 1915 ) 1916 .into_response(); 1917 } 1918 }; 1919 1920 if request_data.expires_at < Utc::now() { 1921 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1922 return ( 1923 StatusCode::BAD_REQUEST, 1924 Json(serde_json::json!({ 1925 "error": "invalid_request", 1926 "error_description": "Authorization request has expired." 1927 })), 1928 ) 1929 .into_response(); 1930 } 1931 1932 let did = match request_data.did { 1933 Some(d) => d, 1934 None => { 1935 return ( 1936 StatusCode::BAD_REQUEST, 1937 Json(serde_json::json!({ 1938 "error": "invalid_request", 1939 "error_description": "No passkey authentication in progress." 1940 })), 1941 ) 1942 .into_response(); 1943 } 1944 }; 1945 1946 let auth_state = match crate::auth::webauthn::load_authentication_state(&state.db, &did).await { 1947 Ok(Some(s)) => s, 1948 Ok(None) => { 1949 return ( 1950 StatusCode::BAD_REQUEST, 1951 Json(serde_json::json!({ 1952 "error": "invalid_request", 1953 "error_description": "No passkey authentication in progress or challenge expired." 1954 })), 1955 ) 1956 .into_response(); 1957 } 1958 Err(e) => { 1959 tracing::error!(error = %e, "Failed to load authentication state"); 1960 return ( 1961 StatusCode::INTERNAL_SERVER_ERROR, 1962 Json(serde_json::json!({ 1963 "error": "server_error", 1964 "error_description": "An error occurred." 1965 })), 1966 ) 1967 .into_response(); 1968 } 1969 }; 1970 1971 let credential: webauthn_rs::prelude::PublicKeyCredential = 1972 match serde_json::from_value(form.credential) { 1973 Ok(c) => c, 1974 Err(e) => { 1975 tracing::warn!(error = %e, "Failed to parse credential"); 1976 return ( 1977 StatusCode::BAD_REQUEST, 1978 Json(serde_json::json!({ 1979 "error": "invalid_request", 1980 "error_description": "Failed to parse credential response." 1981 })), 1982 ) 1983 .into_response(); 1984 } 1985 }; 1986 1987 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1988 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 1989 Ok(w) => w, 1990 Err(e) => { 1991 tracing::error!(error = %e, "Failed to create WebAuthn config"); 1992 return ( 1993 StatusCode::INTERNAL_SERVER_ERROR, 1994 Json(serde_json::json!({ 1995 "error": "server_error", 1996 "error_description": "WebAuthn configuration failed." 1997 })), 1998 ) 1999 .into_response(); 2000 } 2001 }; 2002 2003 let auth_result = match webauthn.finish_authentication(&credential, &auth_state) { 2004 Ok(r) => r, 2005 Err(e) => { 2006 tracing::warn!(error = %e, did = %did, "Failed to verify passkey authentication"); 2007 return ( 2008 StatusCode::FORBIDDEN, 2009 Json(serde_json::json!({ 2010 "error": "access_denied", 2011 "error_description": "Passkey verification failed." 2012 })), 2013 ) 2014 .into_response(); 2015 } 2016 }; 2017 2018 if let Err(e) = crate::auth::webauthn::delete_authentication_state(&state.db, &did).await { 2019 tracing::warn!(error = %e, "Failed to delete authentication state"); 2020 } 2021 2022 if auth_result.needs_update() 2023 && let Err(e) = crate::auth::webauthn::update_passkey_counter( 2024 &state.db, 2025 auth_result.cred_id(), 2026 auth_result.counter(), 2027 ) 2028 .await 2029 { 2030 tracing::warn!(error = %e, "Failed to update passkey counter"); 2031 } 2032 2033 tracing::info!(did = %did, "Passkey authentication successful"); 2034 2035 let has_totp = crate::api::server::has_totp_enabled(&state, &did).await; 2036 if has_totp { 2037 return Json(serde_json::json!({ 2038 "needs_totp": true 2039 })) 2040 .into_response(); 2041 } 2042 2043 let user = sqlx::query!( 2044 "SELECT two_factor_enabled, preferred_comms_channel as \"preferred_comms_channel: CommsChannel\", id FROM users WHERE did = $1", 2045 did 2046 ) 2047 .fetch_optional(&state.db) 2048 .await; 2049 2050 if let Ok(Some(user)) = user 2051 && user.two_factor_enabled 2052 { 2053 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 2054 match db::create_2fa_challenge(&state.db, &did, &form.request_uri).await { 2055 Ok(challenge) => { 2056 let hostname = 2057 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2058 if let Err(e) = 2059 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await 2060 { 2061 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification"); 2062 } 2063 let channel_name = channel_display_name(user.preferred_comms_channel); 2064 return Json(serde_json::json!({ 2065 "needs_2fa": true, 2066 "channel": channel_name 2067 })) 2068 .into_response(); 2069 } 2070 Err(_) => { 2071 return ( 2072 StatusCode::INTERNAL_SERVER_ERROR, 2073 Json(serde_json::json!({ 2074 "error": "server_error", 2075 "error_description": "An error occurred." 2076 })), 2077 ) 2078 .into_response(); 2079 } 2080 } 2081 } 2082 2083 let device_id = extract_device_cookie(&headers); 2084 let requested_scope_str = request_data 2085 .parameters 2086 .scope 2087 .as_deref() 2088 .unwrap_or("atproto"); 2089 let requested_scopes: Vec<String> = requested_scope_str 2090 .split_whitespace() 2091 .map(|s| s.to_string()) 2092 .collect(); 2093 2094 let needs_consent = db::should_show_consent( 2095 &state.db, 2096 &did, 2097 &request_data.parameters.client_id, 2098 &requested_scopes, 2099 ) 2100 .await 2101 .unwrap_or(true); 2102 2103 if needs_consent { 2104 let consent_url = format!( 2105 "/#/oauth/consent?request_uri={}", 2106 url_encode(&form.request_uri) 2107 ); 2108 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 2109 } 2110 2111 let code = Code::generate(); 2112 if db::update_authorization_request( 2113 &state.db, 2114 &form.request_uri, 2115 &did, 2116 device_id.as_deref(), 2117 &code.0, 2118 ) 2119 .await 2120 .is_err() 2121 { 2122 return ( 2123 StatusCode::INTERNAL_SERVER_ERROR, 2124 Json(serde_json::json!({ 2125 "error": "server_error", 2126 "error_description": "An error occurred." 2127 })), 2128 ) 2129 .into_response(); 2130 } 2131 2132 let redirect_url = build_success_redirect( 2133 &request_data.parameters.redirect_uri, 2134 &code.0, 2135 request_data.parameters.state.as_deref(), 2136 request_data.parameters.response_mode.as_deref(), 2137 ); 2138 2139 Json(serde_json::json!({ 2140 "redirect_uri": redirect_url 2141 })) 2142 .into_response() 2143}