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