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