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