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