this repo has no description
1use crate::auth::BearerAuth; 2use crate::state::{AppState, RateLimitKind}; 3use crate::validation::validate_password; 4use axum::{ 5 Json, 6 extract::State, 7 http::{HeaderMap, StatusCode}, 8 response::{IntoResponse, Response}, 9}; 10use bcrypt::{DEFAULT_COST, hash, verify}; 11use chrono::{Duration, Utc}; 12use serde::Deserialize; 13use serde_json::json; 14use tracing::{error, info, warn}; 15use uuid::Uuid; 16 17fn generate_reset_code() -> String { 18 crate::util::generate_token_code() 19} 20fn extract_client_ip(headers: &HeaderMap) -> String { 21 if let Some(forwarded) = headers.get("x-forwarded-for") 22 && let Ok(value) = forwarded.to_str() 23 && let Some(first_ip) = value.split(',').next() 24 { 25 return first_ip.trim().to_string(); 26 } 27 if let Some(real_ip) = headers.get("x-real-ip") 28 && let Ok(value) = real_ip.to_str() 29 { 30 return value.trim().to_string(); 31 } 32 "unknown".to_string() 33} 34 35#[derive(Deserialize)] 36pub struct RequestPasswordResetInput { 37 #[serde(alias = "identifier")] 38 pub email: String, 39} 40 41pub async fn request_password_reset( 42 State(state): State<AppState>, 43 headers: HeaderMap, 44 Json(input): Json<RequestPasswordResetInput>, 45) -> Response { 46 let client_ip = extract_client_ip(&headers); 47 if !state 48 .check_rate_limit(RateLimitKind::PasswordReset, &client_ip) 49 .await 50 { 51 warn!(ip = %client_ip, "Password reset rate limit exceeded"); 52 return ( 53 StatusCode::TOO_MANY_REQUESTS, 54 Json(json!({ 55 "error": "RateLimitExceeded", 56 "message": "Too many password reset requests. Please try again later." 57 })), 58 ) 59 .into_response(); 60 } 61 let identifier = input.email.trim(); 62 if identifier.is_empty() { 63 return ( 64 StatusCode::BAD_REQUEST, 65 Json(json!({"error": "InvalidRequest", "message": "email or handle is required"})), 66 ) 67 .into_response(); 68 } 69 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 70 let normalized = identifier.to_lowercase(); 71 let normalized = normalized.strip_prefix('@').unwrap_or(&normalized); 72 let normalized_handle = if normalized.contains('@') || normalized.contains('.') { 73 normalized.to_string() 74 } else { 75 format!("{}.{}", normalized, pds_hostname) 76 }; 77 let user = sqlx::query!( 78 "SELECT id FROM users WHERE LOWER(email) = $1 OR handle = $2", 79 normalized, 80 normalized_handle 81 ) 82 .fetch_optional(&state.db) 83 .await; 84 let user_id = match user { 85 Ok(Some(row)) => row.id, 86 Ok(None) => { 87 info!("Password reset requested for unknown identifier"); 88 return (StatusCode::OK, Json(json!({}))).into_response(); 89 } 90 Err(e) => { 91 error!("DB error in request_password_reset: {:?}", e); 92 return ( 93 StatusCode::INTERNAL_SERVER_ERROR, 94 Json(json!({"error": "InternalError"})), 95 ) 96 .into_response(); 97 } 98 }; 99 let code = generate_reset_code(); 100 let expires_at = Utc::now() + Duration::minutes(10); 101 let update = sqlx::query!( 102 "UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3", 103 code, 104 expires_at, 105 user_id 106 ) 107 .execute(&state.db) 108 .await; 109 if let Err(e) = update { 110 error!("DB error setting reset code: {:?}", e); 111 return ( 112 StatusCode::INTERNAL_SERVER_ERROR, 113 Json(json!({"error": "InternalError"})), 114 ) 115 .into_response(); 116 } 117 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 118 if let Err(e) = crate::comms::enqueue_password_reset(&state.db, user_id, &code, &hostname).await 119 { 120 warn!("Failed to enqueue password reset notification: {:?}", e); 121 } 122 info!("Password reset requested for user {}", user_id); 123 (StatusCode::OK, Json(json!({}))).into_response() 124} 125 126#[derive(Deserialize)] 127pub struct ResetPasswordInput { 128 pub token: String, 129 pub password: String, 130} 131 132pub async fn reset_password( 133 State(state): State<AppState>, 134 headers: HeaderMap, 135 Json(input): Json<ResetPasswordInput>, 136) -> Response { 137 let client_ip = extract_client_ip(&headers); 138 if !state 139 .check_rate_limit(RateLimitKind::ResetPassword, &client_ip) 140 .await 141 { 142 warn!(ip = %client_ip, "Reset password rate limit exceeded"); 143 return ( 144 StatusCode::TOO_MANY_REQUESTS, 145 Json(json!({ 146 "error": "RateLimitExceeded", 147 "message": "Too many requests. Please try again later." 148 })), 149 ) 150 .into_response(); 151 } 152 let token = input.token.trim(); 153 let password = &input.password; 154 if token.is_empty() { 155 return ( 156 StatusCode::BAD_REQUEST, 157 Json(json!({"error": "InvalidToken", "message": "token is required"})), 158 ) 159 .into_response(); 160 } 161 if password.is_empty() { 162 return ( 163 StatusCode::BAD_REQUEST, 164 Json(json!({"error": "InvalidRequest", "message": "password is required"})), 165 ) 166 .into_response(); 167 } 168 if let Err(e) = validate_password(password) { 169 return ( 170 StatusCode::BAD_REQUEST, 171 Json(json!({ 172 "error": "InvalidPassword", 173 "message": e.to_string() 174 })), 175 ) 176 .into_response(); 177 } 178 let user = sqlx::query!( 179 "SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1", 180 token 181 ) 182 .fetch_optional(&state.db) 183 .await; 184 let (user_id, expires_at) = match user { 185 Ok(Some(row)) => { 186 let expires = row.password_reset_code_expires_at; 187 (row.id, expires) 188 } 189 Ok(None) => { 190 return ( 191 StatusCode::BAD_REQUEST, 192 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 193 ) 194 .into_response(); 195 } 196 Err(e) => { 197 error!("DB error in reset_password: {:?}", e); 198 return ( 199 StatusCode::INTERNAL_SERVER_ERROR, 200 Json(json!({"error": "InternalError"})), 201 ) 202 .into_response(); 203 } 204 }; 205 if let Some(exp) = expires_at { 206 if Utc::now() > exp { 207 if let Err(e) = sqlx::query!( 208 "UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1", 209 user_id 210 ) 211 .execute(&state.db) 212 .await 213 { 214 error!("Failed to clear expired reset code: {:?}", e); 215 } 216 return ( 217 StatusCode::BAD_REQUEST, 218 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 219 ) 220 .into_response(); 221 } 222 } else { 223 return ( 224 StatusCode::BAD_REQUEST, 225 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 226 ) 227 .into_response(); 228 } 229 let password_clone = password.to_string(); 230 let password_hash = 231 match tokio::task::spawn_blocking(move || hash(password_clone, DEFAULT_COST)).await { 232 Ok(Ok(h)) => h, 233 Ok(Err(e)) => { 234 error!("Failed to hash password: {:?}", e); 235 return ( 236 StatusCode::INTERNAL_SERVER_ERROR, 237 Json(json!({"error": "InternalError"})), 238 ) 239 .into_response(); 240 } 241 Err(e) => { 242 error!("Failed to spawn blocking task: {:?}", e); 243 return ( 244 StatusCode::INTERNAL_SERVER_ERROR, 245 Json(json!({"error": "InternalError"})), 246 ) 247 .into_response(); 248 } 249 }; 250 let mut tx = match state.db.begin().await { 251 Ok(tx) => tx, 252 Err(e) => { 253 error!("Failed to begin transaction: {:?}", e); 254 return ( 255 StatusCode::INTERNAL_SERVER_ERROR, 256 Json(json!({"error": "InternalError"})), 257 ) 258 .into_response(); 259 } 260 }; 261 if let Err(e) = sqlx::query!( 262 "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL, password_required = TRUE WHERE id = $2", 263 password_hash, 264 user_id 265 ) 266 .execute(&mut *tx) 267 .await 268 { 269 error!("DB error updating password: {:?}", e); 270 return ( 271 StatusCode::INTERNAL_SERVER_ERROR, 272 Json(json!({"error": "InternalError"})), 273 ) 274 .into_response(); 275 } 276 let user_did = match sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", user_id) 277 .fetch_one(&mut *tx) 278 .await 279 { 280 Ok(did) => did, 281 Err(e) => { 282 error!("Failed to get DID for user {}: {:?}", user_id, e); 283 return ( 284 StatusCode::INTERNAL_SERVER_ERROR, 285 Json(json!({"error": "InternalError"})), 286 ) 287 .into_response(); 288 } 289 }; 290 let session_jtis: Vec<String> = match sqlx::query_scalar!( 291 "SELECT access_jti FROM session_tokens WHERE did = $1", 292 user_did 293 ) 294 .fetch_all(&mut *tx) 295 .await 296 { 297 Ok(jtis) => jtis, 298 Err(e) => { 299 error!("Failed to fetch session JTIs: {:?}", e); 300 vec![] 301 } 302 }; 303 if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = $1", user_did) 304 .execute(&mut *tx) 305 .await 306 { 307 error!( 308 "Failed to invalidate sessions after password reset: {:?}", 309 e 310 ); 311 return ( 312 StatusCode::INTERNAL_SERVER_ERROR, 313 Json(json!({"error": "InternalError"})), 314 ) 315 .into_response(); 316 } 317 if let Err(e) = tx.commit().await { 318 error!("Failed to commit password reset transaction: {:?}", e); 319 return ( 320 StatusCode::INTERNAL_SERVER_ERROR, 321 Json(json!({"error": "InternalError"})), 322 ) 323 .into_response(); 324 } 325 for jti in session_jtis { 326 let cache_key = format!("auth:session:{}:{}", user_did, jti); 327 if let Err(e) = state.cache.delete(&cache_key).await { 328 warn!( 329 "Failed to invalidate session cache for {}: {:?}", 330 cache_key, e 331 ); 332 } 333 } 334 info!("Password reset completed for user {}", user_id); 335 (StatusCode::OK, Json(json!({}))).into_response() 336} 337 338#[derive(Deserialize)] 339#[serde(rename_all = "camelCase")] 340pub struct ChangePasswordInput { 341 pub current_password: String, 342 pub new_password: String, 343} 344 345pub async fn change_password( 346 State(state): State<AppState>, 347 auth: BearerAuth, 348 Json(input): Json<ChangePasswordInput>, 349) -> Response { 350 if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, &auth.0.did).await { 351 return crate::api::server::reauth::legacy_mfa_required_response(&state.db, &auth.0.did) 352 .await; 353 } 354 355 let current_password = &input.current_password; 356 let new_password = &input.new_password; 357 if current_password.is_empty() { 358 return ( 359 StatusCode::BAD_REQUEST, 360 Json(json!({"error": "InvalidRequest", "message": "currentPassword is required"})), 361 ) 362 .into_response(); 363 } 364 if new_password.is_empty() { 365 return ( 366 StatusCode::BAD_REQUEST, 367 Json(json!({"error": "InvalidRequest", "message": "newPassword is required"})), 368 ) 369 .into_response(); 370 } 371 if let Err(e) = validate_password(new_password) { 372 return ( 373 StatusCode::BAD_REQUEST, 374 Json(json!({ 375 "error": "InvalidPassword", 376 "message": e.to_string() 377 })), 378 ) 379 .into_response(); 380 } 381 let user = 382 sqlx::query_as::<_, (Uuid, String)>("SELECT id, password_hash FROM users WHERE did = $1") 383 .bind(&auth.0.did) 384 .fetch_optional(&state.db) 385 .await; 386 let (user_id, password_hash) = match user { 387 Ok(Some(row)) => row, 388 Ok(None) => { 389 return ( 390 StatusCode::NOT_FOUND, 391 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 392 ) 393 .into_response(); 394 } 395 Err(e) => { 396 error!("DB error in change_password: {:?}", e); 397 return ( 398 StatusCode::INTERNAL_SERVER_ERROR, 399 Json(json!({"error": "InternalError"})), 400 ) 401 .into_response(); 402 } 403 }; 404 let valid = match verify(current_password, &password_hash) { 405 Ok(v) => v, 406 Err(e) => { 407 error!("Password verification error: {:?}", e); 408 return ( 409 StatusCode::INTERNAL_SERVER_ERROR, 410 Json(json!({"error": "InternalError"})), 411 ) 412 .into_response(); 413 } 414 }; 415 if !valid { 416 return ( 417 StatusCode::UNAUTHORIZED, 418 Json(json!({"error": "InvalidPassword", "message": "Current password is incorrect"})), 419 ) 420 .into_response(); 421 } 422 let new_password_clone = new_password.to_string(); 423 let new_hash = 424 match tokio::task::spawn_blocking(move || hash(new_password_clone, DEFAULT_COST)).await { 425 Ok(Ok(h)) => h, 426 Ok(Err(e)) => { 427 error!("Failed to hash password: {:?}", e); 428 return ( 429 StatusCode::INTERNAL_SERVER_ERROR, 430 Json(json!({"error": "InternalError"})), 431 ) 432 .into_response(); 433 } 434 Err(e) => { 435 error!("Failed to spawn blocking task: {:?}", e); 436 return ( 437 StatusCode::INTERNAL_SERVER_ERROR, 438 Json(json!({"error": "InternalError"})), 439 ) 440 .into_response(); 441 } 442 }; 443 if let Err(e) = sqlx::query("UPDATE users SET password_hash = $1 WHERE id = $2") 444 .bind(&new_hash) 445 .bind(user_id) 446 .execute(&state.db) 447 .await 448 { 449 error!("DB error updating password: {:?}", e); 450 return ( 451 StatusCode::INTERNAL_SERVER_ERROR, 452 Json(json!({"error": "InternalError"})), 453 ) 454 .into_response(); 455 } 456 info!(did = %auth.0.did, "Password changed successfully"); 457 (StatusCode::OK, Json(json!({}))).into_response() 458} 459 460pub async fn get_password_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 461 let user = sqlx::query!( 462 "SELECT password_hash IS NOT NULL as has_password FROM users WHERE did = $1", 463 auth.0.did 464 ) 465 .fetch_optional(&state.db) 466 .await; 467 468 match user { 469 Ok(Some(row)) => { 470 Json(json!({"hasPassword": row.has_password.unwrap_or(false)})).into_response() 471 } 472 Ok(None) => ( 473 StatusCode::NOT_FOUND, 474 Json(json!({"error": "AccountNotFound"})), 475 ) 476 .into_response(), 477 Err(e) => { 478 error!("DB error: {:?}", e); 479 ( 480 StatusCode::INTERNAL_SERVER_ERROR, 481 Json(json!({"error": "InternalError"})), 482 ) 483 .into_response() 484 } 485 } 486} 487 488pub async fn remove_password(State(state): State<AppState>, auth: BearerAuth) -> Response { 489 if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, &auth.0.did).await { 490 return crate::api::server::reauth::legacy_mfa_required_response(&state.db, &auth.0.did) 491 .await; 492 } 493 494 if crate::api::server::reauth::check_reauth_required_cached( 495 &state.db, 496 &state.cache, 497 &auth.0.did, 498 ) 499 .await 500 { 501 return crate::api::server::reauth::reauth_required_response(&state.db, &auth.0.did).await; 502 } 503 504 let has_passkeys = 505 crate::api::server::passkeys::has_passkeys_for_user_db(&state.db, &auth.0.did).await; 506 if !has_passkeys { 507 return ( 508 StatusCode::BAD_REQUEST, 509 Json(json!({ 510 "error": "NoPasskeys", 511 "message": "You must have at least one passkey registered before removing your password" 512 })), 513 ) 514 .into_response(); 515 } 516 517 let user = sqlx::query!( 518 "SELECT id, password_hash FROM users WHERE did = $1", 519 auth.0.did 520 ) 521 .fetch_optional(&state.db) 522 .await; 523 524 let user = match user { 525 Ok(Some(u)) => u, 526 Ok(None) => { 527 return ( 528 StatusCode::NOT_FOUND, 529 Json(json!({"error": "AccountNotFound"})), 530 ) 531 .into_response(); 532 } 533 Err(e) => { 534 error!("DB error: {:?}", e); 535 return ( 536 StatusCode::INTERNAL_SERVER_ERROR, 537 Json(json!({"error": "InternalError"})), 538 ) 539 .into_response(); 540 } 541 }; 542 543 if user.password_hash.is_none() { 544 return ( 545 StatusCode::BAD_REQUEST, 546 Json(json!({ 547 "error": "NoPassword", 548 "message": "Account already has no password" 549 })), 550 ) 551 .into_response(); 552 } 553 554 if let Err(e) = sqlx::query!( 555 "UPDATE users SET password_hash = NULL, password_required = FALSE WHERE id = $1", 556 user.id 557 ) 558 .execute(&state.db) 559 .await 560 { 561 error!("DB error removing password: {:?}", e); 562 return ( 563 StatusCode::INTERNAL_SERVER_ERROR, 564 Json(json!({"error": "InternalError"})), 565 ) 566 .into_response(); 567 } 568 569 info!(did = %auth.0.did, "Password removed - account is now passkey-only"); 570 (StatusCode::OK, Json(json!({"success": true}))).into_response() 571}