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 "/app/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 "/app/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 "/app/oauth/accounts?request_uri={}", 263 url_encode(&request_uri) 264 )); 265 } 266 redirect_see_other(&format!( 267 "/app/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 "/app/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 "/app/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 "/app/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 "/app/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 "/app/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 "/app/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 "/app/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("identity:") 1468 || s.starts_with("include:") 1469 }); 1470 if !has_valid_scope { 1471 return ( 1472 StatusCode::BAD_REQUEST, 1473 Json(serde_json::json!({ 1474 "error": "invalid_request", 1475 "error_description": "Invalid scope format" 1476 })), 1477 ) 1478 .into_response(); 1479 } 1480 if form.remember { 1481 let preferences: Vec<db::ScopePreference> = requested_scopes 1482 .iter() 1483 .map(|s| db::ScopePreference { 1484 scope: s.to_string(), 1485 granted: form.approved_scopes.contains(&s.to_string()), 1486 }) 1487 .collect(); 1488 let _ = db::upsert_scope_preferences( 1489 &state.db, 1490 &did, 1491 &request_data.parameters.client_id, 1492 &preferences, 1493 ) 1494 .await; 1495 } 1496 if let Err(e) = 1497 db::update_request_scope(&state.db, &form.request_uri, &approved_scope_str).await 1498 { 1499 tracing::warn!("Failed to update request scope: {:?}", e); 1500 } 1501 let code = Code::generate(); 1502 if db::update_authorization_request( 1503 &state.db, 1504 &form.request_uri, 1505 &did, 1506 request_data.device_id.as_deref(), 1507 &code.0, 1508 ) 1509 .await 1510 .is_err() 1511 { 1512 return ( 1513 StatusCode::INTERNAL_SERVER_ERROR, 1514 Json(serde_json::json!({ 1515 "error": "server_error", 1516 "error_description": "Failed to complete authorization" 1517 })), 1518 ) 1519 .into_response(); 1520 } 1521 let redirect_url = build_success_redirect( 1522 &request_data.parameters.redirect_uri, 1523 &code.0, 1524 request_data.parameters.state.as_deref(), 1525 request_data.parameters.response_mode.as_deref(), 1526 ); 1527 Json(serde_json::json!({ 1528 "redirect_uri": redirect_url 1529 })) 1530 .into_response() 1531} 1532 1533pub async fn authorize_2fa_post( 1534 State(state): State<AppState>, 1535 headers: HeaderMap, 1536 Json(form): Json<Authorize2faSubmit>, 1537) -> Response { 1538 let json_error = |status: StatusCode, error: &str, description: &str| -> Response { 1539 ( 1540 status, 1541 Json(serde_json::json!({ 1542 "error": error, 1543 "error_description": description 1544 })), 1545 ) 1546 .into_response() 1547 }; 1548 let client_ip = extract_client_ip(&headers); 1549 if !state 1550 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip) 1551 .await 1552 { 1553 tracing::warn!(ip = %client_ip, "OAuth 2FA rate limit exceeded"); 1554 return json_error( 1555 StatusCode::TOO_MANY_REQUESTS, 1556 "RateLimitExceeded", 1557 "Too many attempts. Please try again later.", 1558 ); 1559 } 1560 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 1561 Ok(Some(d)) => d, 1562 Ok(None) => { 1563 return json_error( 1564 StatusCode::BAD_REQUEST, 1565 "invalid_request", 1566 "Authorization request not found.", 1567 ); 1568 } 1569 Err(_) => { 1570 return json_error( 1571 StatusCode::INTERNAL_SERVER_ERROR, 1572 "server_error", 1573 "An error occurred.", 1574 ); 1575 } 1576 }; 1577 if request_data.expires_at < Utc::now() { 1578 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1579 return json_error( 1580 StatusCode::BAD_REQUEST, 1581 "invalid_request", 1582 "Authorization request has expired.", 1583 ); 1584 } 1585 let challenge = db::get_2fa_challenge(&state.db, &form.request_uri) 1586 .await 1587 .ok() 1588 .flatten(); 1589 if let Some(challenge) = challenge { 1590 if challenge.expires_at < Utc::now() { 1591 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 1592 return json_error( 1593 StatusCode::BAD_REQUEST, 1594 "invalid_request", 1595 "2FA code has expired. Please start over.", 1596 ); 1597 } 1598 if challenge.attempts >= MAX_2FA_ATTEMPTS { 1599 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 1600 return json_error( 1601 StatusCode::FORBIDDEN, 1602 "access_denied", 1603 "Too many failed attempts. Please start over.", 1604 ); 1605 } 1606 let code_valid: bool = form 1607 .code 1608 .trim() 1609 .as_bytes() 1610 .ct_eq(challenge.code.as_bytes()) 1611 .into(); 1612 if !code_valid { 1613 let _ = db::increment_2fa_attempts(&state.db, challenge.id).await; 1614 return json_error( 1615 StatusCode::FORBIDDEN, 1616 "invalid_code", 1617 "Invalid verification code. Please try again.", 1618 ); 1619 } 1620 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 1621 let code = Code::generate(); 1622 let device_id = extract_device_cookie(&headers); 1623 if db::update_authorization_request( 1624 &state.db, 1625 &form.request_uri, 1626 &challenge.did, 1627 device_id.as_deref(), 1628 &code.0, 1629 ) 1630 .await 1631 .is_err() 1632 { 1633 return json_error( 1634 StatusCode::INTERNAL_SERVER_ERROR, 1635 "server_error", 1636 "An error occurred. Please try again.", 1637 ); 1638 } 1639 let redirect_url = build_success_redirect( 1640 &request_data.parameters.redirect_uri, 1641 &code.0, 1642 request_data.parameters.state.as_deref(), 1643 request_data.parameters.response_mode.as_deref(), 1644 ); 1645 return Json(serde_json::json!({ 1646 "redirect_uri": redirect_url 1647 })) 1648 .into_response(); 1649 } 1650 let did = match &request_data.did { 1651 Some(d) => d.clone(), 1652 None => { 1653 return json_error( 1654 StatusCode::BAD_REQUEST, 1655 "invalid_request", 1656 "No 2FA challenge found. Please start over.", 1657 ); 1658 } 1659 }; 1660 if !crate::api::server::has_totp_enabled(&state, &did).await { 1661 return json_error( 1662 StatusCode::BAD_REQUEST, 1663 "invalid_request", 1664 "No 2FA challenge found. Please start over.", 1665 ); 1666 } 1667 if !state 1668 .check_rate_limit(RateLimitKind::TotpVerify, &did) 1669 .await 1670 { 1671 tracing::warn!(did = %did, "TOTP verification rate limit exceeded"); 1672 return json_error( 1673 StatusCode::TOO_MANY_REQUESTS, 1674 "RateLimitExceeded", 1675 "Too many verification attempts. Please try again in a few minutes.", 1676 ); 1677 } 1678 let totp_valid = 1679 crate::api::server::verify_totp_or_backup_for_user(&state, &did, &form.code).await; 1680 if !totp_valid { 1681 return json_error( 1682 StatusCode::FORBIDDEN, 1683 "invalid_code", 1684 "Invalid verification code. Please try again.", 1685 ); 1686 } 1687 let device_id = extract_device_cookie(&headers); 1688 if form.trust_device 1689 && let Some(ref dev_id) = device_id 1690 { 1691 let _ = crate::api::server::trust_device(&state.db, dev_id).await; 1692 } 1693 let requested_scope_str = request_data 1694 .parameters 1695 .scope 1696 .as_deref() 1697 .unwrap_or("atproto"); 1698 let requested_scopes: Vec<String> = requested_scope_str 1699 .split_whitespace() 1700 .map(|s| s.to_string()) 1701 .collect(); 1702 let needs_consent = db::should_show_consent( 1703 &state.db, 1704 &did, 1705 &request_data.parameters.client_id, 1706 &requested_scopes, 1707 ) 1708 .await 1709 .unwrap_or(true); 1710 if needs_consent { 1711 let consent_url = format!( 1712 "/app/oauth/consent?request_uri={}", 1713 url_encode(&form.request_uri) 1714 ); 1715 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 1716 } 1717 let code = Code::generate(); 1718 if db::update_authorization_request( 1719 &state.db, 1720 &form.request_uri, 1721 &did, 1722 device_id.as_deref(), 1723 &code.0, 1724 ) 1725 .await 1726 .is_err() 1727 { 1728 return json_error( 1729 StatusCode::INTERNAL_SERVER_ERROR, 1730 "server_error", 1731 "An error occurred. Please try again.", 1732 ); 1733 } 1734 let redirect_url = build_success_redirect( 1735 &request_data.parameters.redirect_uri, 1736 &code.0, 1737 request_data.parameters.state.as_deref(), 1738 request_data.parameters.response_mode.as_deref(), 1739 ); 1740 Json(serde_json::json!({ 1741 "redirect_uri": redirect_url 1742 })) 1743 .into_response() 1744} 1745 1746#[derive(Debug, Deserialize)] 1747#[serde(rename_all = "camelCase")] 1748pub struct CheckPasskeysQuery { 1749 pub identifier: String, 1750} 1751 1752#[derive(Debug, Serialize)] 1753#[serde(rename_all = "camelCase")] 1754pub struct CheckPasskeysResponse { 1755 pub has_passkeys: bool, 1756} 1757 1758pub async fn check_user_has_passkeys( 1759 State(state): State<AppState>, 1760 Query(query): Query<CheckPasskeysQuery>, 1761) -> Response { 1762 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1763 let normalized_identifier = query.identifier.trim(); 1764 let normalized_identifier = normalized_identifier 1765 .strip_prefix('@') 1766 .unwrap_or(normalized_identifier); 1767 let normalized_identifier = if let Some(bare_handle) = 1768 normalized_identifier.strip_suffix(&format!(".{}", pds_hostname)) 1769 { 1770 bare_handle.to_string() 1771 } else { 1772 normalized_identifier.to_string() 1773 }; 1774 1775 let user = sqlx::query!( 1776 "SELECT did FROM users WHERE handle = $1 OR email = $1", 1777 normalized_identifier 1778 ) 1779 .fetch_optional(&state.db) 1780 .await; 1781 1782 let has_passkeys = match user { 1783 Ok(Some(u)) => crate::api::server::has_passkeys_for_user(&state, &u.did).await, 1784 _ => false, 1785 }; 1786 1787 Json(CheckPasskeysResponse { has_passkeys }).into_response() 1788} 1789 1790#[derive(Debug, Serialize)] 1791#[serde(rename_all = "camelCase")] 1792pub struct SecurityStatusResponse { 1793 pub has_passkeys: bool, 1794 pub has_totp: bool, 1795 pub has_password: bool, 1796 pub is_delegated: bool, 1797 #[serde(skip_serializing_if = "Option::is_none")] 1798 pub did: Option<String>, 1799} 1800 1801pub async fn check_user_security_status( 1802 State(state): State<AppState>, 1803 Query(query): Query<CheckPasskeysQuery>, 1804) -> Response { 1805 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1806 let identifier = query.identifier.trim(); 1807 let identifier = identifier.strip_prefix('@').unwrap_or(identifier); 1808 let normalized_identifier = if identifier.contains('@') || identifier.starts_with("did:") { 1809 identifier.to_string() 1810 } else if !identifier.contains('.') { 1811 format!("{}.{}", identifier.to_lowercase(), pds_hostname) 1812 } else { 1813 identifier.to_lowercase() 1814 }; 1815 1816 let user = sqlx::query!( 1817 "SELECT did, password_hash FROM users WHERE handle = $1 OR email = $1", 1818 normalized_identifier 1819 ) 1820 .fetch_optional(&state.db) 1821 .await; 1822 1823 let (has_passkeys, has_totp, has_password, is_delegated, did): ( 1824 bool, 1825 bool, 1826 bool, 1827 bool, 1828 Option<String>, 1829 ) = match user { 1830 Ok(Some(u)) => { 1831 let passkeys = crate::api::server::has_passkeys_for_user(&state, &u.did).await; 1832 let totp = crate::api::server::has_totp_enabled(&state, &u.did).await; 1833 let has_pw = u.password_hash.is_some(); 1834 let has_controllers = crate::delegation::is_delegated_account(&state.db, &u.did) 1835 .await 1836 .unwrap_or(false); 1837 (passkeys, totp, has_pw, has_controllers, Some(u.did)) 1838 } 1839 _ => (false, false, false, false, None), 1840 }; 1841 1842 Json(SecurityStatusResponse { 1843 has_passkeys, 1844 has_totp, 1845 has_password, 1846 is_delegated, 1847 did, 1848 }) 1849 .into_response() 1850} 1851 1852#[derive(Debug, Deserialize)] 1853pub struct PasskeyStartInput { 1854 pub request_uri: String, 1855 pub identifier: String, 1856} 1857 1858#[derive(Debug, Serialize)] 1859#[serde(rename_all = "camelCase")] 1860pub struct PasskeyStartResponse { 1861 pub options: serde_json::Value, 1862} 1863 1864pub async fn passkey_start( 1865 State(state): State<AppState>, 1866 headers: HeaderMap, 1867 Json(form): Json<PasskeyStartInput>, 1868) -> Response { 1869 let client_ip = extract_client_ip(&headers); 1870 1871 if !state 1872 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip) 1873 .await 1874 { 1875 tracing::warn!(ip = %client_ip, "OAuth passkey rate limit exceeded"); 1876 return ( 1877 StatusCode::TOO_MANY_REQUESTS, 1878 Json(serde_json::json!({ 1879 "error": "RateLimitExceeded", 1880 "error_description": "Too many login attempts. Please try again later." 1881 })), 1882 ) 1883 .into_response(); 1884 } 1885 1886 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 1887 Ok(Some(data)) => data, 1888 Ok(None) => { 1889 return ( 1890 StatusCode::BAD_REQUEST, 1891 Json(serde_json::json!({ 1892 "error": "invalid_request", 1893 "error_description": "Invalid or expired request_uri." 1894 })), 1895 ) 1896 .into_response(); 1897 } 1898 Err(_) => { 1899 return ( 1900 StatusCode::INTERNAL_SERVER_ERROR, 1901 Json(serde_json::json!({ 1902 "error": "server_error", 1903 "error_description": "An error occurred." 1904 })), 1905 ) 1906 .into_response(); 1907 } 1908 }; 1909 1910 if request_data.expires_at < Utc::now() { 1911 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1912 return ( 1913 StatusCode::BAD_REQUEST, 1914 Json(serde_json::json!({ 1915 "error": "invalid_request", 1916 "error_description": "Authorization request has expired." 1917 })), 1918 ) 1919 .into_response(); 1920 } 1921 1922 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1923 let normalized_username = form.identifier.trim(); 1924 let normalized_username = normalized_username 1925 .strip_prefix('@') 1926 .unwrap_or(normalized_username); 1927 let normalized_username = if normalized_username.contains('@') { 1928 normalized_username.to_string() 1929 } else if !normalized_username.contains('.') { 1930 format!("{}.{}", normalized_username, pds_hostname) 1931 } else { 1932 normalized_username.to_string() 1933 }; 1934 1935 let user = match sqlx::query!( 1936 r#" 1937 SELECT did, deactivated_at, takedown_ref, 1938 email_verified, discord_verified, telegram_verified, signal_verified 1939 FROM users 1940 WHERE handle = $1 OR email = $1 1941 "#, 1942 normalized_username 1943 ) 1944 .fetch_optional(&state.db) 1945 .await 1946 { 1947 Ok(Some(u)) => u, 1948 Ok(None) => { 1949 return ( 1950 StatusCode::FORBIDDEN, 1951 Json(serde_json::json!({ 1952 "error": "access_denied", 1953 "error_description": "User not found or has no passkeys." 1954 })), 1955 ) 1956 .into_response(); 1957 } 1958 Err(_) => { 1959 return ( 1960 StatusCode::INTERNAL_SERVER_ERROR, 1961 Json(serde_json::json!({ 1962 "error": "server_error", 1963 "error_description": "An error occurred." 1964 })), 1965 ) 1966 .into_response(); 1967 } 1968 }; 1969 1970 if user.deactivated_at.is_some() { 1971 return ( 1972 StatusCode::FORBIDDEN, 1973 Json(serde_json::json!({ 1974 "error": "access_denied", 1975 "error_description": "This account has been deactivated." 1976 })), 1977 ) 1978 .into_response(); 1979 } 1980 1981 if user.takedown_ref.is_some() { 1982 return ( 1983 StatusCode::FORBIDDEN, 1984 Json(serde_json::json!({ 1985 "error": "access_denied", 1986 "error_description": "This account has been taken down." 1987 })), 1988 ) 1989 .into_response(); 1990 } 1991 1992 let is_verified = user.email_verified 1993 || user.discord_verified 1994 || user.telegram_verified 1995 || user.signal_verified; 1996 1997 if !is_verified { 1998 return ( 1999 StatusCode::FORBIDDEN, 2000 Json(serde_json::json!({ 2001 "error": "access_denied", 2002 "error_description": "Please verify your account before logging in." 2003 })), 2004 ) 2005 .into_response(); 2006 } 2007 2008 let stored_passkeys = 2009 match crate::auth::webauthn::get_passkeys_for_user(&state.db, &user.did).await { 2010 Ok(pks) => pks, 2011 Err(e) => { 2012 tracing::error!(error = %e, "Failed to get passkeys"); 2013 return ( 2014 StatusCode::INTERNAL_SERVER_ERROR, 2015 Json(serde_json::json!({ 2016 "error": "server_error", 2017 "error_description": "An error occurred." 2018 })), 2019 ) 2020 .into_response(); 2021 } 2022 }; 2023 2024 if stored_passkeys.is_empty() { 2025 return ( 2026 StatusCode::FORBIDDEN, 2027 Json(serde_json::json!({ 2028 "error": "access_denied", 2029 "error_description": "User not found or has no passkeys." 2030 })), 2031 ) 2032 .into_response(); 2033 } 2034 2035 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys 2036 .iter() 2037 .filter_map(|sp| sp.to_security_key().ok()) 2038 .collect(); 2039 2040 if passkeys.is_empty() { 2041 return ( 2042 StatusCode::INTERNAL_SERVER_ERROR, 2043 Json(serde_json::json!({ 2044 "error": "server_error", 2045 "error_description": "Failed to load passkeys." 2046 })), 2047 ) 2048 .into_response(); 2049 } 2050 2051 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 2052 Ok(w) => w, 2053 Err(e) => { 2054 tracing::error!(error = %e, "Failed to create WebAuthn config"); 2055 return ( 2056 StatusCode::INTERNAL_SERVER_ERROR, 2057 Json(serde_json::json!({ 2058 "error": "server_error", 2059 "error_description": "WebAuthn configuration failed." 2060 })), 2061 ) 2062 .into_response(); 2063 } 2064 }; 2065 2066 let (rcr, auth_state) = match webauthn.start_authentication(passkeys) { 2067 Ok(result) => result, 2068 Err(e) => { 2069 tracing::error!(error = %e, "Failed to start passkey authentication"); 2070 return ( 2071 StatusCode::INTERNAL_SERVER_ERROR, 2072 Json(serde_json::json!({ 2073 "error": "server_error", 2074 "error_description": "Failed to start authentication." 2075 })), 2076 ) 2077 .into_response(); 2078 } 2079 }; 2080 2081 if let Err(e) = 2082 crate::auth::webauthn::save_authentication_state(&state.db, &user.did, &auth_state).await 2083 { 2084 tracing::error!(error = %e, "Failed to save authentication state"); 2085 return ( 2086 StatusCode::INTERNAL_SERVER_ERROR, 2087 Json(serde_json::json!({ 2088 "error": "server_error", 2089 "error_description": "An error occurred." 2090 })), 2091 ) 2092 .into_response(); 2093 } 2094 2095 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None) 2096 .await 2097 .is_err() 2098 { 2099 return ( 2100 StatusCode::INTERNAL_SERVER_ERROR, 2101 Json(serde_json::json!({ 2102 "error": "server_error", 2103 "error_description": "An error occurred." 2104 })), 2105 ) 2106 .into_response(); 2107 } 2108 2109 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 2110 2111 Json(PasskeyStartResponse { options }).into_response() 2112} 2113 2114#[derive(Debug, Deserialize)] 2115pub struct PasskeyFinishInput { 2116 pub request_uri: String, 2117 pub credential: serde_json::Value, 2118} 2119 2120pub async fn passkey_finish( 2121 State(state): State<AppState>, 2122 headers: HeaderMap, 2123 Json(form): Json<PasskeyFinishInput>, 2124) -> Response { 2125 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 2126 Ok(Some(data)) => data, 2127 Ok(None) => { 2128 return ( 2129 StatusCode::BAD_REQUEST, 2130 Json(serde_json::json!({ 2131 "error": "invalid_request", 2132 "error_description": "Invalid or expired request_uri." 2133 })), 2134 ) 2135 .into_response(); 2136 } 2137 Err(_) => { 2138 return ( 2139 StatusCode::INTERNAL_SERVER_ERROR, 2140 Json(serde_json::json!({ 2141 "error": "server_error", 2142 "error_description": "An error occurred." 2143 })), 2144 ) 2145 .into_response(); 2146 } 2147 }; 2148 2149 if request_data.expires_at < Utc::now() { 2150 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 2151 return ( 2152 StatusCode::BAD_REQUEST, 2153 Json(serde_json::json!({ 2154 "error": "invalid_request", 2155 "error_description": "Authorization request has expired." 2156 })), 2157 ) 2158 .into_response(); 2159 } 2160 2161 let did = match request_data.did { 2162 Some(d) => d, 2163 None => { 2164 return ( 2165 StatusCode::BAD_REQUEST, 2166 Json(serde_json::json!({ 2167 "error": "invalid_request", 2168 "error_description": "No passkey authentication in progress." 2169 })), 2170 ) 2171 .into_response(); 2172 } 2173 }; 2174 2175 let auth_state = match crate::auth::webauthn::load_authentication_state(&state.db, &did).await { 2176 Ok(Some(s)) => s, 2177 Ok(None) => { 2178 return ( 2179 StatusCode::BAD_REQUEST, 2180 Json(serde_json::json!({ 2181 "error": "invalid_request", 2182 "error_description": "No passkey authentication in progress or challenge expired." 2183 })), 2184 ) 2185 .into_response(); 2186 } 2187 Err(e) => { 2188 tracing::error!(error = %e, "Failed to load authentication state"); 2189 return ( 2190 StatusCode::INTERNAL_SERVER_ERROR, 2191 Json(serde_json::json!({ 2192 "error": "server_error", 2193 "error_description": "An error occurred." 2194 })), 2195 ) 2196 .into_response(); 2197 } 2198 }; 2199 2200 let credential: webauthn_rs::prelude::PublicKeyCredential = 2201 match serde_json::from_value(form.credential) { 2202 Ok(c) => c, 2203 Err(e) => { 2204 tracing::warn!(error = %e, "Failed to parse credential"); 2205 return ( 2206 StatusCode::BAD_REQUEST, 2207 Json(serde_json::json!({ 2208 "error": "invalid_request", 2209 "error_description": "Failed to parse credential response." 2210 })), 2211 ) 2212 .into_response(); 2213 } 2214 }; 2215 2216 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2217 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 2218 Ok(w) => w, 2219 Err(e) => { 2220 tracing::error!(error = %e, "Failed to create WebAuthn config"); 2221 return ( 2222 StatusCode::INTERNAL_SERVER_ERROR, 2223 Json(serde_json::json!({ 2224 "error": "server_error", 2225 "error_description": "WebAuthn configuration failed." 2226 })), 2227 ) 2228 .into_response(); 2229 } 2230 }; 2231 2232 let auth_result = match webauthn.finish_authentication(&credential, &auth_state) { 2233 Ok(r) => r, 2234 Err(e) => { 2235 tracing::warn!(error = %e, did = %did, "Failed to verify passkey authentication"); 2236 return ( 2237 StatusCode::FORBIDDEN, 2238 Json(serde_json::json!({ 2239 "error": "access_denied", 2240 "error_description": "Passkey verification failed." 2241 })), 2242 ) 2243 .into_response(); 2244 } 2245 }; 2246 2247 if let Err(e) = crate::auth::webauthn::delete_authentication_state(&state.db, &did).await { 2248 tracing::warn!(error = %e, "Failed to delete authentication state"); 2249 } 2250 2251 if auth_result.needs_update() { 2252 match crate::auth::webauthn::update_passkey_counter( 2253 &state.db, 2254 auth_result.cred_id(), 2255 auth_result.counter(), 2256 ) 2257 .await 2258 { 2259 Ok(false) => { 2260 tracing::warn!(did = %did, "Passkey counter anomaly detected - possible cloned key"); 2261 return ( 2262 StatusCode::FORBIDDEN, 2263 Json(serde_json::json!({ 2264 "error": "access_denied", 2265 "error_description": "Security key counter anomaly detected. This may indicate a cloned key." 2266 })), 2267 ) 2268 .into_response(); 2269 } 2270 Err(e) => { 2271 tracing::warn!(error = %e, "Failed to update passkey counter"); 2272 } 2273 Ok(true) => {} 2274 } 2275 } 2276 2277 tracing::info!(did = %did, "Passkey authentication successful"); 2278 2279 let has_totp = crate::api::server::has_totp_enabled(&state, &did).await; 2280 if has_totp { 2281 return Json(serde_json::json!({ 2282 "needs_totp": true 2283 })) 2284 .into_response(); 2285 } 2286 2287 let user = sqlx::query!( 2288 "SELECT two_factor_enabled, preferred_comms_channel as \"preferred_comms_channel: CommsChannel\", id FROM users WHERE did = $1", 2289 did 2290 ) 2291 .fetch_optional(&state.db) 2292 .await; 2293 2294 if let Ok(Some(user)) = user 2295 && user.two_factor_enabled 2296 { 2297 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 2298 match db::create_2fa_challenge(&state.db, &did, &form.request_uri).await { 2299 Ok(challenge) => { 2300 let hostname = 2301 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2302 if let Err(e) = 2303 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await 2304 { 2305 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification"); 2306 } 2307 let channel_name = channel_display_name(user.preferred_comms_channel); 2308 return Json(serde_json::json!({ 2309 "needs_2fa": true, 2310 "channel": channel_name 2311 })) 2312 .into_response(); 2313 } 2314 Err(_) => { 2315 return ( 2316 StatusCode::INTERNAL_SERVER_ERROR, 2317 Json(serde_json::json!({ 2318 "error": "server_error", 2319 "error_description": "An error occurred." 2320 })), 2321 ) 2322 .into_response(); 2323 } 2324 } 2325 } 2326 2327 let device_id = extract_device_cookie(&headers); 2328 let requested_scope_str = request_data 2329 .parameters 2330 .scope 2331 .as_deref() 2332 .unwrap_or("atproto"); 2333 let requested_scopes: Vec<String> = requested_scope_str 2334 .split_whitespace() 2335 .map(|s| s.to_string()) 2336 .collect(); 2337 2338 let needs_consent = db::should_show_consent( 2339 &state.db, 2340 &did, 2341 &request_data.parameters.client_id, 2342 &requested_scopes, 2343 ) 2344 .await 2345 .unwrap_or(true); 2346 2347 if needs_consent { 2348 let consent_url = format!( 2349 "/app/oauth/consent?request_uri={}", 2350 url_encode(&form.request_uri) 2351 ); 2352 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 2353 } 2354 2355 let code = Code::generate(); 2356 if db::update_authorization_request( 2357 &state.db, 2358 &form.request_uri, 2359 &did, 2360 device_id.as_deref(), 2361 &code.0, 2362 ) 2363 .await 2364 .is_err() 2365 { 2366 return ( 2367 StatusCode::INTERNAL_SERVER_ERROR, 2368 Json(serde_json::json!({ 2369 "error": "server_error", 2370 "error_description": "An error occurred." 2371 })), 2372 ) 2373 .into_response(); 2374 } 2375 2376 let redirect_url = build_success_redirect( 2377 &request_data.parameters.redirect_uri, 2378 &code.0, 2379 request_data.parameters.state.as_deref(), 2380 request_data.parameters.response_mode.as_deref(), 2381 ); 2382 2383 Json(serde_json::json!({ 2384 "redirect_uri": redirect_url 2385 })) 2386 .into_response() 2387} 2388 2389#[derive(Debug, Deserialize)] 2390pub struct AuthorizePasskeyQuery { 2391 pub request_uri: String, 2392} 2393 2394#[derive(Debug, Serialize)] 2395#[serde(rename_all = "camelCase")] 2396pub struct PasskeyAuthResponse { 2397 pub options: serde_json::Value, 2398 pub request_uri: String, 2399} 2400 2401pub async fn authorize_passkey_start( 2402 State(state): State<AppState>, 2403 Query(query): Query<AuthorizePasskeyQuery>, 2404) -> Response { 2405 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2406 2407 let request_data = match db::get_authorization_request(&state.db, &query.request_uri).await { 2408 Ok(Some(d)) => d, 2409 Ok(None) => { 2410 return ( 2411 StatusCode::BAD_REQUEST, 2412 Json(serde_json::json!({ 2413 "error": "invalid_request", 2414 "error_description": "Authorization request not found." 2415 })), 2416 ) 2417 .into_response(); 2418 } 2419 Err(_) => { 2420 return ( 2421 StatusCode::INTERNAL_SERVER_ERROR, 2422 Json(serde_json::json!({ 2423 "error": "server_error", 2424 "error_description": "An error occurred." 2425 })), 2426 ) 2427 .into_response(); 2428 } 2429 }; 2430 2431 if request_data.expires_at < Utc::now() { 2432 let _ = db::delete_authorization_request(&state.db, &query.request_uri).await; 2433 return ( 2434 StatusCode::BAD_REQUEST, 2435 Json(serde_json::json!({ 2436 "error": "invalid_request", 2437 "error_description": "Authorization request has expired." 2438 })), 2439 ) 2440 .into_response(); 2441 } 2442 2443 let did = match &request_data.did { 2444 Some(d) => d.clone(), 2445 None => { 2446 return ( 2447 StatusCode::BAD_REQUEST, 2448 Json(serde_json::json!({ 2449 "error": "invalid_request", 2450 "error_description": "User not authenticated yet." 2451 })), 2452 ) 2453 .into_response(); 2454 } 2455 }; 2456 2457 let stored_passkeys = match crate::auth::webauthn::get_passkeys_for_user(&state.db, &did).await 2458 { 2459 Ok(pks) => pks, 2460 Err(e) => { 2461 tracing::error!("Failed to get passkeys: {:?}", e); 2462 return ( 2463 StatusCode::INTERNAL_SERVER_ERROR, 2464 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2465 ) 2466 .into_response(); 2467 } 2468 }; 2469 2470 if stored_passkeys.is_empty() { 2471 return ( 2472 StatusCode::BAD_REQUEST, 2473 Json(serde_json::json!({ 2474 "error": "invalid_request", 2475 "error_description": "No passkeys registered for this account." 2476 })), 2477 ) 2478 .into_response(); 2479 } 2480 2481 let passkeys: Vec<webauthn_rs::prelude::SecurityKey> = stored_passkeys 2482 .iter() 2483 .filter_map(|sp| sp.to_security_key().ok()) 2484 .collect(); 2485 2486 if passkeys.is_empty() { 2487 return ( 2488 StatusCode::INTERNAL_SERVER_ERROR, 2489 Json(serde_json::json!({"error": "server_error", "error_description": "Failed to load passkeys."})), 2490 ) 2491 .into_response(); 2492 } 2493 2494 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 2495 Ok(w) => w, 2496 Err(e) => { 2497 tracing::error!("Failed to create WebAuthn config: {:?}", e); 2498 return ( 2499 StatusCode::INTERNAL_SERVER_ERROR, 2500 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2501 ) 2502 .into_response(); 2503 } 2504 }; 2505 2506 let (rcr, auth_state) = match webauthn.start_authentication(passkeys) { 2507 Ok(result) => result, 2508 Err(e) => { 2509 tracing::error!("Failed to start passkey authentication: {:?}", e); 2510 return ( 2511 StatusCode::INTERNAL_SERVER_ERROR, 2512 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2513 ) 2514 .into_response(); 2515 } 2516 }; 2517 2518 if let Err(e) = 2519 crate::auth::webauthn::save_authentication_state(&state.db, &did, &auth_state).await 2520 { 2521 tracing::error!("Failed to save authentication state: {:?}", e); 2522 return ( 2523 StatusCode::INTERNAL_SERVER_ERROR, 2524 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2525 ) 2526 .into_response(); 2527 } 2528 2529 let options = serde_json::to_value(&rcr).unwrap_or(serde_json::json!({})); 2530 Json(PasskeyAuthResponse { 2531 options, 2532 request_uri: query.request_uri, 2533 }) 2534 .into_response() 2535} 2536 2537#[derive(Debug, Deserialize)] 2538#[serde(rename_all = "camelCase")] 2539pub struct AuthorizePasskeySubmit { 2540 pub request_uri: String, 2541 pub credential: serde_json::Value, 2542} 2543 2544pub async fn authorize_passkey_finish( 2545 State(state): State<AppState>, 2546 headers: HeaderMap, 2547 Json(form): Json<AuthorizePasskeySubmit>, 2548) -> Response { 2549 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 2550 2551 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 2552 Ok(Some(d)) => d, 2553 Ok(None) => { 2554 return ( 2555 StatusCode::BAD_REQUEST, 2556 Json(serde_json::json!({ 2557 "error": "invalid_request", 2558 "error_description": "Authorization request not found." 2559 })), 2560 ) 2561 .into_response(); 2562 } 2563 Err(_) => { 2564 return ( 2565 StatusCode::INTERNAL_SERVER_ERROR, 2566 Json(serde_json::json!({ 2567 "error": "server_error", 2568 "error_description": "An error occurred." 2569 })), 2570 ) 2571 .into_response(); 2572 } 2573 }; 2574 2575 if request_data.expires_at < Utc::now() { 2576 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 2577 return ( 2578 StatusCode::BAD_REQUEST, 2579 Json(serde_json::json!({ 2580 "error": "invalid_request", 2581 "error_description": "Authorization request has expired." 2582 })), 2583 ) 2584 .into_response(); 2585 } 2586 2587 let did = match &request_data.did { 2588 Some(d) => d.clone(), 2589 None => { 2590 return ( 2591 StatusCode::BAD_REQUEST, 2592 Json(serde_json::json!({ 2593 "error": "invalid_request", 2594 "error_description": "User not authenticated yet." 2595 })), 2596 ) 2597 .into_response(); 2598 } 2599 }; 2600 2601 let auth_state = match crate::auth::webauthn::load_authentication_state(&state.db, &did).await { 2602 Ok(Some(s)) => s, 2603 Ok(None) => { 2604 return ( 2605 StatusCode::BAD_REQUEST, 2606 Json(serde_json::json!({ 2607 "error": "invalid_request", 2608 "error_description": "No passkey challenge found. Please start over." 2609 })), 2610 ) 2611 .into_response(); 2612 } 2613 Err(e) => { 2614 tracing::error!("Failed to load authentication state: {:?}", e); 2615 return ( 2616 StatusCode::INTERNAL_SERVER_ERROR, 2617 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2618 ) 2619 .into_response(); 2620 } 2621 }; 2622 2623 let credential: webauthn_rs::prelude::PublicKeyCredential = 2624 match serde_json::from_value(form.credential.clone()) { 2625 Ok(c) => c, 2626 Err(e) => { 2627 tracing::error!("Failed to parse credential: {:?}", e); 2628 return ( 2629 StatusCode::BAD_REQUEST, 2630 Json(serde_json::json!({ 2631 "error": "invalid_request", 2632 "error_description": "Invalid credential format." 2633 })), 2634 ) 2635 .into_response(); 2636 } 2637 }; 2638 2639 let webauthn = match crate::auth::webauthn::WebAuthnConfig::new(&pds_hostname) { 2640 Ok(w) => w, 2641 Err(e) => { 2642 tracing::error!("Failed to create WebAuthn config: {:?}", e); 2643 return ( 2644 StatusCode::INTERNAL_SERVER_ERROR, 2645 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2646 ) 2647 .into_response(); 2648 } 2649 }; 2650 2651 let auth_result = match webauthn.finish_authentication(&credential, &auth_state) { 2652 Ok(r) => r, 2653 Err(e) => { 2654 tracing::warn!("Passkey authentication failed: {:?}", e); 2655 return ( 2656 StatusCode::FORBIDDEN, 2657 Json(serde_json::json!({ 2658 "error": "access_denied", 2659 "error_description": "Passkey authentication failed." 2660 })), 2661 ) 2662 .into_response(); 2663 } 2664 }; 2665 2666 let _ = crate::auth::webauthn::delete_authentication_state(&state.db, &did).await; 2667 2668 match crate::auth::webauthn::update_passkey_counter( 2669 &state.db, 2670 credential.id.as_ref(), 2671 auth_result.counter(), 2672 ) 2673 .await 2674 { 2675 Ok(false) => { 2676 tracing::warn!(did = %did, "Passkey counter anomaly detected - possible cloned key"); 2677 return ( 2678 StatusCode::FORBIDDEN, 2679 Json(serde_json::json!({ 2680 "error": "access_denied", 2681 "error_description": "Security key counter anomaly detected. This may indicate a cloned key." 2682 })), 2683 ) 2684 .into_response(); 2685 } 2686 Err(e) => { 2687 tracing::warn!("Failed to update passkey counter: {:?}", e); 2688 } 2689 Ok(true) => {} 2690 } 2691 2692 let has_totp = crate::api::server::has_totp_enabled_db(&state.db, &did).await; 2693 if has_totp { 2694 let device_cookie = extract_device_cookie(&headers); 2695 let device_is_trusted = if let Some(ref dev_id) = device_cookie { 2696 crate::api::server::is_device_trusted(&state.db, dev_id, &did).await 2697 } else { 2698 false 2699 }; 2700 2701 if device_is_trusted { 2702 if let Some(ref dev_id) = device_cookie { 2703 let _ = crate::api::server::extend_device_trust(&state.db, dev_id).await; 2704 } 2705 } else { 2706 let user = match sqlx::query!( 2707 r#"SELECT id, preferred_comms_channel as "preferred_comms_channel: CommsChannel" FROM users WHERE did = $1"#, 2708 did 2709 ) 2710 .fetch_optional(&state.db) 2711 .await 2712 { 2713 Ok(Some(u)) => u, 2714 _ => { 2715 return ( 2716 StatusCode::INTERNAL_SERVER_ERROR, 2717 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2718 ) 2719 .into_response(); 2720 } 2721 }; 2722 2723 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 2724 match db::create_2fa_challenge(&state.db, &did, &form.request_uri).await { 2725 Ok(challenge) => { 2726 if let Err(e) = 2727 enqueue_2fa_code(&state.db, user.id, &challenge.code, &pds_hostname).await 2728 { 2729 tracing::warn!(did = %did, error = %e, "Failed to enqueue 2FA notification"); 2730 } 2731 let channel_name = channel_display_name(user.preferred_comms_channel); 2732 let redirect_url = format!( 2733 "/app/oauth/2fa?request_uri={}&channel={}", 2734 url_encode(&form.request_uri), 2735 url_encode(channel_name) 2736 ); 2737 return ( 2738 StatusCode::OK, 2739 Json(serde_json::json!({ 2740 "next": "2fa", 2741 "redirect": redirect_url 2742 })), 2743 ) 2744 .into_response(); 2745 } 2746 Err(_) => { 2747 return ( 2748 StatusCode::INTERNAL_SERVER_ERROR, 2749 Json(serde_json::json!({"error": "server_error", "error_description": "An error occurred."})), 2750 ) 2751 .into_response(); 2752 } 2753 } 2754 } 2755 } 2756 2757 let redirect_url = format!( 2758 "/app/oauth/consent?request_uri={}", 2759 url_encode(&form.request_uri) 2760 ); 2761 ( 2762 StatusCode::OK, 2763 Json(serde_json::json!({ 2764 "next": "consent", 2765 "redirect": redirect_url 2766 })), 2767 ) 2768 .into_response() 2769}