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