this repo has no description
at main 93 kB view raw
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 cookie_str.split(';').map(|c| c.trim()).find_map(|cookie| { 106 cookie 107 .strip_prefix(&format!("{}=", DEVICE_COOKIE_NAME)) 108 .and_then(|value| crate::config::AuthConfig::get().verify_device_cookie(value)) 109 }) 110 }) 111} 112 113fn extract_client_ip(headers: &HeaderMap) -> String { 114 if let Some(forwarded) = headers.get("x-forwarded-for") 115 && let Ok(value) = forwarded.to_str() 116 && let Some(first_ip) = value.split(',').next() 117 { 118 return first_ip.trim().to_string(); 119 } 120 if let Some(real_ip) = headers.get("x-real-ip") 121 && let Ok(value) = real_ip.to_str() 122 { 123 return value.trim().to_string(); 124 } 125 "0.0.0.0".to_string() 126} 127 128fn extract_user_agent(headers: &HeaderMap) -> Option<String> { 129 headers 130 .get("user-agent") 131 .and_then(|v| v.to_str().ok()) 132 .map(|s| s.to_string()) 133} 134 135fn make_device_cookie(device_id: &str) -> String { 136 let signed_value = crate::config::AuthConfig::get().sign_device_cookie(device_id); 137 format!( 138 "{}={}; Path=/oauth; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000", 139 DEVICE_COOKIE_NAME, signed_value 140 ) 141} 142 143#[derive(Debug, Deserialize)] 144pub struct AuthorizeQuery { 145 pub request_uri: Option<String>, 146 pub client_id: Option<String>, 147 pub new_account: Option<bool>, 148} 149 150#[derive(Debug, Serialize)] 151pub struct AuthorizeResponse { 152 pub client_id: String, 153 pub client_name: Option<String>, 154 pub scope: Option<String>, 155 pub redirect_uri: String, 156 pub state: Option<String>, 157 pub login_hint: Option<String>, 158} 159 160#[derive(Debug, Deserialize)] 161pub struct AuthorizeSubmit { 162 pub request_uri: String, 163 pub username: String, 164 pub password: PlainPassword, 165 #[serde(default)] 166 pub remember_device: bool, 167} 168 169#[derive(Debug, Deserialize)] 170pub struct AuthorizeSelectSubmit { 171 pub request_uri: String, 172 pub did: String, 173} 174 175fn wants_json(headers: &HeaderMap) -> bool { 176 headers 177 .get("accept") 178 .and_then(|v| v.to_str().ok()) 179 .map(|accept| accept.contains("application/json")) 180 .unwrap_or(false) 181} 182 183pub async fn authorize_get( 184 State(state): State<AppState>, 185 headers: HeaderMap, 186 Query(query): Query<AuthorizeQuery>, 187) -> Response { 188 let request_uri = match query.request_uri { 189 Some(uri) => uri, 190 None => { 191 if wants_json(&headers) { 192 return ( 193 StatusCode::BAD_REQUEST, 194 Json(serde_json::json!({ 195 "error": "invalid_request", 196 "error_description": "Missing request_uri parameter. Use PAR to initiate authorization." 197 })), 198 ).into_response(); 199 } 200 return redirect_to_frontend_error( 201 "invalid_request", 202 "Missing request_uri parameter. Use PAR to initiate authorization.", 203 ); 204 } 205 }; 206 let request_data = match db::get_authorization_request(&state.db, &request_uri).await { 207 Ok(Some(data)) => data, 208 Ok(None) => { 209 if wants_json(&headers) { 210 return ( 211 StatusCode::BAD_REQUEST, 212 Json(serde_json::json!({ 213 "error": "invalid_request", 214 "error_description": "Invalid or expired request_uri. Please start a new authorization request." 215 })), 216 ).into_response(); 217 } 218 return redirect_to_frontend_error( 219 "invalid_request", 220 "Invalid or expired request_uri. Please start a new authorization request.", 221 ); 222 } 223 Err(e) => { 224 if wants_json(&headers) { 225 return ( 226 StatusCode::INTERNAL_SERVER_ERROR, 227 Json(serde_json::json!({ 228 "error": "server_error", 229 "error_description": format!("Database error: {:?}", e) 230 })), 231 ) 232 .into_response(); 233 } 234 return redirect_to_frontend_error("server_error", "A database error occurred."); 235 } 236 }; 237 if request_data.expires_at < Utc::now() { 238 let _ = db::delete_authorization_request(&state.db, &request_uri).await; 239 if wants_json(&headers) { 240 return ( 241 StatusCode::BAD_REQUEST, 242 Json(serde_json::json!({ 243 "error": "invalid_request", 244 "error_description": "Authorization request has expired. Please start a new request." 245 })), 246 ).into_response(); 247 } 248 return redirect_to_frontend_error( 249 "invalid_request", 250 "Authorization request has expired. Please start a new request.", 251 ); 252 } 253 let client_cache = ClientMetadataCache::new(3600); 254 let client_name = client_cache 255 .get(&request_data.parameters.client_id) 256 .await 257 .ok() 258 .and_then(|m| m.client_name); 259 if wants_json(&headers) { 260 return Json(AuthorizeResponse { 261 client_id: request_data.parameters.client_id.clone(), 262 client_name: client_name.clone(), 263 scope: request_data.parameters.scope.clone(), 264 redirect_uri: request_data.parameters.redirect_uri.clone(), 265 state: request_data.parameters.state.clone(), 266 login_hint: request_data.parameters.login_hint.clone(), 267 }) 268 .into_response(); 269 } 270 let force_new_account = query.new_account.unwrap_or(false); 271 272 if let Some(ref login_hint) = request_data.parameters.login_hint { 273 tracing::info!(login_hint = %login_hint, "Checking login_hint for delegation"); 274 let pds_hostname = 275 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 276 let normalized = if login_hint.contains('@') || login_hint.starts_with("did:") { 277 login_hint.clone() 278 } else if !login_hint.contains('.') { 279 format!("{}.{}", login_hint.to_lowercase(), pds_hostname) 280 } else { 281 login_hint.to_lowercase() 282 }; 283 tracing::info!(normalized = %normalized, "Normalized login_hint"); 284 285 match sqlx::query!( 286 "SELECT did, password_hash FROM users WHERE handle = $1 OR email = $1", 287 normalized 288 ) 289 .fetch_optional(&state.db) 290 .await 291 { 292 Ok(Some(user)) => { 293 tracing::info!(did = %user.did, has_password = user.password_hash.is_some(), "Found user for login_hint"); 294 let is_delegated = crate::delegation::is_delegated_account(&state.db, &user.did) 295 .await 296 .unwrap_or(false); 297 let has_password = user.password_hash.is_some(); 298 tracing::info!(is_delegated = %is_delegated, has_password = %has_password, "Delegation check"); 299 300 if is_delegated && !has_password { 301 tracing::info!("Redirecting to delegation auth"); 302 return redirect_see_other(&format!( 303 "/app/oauth/delegation?request_uri={}&delegated_did={}", 304 url_encode(&request_uri), 305 url_encode(&user.did) 306 )); 307 } 308 } 309 Ok(None) => { 310 tracing::info!(normalized = %normalized, "No user found for login_hint"); 311 } 312 Err(e) => { 313 tracing::error!(error = %e, "Error looking up user for login_hint"); 314 } 315 } 316 } else { 317 tracing::info!("No login_hint in request"); 318 } 319 320 if !force_new_account 321 && let Some(device_id) = extract_device_cookie(&headers) 322 && let Ok(accounts) = db::get_device_accounts(&state.db, &device_id).await 323 && !accounts.is_empty() 324 { 325 return redirect_see_other(&format!( 326 "/app/oauth/accounts?request_uri={}", 327 url_encode(&request_uri) 328 )); 329 } 330 redirect_see_other(&format!( 331 "/app/oauth/login?request_uri={}", 332 url_encode(&request_uri) 333 )) 334} 335 336pub async fn authorize_get_json( 337 State(state): State<AppState>, 338 Query(query): Query<AuthorizeQuery>, 339) -> Result<Json<AuthorizeResponse>, OAuthError> { 340 let request_uri = query 341 .request_uri 342 .ok_or_else(|| OAuthError::InvalidRequest("request_uri is required".to_string()))?; 343 let request_data = db::get_authorization_request(&state.db, &request_uri) 344 .await? 345 .ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?; 346 if request_data.expires_at < Utc::now() { 347 db::delete_authorization_request(&state.db, &request_uri).await?; 348 return Err(OAuthError::InvalidRequest( 349 "request_uri has expired".to_string(), 350 )); 351 } 352 Ok(Json(AuthorizeResponse { 353 client_id: request_data.parameters.client_id.clone(), 354 client_name: None, 355 scope: request_data.parameters.scope.clone(), 356 redirect_uri: request_data.parameters.redirect_uri.clone(), 357 state: request_data.parameters.state.clone(), 358 login_hint: request_data.parameters.login_hint.clone(), 359 })) 360} 361 362#[derive(Debug, Serialize)] 363pub struct AccountInfo { 364 pub did: String, 365 pub handle: Handle, 366 #[serde(skip_serializing_if = "Option::is_none")] 367 pub email: Option<String>, 368} 369 370#[derive(Debug, Serialize)] 371pub struct AccountsResponse { 372 pub accounts: Vec<AccountInfo>, 373 pub request_uri: String, 374} 375 376fn mask_email(email: &str) -> String { 377 if let Some(at_pos) = email.find('@') { 378 let local = &email[..at_pos]; 379 let domain = &email[at_pos..]; 380 if local.len() <= 2 { 381 format!("{}***{}", local.chars().next().unwrap_or('*'), domain) 382 } else { 383 let first = local.chars().next().unwrap_or('*'); 384 let last = local.chars().last().unwrap_or('*'); 385 format!("{}***{}{}", first, last, domain) 386 } 387 } else { 388 "***".to_string() 389 } 390} 391 392pub async fn authorize_accounts( 393 State(state): State<AppState>, 394 headers: HeaderMap, 395 Query(query): Query<AuthorizeQuery>, 396) -> Response { 397 let request_uri = match query.request_uri { 398 Some(uri) => uri, 399 None => { 400 return ( 401 StatusCode::BAD_REQUEST, 402 Json(serde_json::json!({ 403 "error": "invalid_request", 404 "error_description": "Missing request_uri parameter" 405 })), 406 ) 407 .into_response(); 408 } 409 }; 410 let device_id = match extract_device_cookie(&headers) { 411 Some(id) => id, 412 None => { 413 return Json(AccountsResponse { 414 accounts: vec![], 415 request_uri, 416 }) 417 .into_response(); 418 } 419 }; 420 let accounts = match db::get_device_accounts(&state.db, &device_id).await { 421 Ok(accts) => accts, 422 Err(_) => { 423 return Json(AccountsResponse { 424 accounts: vec![], 425 request_uri, 426 }) 427 .into_response(); 428 } 429 }; 430 let account_infos: Vec<AccountInfo> = accounts 431 .into_iter() 432 .map(|row| AccountInfo { 433 did: row.did, 434 handle: row.handle, 435 email: row.email.map(|e| mask_email(&e)), 436 }) 437 .collect(); 438 Json(AccountsResponse { 439 accounts: account_infos, 440 request_uri, 441 }) 442 .into_response() 443} 444 445pub async fn authorize_post( 446 State(state): State<AppState>, 447 headers: HeaderMap, 448 Json(form): Json<AuthorizeSubmit>, 449) -> Response { 450 let json_response = wants_json(&headers); 451 let client_ip = extract_client_ip(&headers); 452 if !state 453 .check_rate_limit(RateLimitKind::OAuthAuthorize, &client_ip) 454 .await 455 { 456 tracing::warn!(ip = %client_ip, "OAuth authorize rate limit exceeded"); 457 if json_response { 458 return ( 459 axum::http::StatusCode::TOO_MANY_REQUESTS, 460 Json(serde_json::json!({ 461 "error": "RateLimitExceeded", 462 "error_description": "Too many login attempts. Please try again later." 463 })), 464 ) 465 .into_response(); 466 } 467 return redirect_to_frontend_error( 468 "RateLimitExceeded", 469 "Too many login attempts. Please try again later.", 470 ); 471 } 472 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 473 Ok(Some(data)) => data, 474 Ok(None) => { 475 if json_response { 476 return ( 477 axum::http::StatusCode::BAD_REQUEST, 478 Json(serde_json::json!({ 479 "error": "invalid_request", 480 "error_description": "Invalid or expired request_uri." 481 })), 482 ) 483 .into_response(); 484 } 485 return redirect_to_frontend_error( 486 "invalid_request", 487 "Invalid or expired request_uri. Please start a new authorization request.", 488 ); 489 } 490 Err(e) => { 491 if json_response { 492 return ( 493 axum::http::StatusCode::INTERNAL_SERVER_ERROR, 494 Json(serde_json::json!({ 495 "error": "server_error", 496 "error_description": format!("Database error: {:?}", e) 497 })), 498 ) 499 .into_response(); 500 } 501 return redirect_to_frontend_error("server_error", &format!("Database error: {:?}", e)); 502 } 503 }; 504 if request_data.expires_at < Utc::now() { 505 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 506 if json_response { 507 return ( 508 axum::http::StatusCode::BAD_REQUEST, 509 Json(serde_json::json!({ 510 "error": "invalid_request", 511 "error_description": "Authorization request has expired." 512 })), 513 ) 514 .into_response(); 515 } 516 return redirect_to_frontend_error( 517 "invalid_request", 518 "Authorization request has expired. Please start a new request.", 519 ); 520 } 521 let show_login_error = |error_msg: &str, json: bool| -> Response { 522 if json { 523 return ( 524 axum::http::StatusCode::FORBIDDEN, 525 Json(serde_json::json!({ 526 "error": "access_denied", 527 "error_description": error_msg 528 })), 529 ) 530 .into_response(); 531 } 532 redirect_see_other(&format!( 533 "/app/oauth/login?request_uri={}&error={}", 534 url_encode(&form.request_uri), 535 url_encode(error_msg) 536 )) 537 }; 538 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 539 let normalized_username = form.username.trim(); 540 let normalized_username = normalized_username 541 .strip_prefix('@') 542 .unwrap_or(normalized_username); 543 let normalized_username = if normalized_username.contains('@') { 544 normalized_username.to_string() 545 } else if !normalized_username.contains('.') { 546 format!("{}.{}", normalized_username, pds_hostname) 547 } else { 548 normalized_username.to_string() 549 }; 550 tracing::debug!( 551 original_username = %form.username, 552 normalized_username = %normalized_username, 553 pds_hostname = %pds_hostname, 554 "Normalized username for lookup" 555 ); 556 let user = match sqlx::query!( 557 r#" 558 SELECT id, did, email, password_hash, password_required, two_factor_enabled, 559 preferred_comms_channel as "preferred_comms_channel: CommsChannel", 560 deactivated_at, takedown_ref, 561 email_verified, discord_verified, telegram_verified, signal_verified, 562 account_type::text as "account_type!" 563 FROM users 564 WHERE handle = $1 OR email = $1 565 "#, 566 normalized_username 567 ) 568 .fetch_optional(&state.db) 569 .await 570 { 571 Ok(Some(u)) => u, 572 Ok(None) => { 573 let _ = bcrypt::verify( 574 &form.password, 575 "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK", 576 ); 577 return show_login_error("Invalid handle/email or password.", json_response); 578 } 579 Err(_) => return show_login_error("An error occurred. Please try again.", json_response), 580 }; 581 if user.deactivated_at.is_some() { 582 return show_login_error("This account has been deactivated.", json_response); 583 } 584 if user.takedown_ref.is_some() { 585 return show_login_error("This account has been taken down.", json_response); 586 } 587 let is_verified = user.email_verified 588 || user.discord_verified 589 || user.telegram_verified 590 || user.signal_verified; 591 if !is_verified { 592 return show_login_error( 593 "Please verify your account before logging in.", 594 json_response, 595 ); 596 } 597 598 if user.account_type == "delegated" { 599 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None) 600 .await 601 .is_err() 602 { 603 return show_login_error("An error occurred. Please try again.", json_response); 604 } 605 let redirect_url = format!( 606 "/app/oauth/delegation?request_uri={}&delegated_did={}", 607 url_encode(&form.request_uri), 608 url_encode(&user.did) 609 ); 610 if json_response { 611 return ( 612 StatusCode::OK, 613 Json(serde_json::json!({ 614 "next": "delegation", 615 "delegated_did": user.did, 616 "redirect": redirect_url 617 })), 618 ) 619 .into_response(); 620 } 621 return redirect_see_other(&redirect_url); 622 } 623 624 if !user.password_required { 625 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None) 626 .await 627 .is_err() 628 { 629 return show_login_error("An error occurred. Please try again.", json_response); 630 } 631 let redirect_url = format!( 632 "/app/oauth/passkey?request_uri={}", 633 url_encode(&form.request_uri) 634 ); 635 if json_response { 636 return ( 637 StatusCode::OK, 638 Json(serde_json::json!({ 639 "next": "passkey", 640 "redirect": redirect_url 641 })), 642 ) 643 .into_response(); 644 } 645 return redirect_see_other(&redirect_url); 646 } 647 648 let password_valid = match &user.password_hash { 649 Some(hash) => match bcrypt::verify(&form.password, hash) { 650 Ok(valid) => valid, 651 Err(_) => { 652 return show_login_error("An error occurred. Please try again.", json_response); 653 } 654 }, 655 None => false, 656 }; 657 if !password_valid { 658 return show_login_error("Invalid handle/email or password.", json_response); 659 } 660 let has_totp = crate::api::server::has_totp_enabled(&state, &user.did).await; 661 if has_totp { 662 let device_cookie = extract_device_cookie(&headers); 663 let device_is_trusted = if let Some(ref dev_id) = device_cookie { 664 crate::api::server::is_device_trusted(&state.db, dev_id, &user.did).await 665 } else { 666 false 667 }; 668 669 if device_is_trusted { 670 if let Some(ref dev_id) = device_cookie { 671 let _ = crate::api::server::extend_device_trust(&state.db, dev_id).await; 672 } 673 } else { 674 if db::set_authorization_did(&state.db, &form.request_uri, &user.did, None) 675 .await 676 .is_err() 677 { 678 return show_login_error("An error occurred. Please try again.", json_response); 679 } 680 if json_response { 681 return Json(serde_json::json!({ 682 "needs_totp": true 683 })) 684 .into_response(); 685 } 686 return redirect_see_other(&format!( 687 "/app/oauth/totp?request_uri={}", 688 url_encode(&form.request_uri) 689 )); 690 } 691 } 692 if user.two_factor_enabled { 693 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 694 match db::create_2fa_challenge(&state.db, &user.did, &form.request_uri).await { 695 Ok(challenge) => { 696 let hostname = 697 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 698 if let Err(e) = 699 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await 700 { 701 tracing::warn!( 702 did = %user.did, 703 error = %e, 704 "Failed to enqueue 2FA notification" 705 ); 706 } 707 let channel_name = channel_display_name(user.preferred_comms_channel); 708 if json_response { 709 return Json(serde_json::json!({ 710 "needs_2fa": true, 711 "channel": channel_name 712 })) 713 .into_response(); 714 } 715 return redirect_see_other(&format!( 716 "/app/oauth/2fa?request_uri={}&channel={}", 717 url_encode(&form.request_uri), 718 url_encode(channel_name) 719 )); 720 } 721 Err(_) => { 722 return show_login_error("An error occurred. Please try again.", json_response); 723 } 724 } 725 } 726 let mut device_id: Option<String> = extract_device_cookie(&headers); 727 let mut new_cookie: Option<String> = None; 728 if form.remember_device { 729 let final_device_id = if let Some(existing_id) = &device_id { 730 existing_id.clone() 731 } else { 732 let new_id = DeviceId::generate(); 733 let device_data = DeviceData { 734 session_id: SessionId::generate().0, 735 user_agent: extract_user_agent(&headers), 736 ip_address: extract_client_ip(&headers), 737 last_seen_at: Utc::now(), 738 }; 739 if db::create_device(&state.db, &new_id.0, &device_data) 740 .await 741 .is_ok() 742 { 743 new_cookie = Some(make_device_cookie(&new_id.0)); 744 device_id = Some(new_id.0.clone()); 745 } 746 new_id.0 747 }; 748 let _ = db::upsert_account_device(&state.db, &user.did, &final_device_id).await; 749 } 750 if db::set_authorization_did( 751 &state.db, 752 &form.request_uri, 753 &user.did, 754 device_id.as_deref(), 755 ) 756 .await 757 .is_err() 758 { 759 return show_login_error("An error occurred. Please try again.", json_response); 760 } 761 let requested_scope_str = request_data 762 .parameters 763 .scope 764 .as_deref() 765 .unwrap_or("atproto"); 766 let requested_scopes: Vec<String> = requested_scope_str 767 .split_whitespace() 768 .map(|s| s.to_string()) 769 .collect(); 770 let needs_consent = db::should_show_consent( 771 &state.db, 772 &user.did, 773 &request_data.parameters.client_id, 774 &requested_scopes, 775 ) 776 .await 777 .unwrap_or(true); 778 if needs_consent { 779 let consent_url = format!( 780 "/app/oauth/consent?request_uri={}", 781 url_encode(&form.request_uri) 782 ); 783 if json_response { 784 if let Some(cookie) = new_cookie { 785 return ( 786 StatusCode::OK, 787 [(SET_COOKIE, cookie)], 788 Json(serde_json::json!({"redirect_uri": consent_url})), 789 ) 790 .into_response(); 791 } 792 return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); 793 } 794 if let Some(cookie) = new_cookie { 795 return ( 796 StatusCode::SEE_OTHER, 797 [(SET_COOKIE, cookie), (LOCATION, consent_url)], 798 ) 799 .into_response(); 800 } 801 return redirect_see_other(&consent_url); 802 } 803 let code = Code::generate(); 804 if db::update_authorization_request( 805 &state.db, 806 &form.request_uri, 807 &user.did, 808 device_id.as_deref(), 809 &code.0, 810 ) 811 .await 812 .is_err() 813 { 814 return show_login_error("An error occurred. Please try again.", json_response); 815 } 816 if json_response { 817 let redirect_url = build_intermediate_redirect_url( 818 &request_data.parameters.redirect_uri, 819 &code.0, 820 request_data.parameters.state.as_deref(), 821 request_data.parameters.response_mode.as_deref(), 822 ); 823 if let Some(cookie) = new_cookie { 824 ( 825 StatusCode::OK, 826 [(SET_COOKIE, cookie)], 827 Json(serde_json::json!({"redirect_uri": redirect_url})), 828 ) 829 .into_response() 830 } else { 831 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 832 } 833 } else { 834 let redirect_url = build_success_redirect( 835 &request_data.parameters.redirect_uri, 836 &code.0, 837 request_data.parameters.state.as_deref(), 838 request_data.parameters.response_mode.as_deref(), 839 ); 840 if let Some(cookie) = new_cookie { 841 ( 842 StatusCode::SEE_OTHER, 843 [(SET_COOKIE, cookie), (LOCATION, redirect_url)], 844 ) 845 .into_response() 846 } else { 847 redirect_see_other(&redirect_url) 848 } 849 } 850} 851 852pub async fn authorize_select( 853 State(state): State<AppState>, 854 headers: HeaderMap, 855 Json(form): Json<AuthorizeSelectSubmit>, 856) -> Response { 857 let json_error = |status: StatusCode, error: &str, description: &str| -> Response { 858 ( 859 status, 860 Json(serde_json::json!({ 861 "error": error, 862 "error_description": description 863 })), 864 ) 865 .into_response() 866 }; 867 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 868 Ok(Some(data)) => data, 869 Ok(None) => { 870 return json_error( 871 StatusCode::BAD_REQUEST, 872 "invalid_request", 873 "Invalid or expired request_uri. Please start a new authorization request.", 874 ); 875 } 876 Err(_) => { 877 return json_error( 878 StatusCode::INTERNAL_SERVER_ERROR, 879 "server_error", 880 "An error occurred. Please try again.", 881 ); 882 } 883 }; 884 if request_data.expires_at < Utc::now() { 885 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 886 return json_error( 887 StatusCode::BAD_REQUEST, 888 "invalid_request", 889 "Authorization request has expired. Please start a new request.", 890 ); 891 } 892 let device_id = match extract_device_cookie(&headers) { 893 Some(id) => id, 894 None => { 895 return json_error( 896 StatusCode::BAD_REQUEST, 897 "invalid_request", 898 "No device session found. Please sign in.", 899 ); 900 } 901 }; 902 let account_valid = match db::verify_account_on_device(&state.db, &device_id, &form.did).await { 903 Ok(valid) => valid, 904 Err(_) => { 905 return json_error( 906 StatusCode::INTERNAL_SERVER_ERROR, 907 "server_error", 908 "An error occurred. Please try again.", 909 ); 910 } 911 }; 912 if !account_valid { 913 return json_error( 914 StatusCode::FORBIDDEN, 915 "access_denied", 916 "This account is not available on this device. Please sign in.", 917 ); 918 } 919 let user = match sqlx::query!( 920 r#" 921 SELECT id, two_factor_enabled, 922 preferred_comms_channel as "preferred_comms_channel: CommsChannel", 923 email_verified, discord_verified, telegram_verified, signal_verified 924 FROM users 925 WHERE did = $1 926 "#, 927 form.did 928 ) 929 .fetch_optional(&state.db) 930 .await 931 { 932 Ok(Some(u)) => u, 933 Ok(None) => { 934 return json_error( 935 StatusCode::FORBIDDEN, 936 "access_denied", 937 "Account not found. Please sign in.", 938 ); 939 } 940 Err(_) => { 941 return json_error( 942 StatusCode::INTERNAL_SERVER_ERROR, 943 "server_error", 944 "An error occurred. Please try again.", 945 ); 946 } 947 }; 948 let is_verified = user.email_verified 949 || user.discord_verified 950 || user.telegram_verified 951 || user.signal_verified; 952 if !is_verified { 953 return json_error( 954 StatusCode::FORBIDDEN, 955 "access_denied", 956 "Please verify your account before logging in.", 957 ); 958 } 959 let has_totp = crate::api::server::has_totp_enabled(&state, &form.did).await; 960 if has_totp { 961 if db::set_authorization_did(&state.db, &form.request_uri, &form.did, Some(&device_id)) 962 .await 963 .is_err() 964 { 965 return json_error( 966 StatusCode::INTERNAL_SERVER_ERROR, 967 "server_error", 968 "An error occurred. Please try again.", 969 ); 970 } 971 return Json(serde_json::json!({ 972 "needs_totp": true 973 })) 974 .into_response(); 975 } 976 if user.two_factor_enabled { 977 let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await; 978 match db::create_2fa_challenge(&state.db, &form.did, &form.request_uri).await { 979 Ok(challenge) => { 980 let hostname = 981 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 982 if let Err(e) = 983 enqueue_2fa_code(&state.db, user.id, &challenge.code, &hostname).await 984 { 985 tracing::warn!( 986 did = %form.did, 987 error = %e, 988 "Failed to enqueue 2FA notification" 989 ); 990 } 991 let channel_name = channel_display_name(user.preferred_comms_channel); 992 return Json(serde_json::json!({ 993 "needs_2fa": true, 994 "channel": channel_name 995 })) 996 .into_response(); 997 } 998 Err(_) => { 999 return json_error( 1000 StatusCode::INTERNAL_SERVER_ERROR, 1001 "server_error", 1002 "An error occurred. Please try again.", 1003 ); 1004 } 1005 } 1006 } 1007 let _ = db::upsert_account_device(&state.db, &form.did, &device_id).await; 1008 let code = Code::generate(); 1009 if db::update_authorization_request( 1010 &state.db, 1011 &form.request_uri, 1012 &form.did, 1013 Some(&device_id), 1014 &code.0, 1015 ) 1016 .await 1017 .is_err() 1018 { 1019 return json_error( 1020 StatusCode::INTERNAL_SERVER_ERROR, 1021 "server_error", 1022 "An error occurred. Please try again.", 1023 ); 1024 } 1025 let redirect_url = build_intermediate_redirect_url( 1026 &request_data.parameters.redirect_uri, 1027 &code.0, 1028 request_data.parameters.state.as_deref(), 1029 request_data.parameters.response_mode.as_deref(), 1030 ); 1031 Json(serde_json::json!({ 1032 "redirect_uri": redirect_url 1033 })) 1034 .into_response() 1035} 1036 1037fn build_success_redirect( 1038 redirect_uri: &str, 1039 code: &str, 1040 state: Option<&str>, 1041 response_mode: Option<&str>, 1042) -> String { 1043 let mut redirect_url = redirect_uri.to_string(); 1044 let use_fragment = response_mode == Some("fragment"); 1045 let separator = if use_fragment { 1046 '#' 1047 } else if redirect_url.contains('?') { 1048 '&' 1049 } else { 1050 '?' 1051 }; 1052 redirect_url.push(separator); 1053 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1054 redirect_url.push_str(&format!( 1055 "iss={}", 1056 url_encode(&format!("https://{}", pds_hostname)) 1057 )); 1058 if let Some(req_state) = state { 1059 redirect_url.push_str(&format!("&state={}", url_encode(req_state))); 1060 } 1061 redirect_url.push_str(&format!("&code={}", url_encode(code))); 1062 redirect_url 1063} 1064 1065fn build_intermediate_redirect_url( 1066 redirect_uri: &str, 1067 code: &str, 1068 state: Option<&str>, 1069 response_mode: Option<&str>, 1070) -> String { 1071 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1072 let mut url = format!( 1073 "https://{}/oauth/authorize/redirect?redirect_uri={}&code={}", 1074 pds_hostname, 1075 url_encode(redirect_uri), 1076 url_encode(code) 1077 ); 1078 if let Some(s) = state { 1079 url.push_str(&format!("&state={}", url_encode(s))); 1080 } 1081 if let Some(rm) = response_mode { 1082 url.push_str(&format!("&response_mode={}", url_encode(rm))); 1083 } 1084 url 1085} 1086 1087#[derive(Debug, Deserialize)] 1088pub struct AuthorizeRedirectParams { 1089 redirect_uri: String, 1090 code: String, 1091 state: Option<String>, 1092 response_mode: Option<String>, 1093} 1094 1095pub async fn authorize_redirect(Query(params): Query<AuthorizeRedirectParams>) -> Response { 1096 let final_url = build_success_redirect( 1097 &params.redirect_uri, 1098 &params.code, 1099 params.state.as_deref(), 1100 params.response_mode.as_deref(), 1101 ); 1102 tracing::info!( 1103 final_url = %final_url, 1104 client_redirect = %params.redirect_uri, 1105 "authorize_redirect performing 303 redirect" 1106 ); 1107 ( 1108 StatusCode::SEE_OTHER, 1109 [ 1110 (axum::http::header::LOCATION, final_url), 1111 (axum::http::header::CACHE_CONTROL, "no-store".to_string()), 1112 ], 1113 ) 1114 .into_response() 1115} 1116 1117#[derive(Debug, Serialize)] 1118pub struct AuthorizeDenyResponse { 1119 pub error: String, 1120 pub error_description: String, 1121} 1122 1123pub async fn authorize_deny( 1124 State(state): State<AppState>, 1125 Json(form): Json<AuthorizeDenyForm>, 1126) -> Response { 1127 let request_data = match db::get_authorization_request(&state.db, &form.request_uri).await { 1128 Ok(Some(data)) => data, 1129 Ok(None) => { 1130 return ( 1131 StatusCode::BAD_REQUEST, 1132 Json(serde_json::json!({ 1133 "error": "invalid_request", 1134 "error_description": "Invalid request_uri" 1135 })), 1136 ) 1137 .into_response(); 1138 } 1139 Err(_) => { 1140 return ( 1141 StatusCode::INTERNAL_SERVER_ERROR, 1142 Json(serde_json::json!({ 1143 "error": "server_error", 1144 "error_description": "An error occurred" 1145 })), 1146 ) 1147 .into_response(); 1148 } 1149 }; 1150 let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1151 let redirect_uri = &request_data.parameters.redirect_uri; 1152 let mut redirect_url = redirect_uri.to_string(); 1153 let separator = if redirect_url.contains('?') { '&' } else { '?' }; 1154 redirect_url.push(separator); 1155 redirect_url.push_str("error=access_denied"); 1156 redirect_url.push_str("&error_description=User%20denied%20the%20request"); 1157 if let Some(state) = &request_data.parameters.state { 1158 redirect_url.push_str(&format!("&state={}", url_encode(state))); 1159 } 1160 Json(serde_json::json!({ 1161 "redirect_uri": redirect_url 1162 })) 1163 .into_response() 1164} 1165 1166#[derive(Debug, Deserialize)] 1167pub struct AuthorizeDenyForm { 1168 pub request_uri: String, 1169} 1170 1171#[derive(Debug, Deserialize)] 1172pub struct Authorize2faQuery { 1173 pub request_uri: String, 1174 pub channel: Option<String>, 1175} 1176 1177#[derive(Debug, Deserialize)] 1178pub struct Authorize2faSubmit { 1179 pub request_uri: String, 1180 pub code: String, 1181 #[serde(default)] 1182 pub trust_device: bool, 1183} 1184 1185const MAX_2FA_ATTEMPTS: i32 = 5; 1186 1187pub async fn authorize_2fa_get( 1188 State(state): State<AppState>, 1189 Query(query): Query<Authorize2faQuery>, 1190) -> Response { 1191 let challenge = match db::get_2fa_challenge(&state.db, &query.request_uri).await { 1192 Ok(Some(c)) => c, 1193 Ok(None) => { 1194 return redirect_to_frontend_error( 1195 "invalid_request", 1196 "No 2FA challenge found. Please start over.", 1197 ); 1198 } 1199 Err(_) => { 1200 return redirect_to_frontend_error( 1201 "server_error", 1202 "An error occurred. Please try again.", 1203 ); 1204 } 1205 }; 1206 if challenge.expires_at < Utc::now() { 1207 let _ = db::delete_2fa_challenge(&state.db, challenge.id).await; 1208 return redirect_to_frontend_error( 1209 "invalid_request", 1210 "2FA code has expired. Please start over.", 1211 ); 1212 } 1213 let _request_data = match db::get_authorization_request(&state.db, &query.request_uri).await { 1214 Ok(Some(d)) => d, 1215 Ok(None) => { 1216 return redirect_to_frontend_error( 1217 "invalid_request", 1218 "Authorization request not found. Please start over.", 1219 ); 1220 } 1221 Err(_) => { 1222 return redirect_to_frontend_error( 1223 "server_error", 1224 "An error occurred. Please try again.", 1225 ); 1226 } 1227 }; 1228 let channel = query.channel.as_deref().unwrap_or("email"); 1229 redirect_see_other(&format!( 1230 "/app/oauth/2fa?request_uri={}&channel={}", 1231 url_encode(&query.request_uri), 1232 url_encode(channel) 1233 )) 1234} 1235 1236#[derive(Debug, Serialize)] 1237pub struct ScopeInfo { 1238 pub scope: String, 1239 pub category: String, 1240 pub required: bool, 1241 pub description: String, 1242 pub display_name: String, 1243 pub granted: Option<bool>, 1244} 1245 1246#[derive(Debug, Serialize)] 1247pub struct ConsentResponse { 1248 pub request_uri: String, 1249 pub client_id: String, 1250 pub client_name: Option<String>, 1251 pub client_uri: Option<String>, 1252 pub logo_uri: Option<String>, 1253 pub scopes: Vec<ScopeInfo>, 1254 pub show_consent: bool, 1255 pub did: String, 1256 #[serde(skip_serializing_if = "Option::is_none")] 1257 pub is_delegation: Option<bool>, 1258 #[serde(skip_serializing_if = "Option::is_none")] 1259 pub controller_did: Option<String>, 1260 #[serde(skip_serializing_if = "Option::is_none")] 1261 pub controller_handle: Option<String>, 1262 #[serde(skip_serializing_if = "Option::is_none")] 1263 pub delegation_level: Option<String>, 1264} 1265 1266#[derive(Debug, Deserialize)] 1267pub struct ConsentQuery { 1268 pub request_uri: String, 1269} 1270 1271#[derive(Debug, Deserialize)] 1272pub struct ConsentSubmit { 1273 pub request_uri: String, 1274 pub approved_scopes: Vec<String>, 1275 pub remember: bool, 1276} 1277 1278pub async fn consent_get( 1279 State(state): State<AppState>, 1280 Query(query): Query<ConsentQuery>, 1281) -> Response { 1282 let (request_data, flow_state) = 1283 match db::get_authorization_request_with_state(&state.db, &query.request_uri).await { 1284 Ok(Some(result)) => result, 1285 Ok(None) => { 1286 return json_error( 1287 StatusCode::BAD_REQUEST, 1288 "invalid_request", 1289 "Invalid or expired request_uri", 1290 ); 1291 } 1292 Err(e) => { 1293 return json_error( 1294 StatusCode::INTERNAL_SERVER_ERROR, 1295 "server_error", 1296 &format!("Database error: {:?}", e), 1297 ); 1298 } 1299 }; 1300 1301 if let Some(err_response) = validate_auth_flow_state(&flow_state, true) { 1302 if flow_state.is_expired() { 1303 let _ = db::delete_authorization_request(&state.db, &query.request_uri).await; 1304 } 1305 return err_response; 1306 } 1307 1308 let did = flow_state.did().unwrap().to_string(); 1309 let client_cache = ClientMetadataCache::new(3600); 1310 let client_metadata = client_cache 1311 .get(&request_data.parameters.client_id) 1312 .await 1313 .ok(); 1314 let requested_scope_str = request_data 1315 .parameters 1316 .scope 1317 .as_deref() 1318 .filter(|s| !s.trim().is_empty()) 1319 .unwrap_or("atproto"); 1320 1321 let delegation_grant = if let Some(ref ctrl_did) = request_data.controller_did { 1322 crate::delegation::get_delegation(&state.db, &did, ctrl_did) 1323 .await 1324 .ok() 1325 .flatten() 1326 } else { 1327 None 1328 }; 1329 1330 let effective_scope_str = if let Some(ref grant) = delegation_grant { 1331 crate::delegation::scopes::intersect_scopes(requested_scope_str, &grant.granted_scopes) 1332 } else { 1333 requested_scope_str.to_string() 1334 }; 1335 1336 let requested_scopes: Vec<&str> = effective_scope_str.split_whitespace().collect(); 1337 let preferences = 1338 db::get_scope_preferences(&state.db, &did, &request_data.parameters.client_id) 1339 .await 1340 .unwrap_or_default(); 1341 let pref_map: std::collections::HashMap<_, _> = preferences 1342 .iter() 1343 .map(|p| (p.scope.as_str(), p.granted)) 1344 .collect(); 1345 let requested_scope_strings: Vec<String> = 1346 requested_scopes.iter().map(|s| s.to_string()).collect(); 1347 let show_consent = db::should_show_consent( 1348 &state.db, 1349 &did, 1350 &request_data.parameters.client_id, 1351 &requested_scope_strings, 1352 ) 1353 .await 1354 .unwrap_or(true); 1355 let scopes: Vec<ScopeInfo> = requested_scopes 1356 .iter() 1357 .map(|scope| { 1358 let (category, required, description, display_name) = 1359 if let Some(def) = crate::oauth::scopes::SCOPE_DEFINITIONS.get(*scope) { 1360 ( 1361 def.category.display_name().to_string(), 1362 def.required, 1363 def.description.to_string(), 1364 def.display_name.to_string(), 1365 ) 1366 } else if scope.starts_with("ref:") { 1367 ( 1368 "Reference".to_string(), 1369 false, 1370 "Referenced scope".to_string(), 1371 scope.to_string(), 1372 ) 1373 } else { 1374 ( 1375 "Other".to_string(), 1376 false, 1377 format!("Access to {}", scope), 1378 scope.to_string(), 1379 ) 1380 }; 1381 let granted = pref_map.get(*scope).copied(); 1382 ScopeInfo { 1383 scope: scope.to_string(), 1384 category, 1385 required, 1386 description, 1387 display_name, 1388 granted, 1389 } 1390 }) 1391 .collect(); 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}