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