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_hash = match hash(password, DEFAULT_COST) { 230 Ok(h) => h, 231 Err(e) => { 232 error!("Failed to hash password: {:?}", e); 233 return ( 234 StatusCode::INTERNAL_SERVER_ERROR, 235 Json(json!({"error": "InternalError"})), 236 ) 237 .into_response(); 238 } 239 }; 240 let mut tx = match state.db.begin().await { 241 Ok(tx) => tx, 242 Err(e) => { 243 error!("Failed to begin transaction: {:?}", e); 244 return ( 245 StatusCode::INTERNAL_SERVER_ERROR, 246 Json(json!({"error": "InternalError"})), 247 ) 248 .into_response(); 249 } 250 }; 251 if let Err(e) = sqlx::query!( 252 "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL, password_required = TRUE WHERE id = $2", 253 password_hash, 254 user_id 255 ) 256 .execute(&mut *tx) 257 .await 258 { 259 error!("DB error updating password: {:?}", e); 260 return ( 261 StatusCode::INTERNAL_SERVER_ERROR, 262 Json(json!({"error": "InternalError"})), 263 ) 264 .into_response(); 265 } 266 let user_did = match sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", user_id) 267 .fetch_one(&mut *tx) 268 .await 269 { 270 Ok(did) => did, 271 Err(e) => { 272 error!("Failed to get DID for user {}: {:?}", user_id, e); 273 return ( 274 StatusCode::INTERNAL_SERVER_ERROR, 275 Json(json!({"error": "InternalError"})), 276 ) 277 .into_response(); 278 } 279 }; 280 let session_jtis: Vec<String> = match sqlx::query_scalar!( 281 "SELECT access_jti FROM session_tokens WHERE did = $1", 282 user_did 283 ) 284 .fetch_all(&mut *tx) 285 .await 286 { 287 Ok(jtis) => jtis, 288 Err(e) => { 289 error!("Failed to fetch session JTIs: {:?}", e); 290 vec![] 291 } 292 }; 293 if let Err(e) = sqlx::query!("DELETE FROM session_tokens WHERE did = $1", user_did) 294 .execute(&mut *tx) 295 .await 296 { 297 error!( 298 "Failed to invalidate sessions after password reset: {:?}", 299 e 300 ); 301 return ( 302 StatusCode::INTERNAL_SERVER_ERROR, 303 Json(json!({"error": "InternalError"})), 304 ) 305 .into_response(); 306 } 307 if let Err(e) = tx.commit().await { 308 error!("Failed to commit password reset transaction: {:?}", e); 309 return ( 310 StatusCode::INTERNAL_SERVER_ERROR, 311 Json(json!({"error": "InternalError"})), 312 ) 313 .into_response(); 314 } 315 for jti in session_jtis { 316 let cache_key = format!("auth:session:{}:{}", user_did, jti); 317 if let Err(e) = state.cache.delete(&cache_key).await { 318 warn!( 319 "Failed to invalidate session cache for {}: {:?}", 320 cache_key, e 321 ); 322 } 323 } 324 info!("Password reset completed for user {}", user_id); 325 (StatusCode::OK, Json(json!({}))).into_response() 326} 327 328#[derive(Deserialize)] 329#[serde(rename_all = "camelCase")] 330pub struct ChangePasswordInput { 331 pub current_password: String, 332 pub new_password: String, 333} 334 335pub async fn change_password( 336 State(state): State<AppState>, 337 auth: BearerAuth, 338 Json(input): Json<ChangePasswordInput>, 339) -> Response { 340 if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, &auth.0.did).await { 341 return crate::api::server::reauth::legacy_mfa_required_response(&state.db, &auth.0.did) 342 .await; 343 } 344 345 let current_password = &input.current_password; 346 let new_password = &input.new_password; 347 if current_password.is_empty() { 348 return ( 349 StatusCode::BAD_REQUEST, 350 Json(json!({"error": "InvalidRequest", "message": "currentPassword is required"})), 351 ) 352 .into_response(); 353 } 354 if new_password.is_empty() { 355 return ( 356 StatusCode::BAD_REQUEST, 357 Json(json!({"error": "InvalidRequest", "message": "newPassword is required"})), 358 ) 359 .into_response(); 360 } 361 if let Err(e) = validate_password(new_password) { 362 return ( 363 StatusCode::BAD_REQUEST, 364 Json(json!({ 365 "error": "InvalidPassword", 366 "message": e.to_string() 367 })), 368 ) 369 .into_response(); 370 } 371 let user = 372 sqlx::query_as::<_, (Uuid, String)>("SELECT id, password_hash FROM users WHERE did = $1") 373 .bind(&auth.0.did) 374 .fetch_optional(&state.db) 375 .await; 376 let (user_id, password_hash) = match user { 377 Ok(Some(row)) => row, 378 Ok(None) => { 379 return ( 380 StatusCode::NOT_FOUND, 381 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 382 ) 383 .into_response(); 384 } 385 Err(e) => { 386 error!("DB error in change_password: {:?}", e); 387 return ( 388 StatusCode::INTERNAL_SERVER_ERROR, 389 Json(json!({"error": "InternalError"})), 390 ) 391 .into_response(); 392 } 393 }; 394 let valid = match verify(current_password, &password_hash) { 395 Ok(v) => v, 396 Err(e) => { 397 error!("Password verification error: {:?}", e); 398 return ( 399 StatusCode::INTERNAL_SERVER_ERROR, 400 Json(json!({"error": "InternalError"})), 401 ) 402 .into_response(); 403 } 404 }; 405 if !valid { 406 return ( 407 StatusCode::UNAUTHORIZED, 408 Json(json!({"error": "InvalidPassword", "message": "Current password is incorrect"})), 409 ) 410 .into_response(); 411 } 412 let new_hash = match hash(new_password, DEFAULT_COST) { 413 Ok(h) => h, 414 Err(e) => { 415 error!("Failed to hash password: {:?}", e); 416 return ( 417 StatusCode::INTERNAL_SERVER_ERROR, 418 Json(json!({"error": "InternalError"})), 419 ) 420 .into_response(); 421 } 422 }; 423 if let Err(e) = sqlx::query("UPDATE users SET password_hash = $1 WHERE id = $2") 424 .bind(&new_hash) 425 .bind(user_id) 426 .execute(&state.db) 427 .await 428 { 429 error!("DB error updating password: {:?}", e); 430 return ( 431 StatusCode::INTERNAL_SERVER_ERROR, 432 Json(json!({"error": "InternalError"})), 433 ) 434 .into_response(); 435 } 436 info!(did = %auth.0.did, "Password changed successfully"); 437 (StatusCode::OK, Json(json!({}))).into_response() 438} 439 440pub async fn get_password_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 441 let user = sqlx::query!( 442 "SELECT password_hash IS NOT NULL as has_password FROM users WHERE did = $1", 443 auth.0.did 444 ) 445 .fetch_optional(&state.db) 446 .await; 447 448 match user { 449 Ok(Some(row)) => { 450 Json(json!({"hasPassword": row.has_password.unwrap_or(false)})).into_response() 451 } 452 Ok(None) => ( 453 StatusCode::NOT_FOUND, 454 Json(json!({"error": "AccountNotFound"})), 455 ) 456 .into_response(), 457 Err(e) => { 458 error!("DB error: {:?}", e); 459 ( 460 StatusCode::INTERNAL_SERVER_ERROR, 461 Json(json!({"error": "InternalError"})), 462 ) 463 .into_response() 464 } 465 } 466} 467 468pub async fn remove_password(State(state): State<AppState>, auth: BearerAuth) -> Response { 469 if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, &auth.0.did).await { 470 return crate::api::server::reauth::legacy_mfa_required_response(&state.db, &auth.0.did) 471 .await; 472 } 473 474 if crate::api::server::reauth::check_reauth_required_cached( 475 &state.db, 476 &state.cache, 477 &auth.0.did, 478 ) 479 .await 480 { 481 return crate::api::server::reauth::reauth_required_response(&state.db, &auth.0.did).await; 482 } 483 484 let has_passkeys = 485 crate::api::server::passkeys::has_passkeys_for_user_db(&state.db, &auth.0.did).await; 486 if !has_passkeys { 487 return ( 488 StatusCode::BAD_REQUEST, 489 Json(json!({ 490 "error": "NoPasskeys", 491 "message": "You must have at least one passkey registered before removing your password" 492 })), 493 ) 494 .into_response(); 495 } 496 497 let user = sqlx::query!( 498 "SELECT id, password_hash FROM users WHERE did = $1", 499 auth.0.did 500 ) 501 .fetch_optional(&state.db) 502 .await; 503 504 let user = match user { 505 Ok(Some(u)) => u, 506 Ok(None) => { 507 return ( 508 StatusCode::NOT_FOUND, 509 Json(json!({"error": "AccountNotFound"})), 510 ) 511 .into_response(); 512 } 513 Err(e) => { 514 error!("DB error: {:?}", e); 515 return ( 516 StatusCode::INTERNAL_SERVER_ERROR, 517 Json(json!({"error": "InternalError"})), 518 ) 519 .into_response(); 520 } 521 }; 522 523 if user.password_hash.is_none() { 524 return ( 525 StatusCode::BAD_REQUEST, 526 Json(json!({ 527 "error": "NoPassword", 528 "message": "Account already has no password" 529 })), 530 ) 531 .into_response(); 532 } 533 534 if let Err(e) = sqlx::query!( 535 "UPDATE users SET password_hash = NULL, password_required = FALSE WHERE id = $1", 536 user.id 537 ) 538 .execute(&state.db) 539 .await 540 { 541 error!("DB error removing password: {:?}", e); 542 return ( 543 StatusCode::INTERNAL_SERVER_ERROR, 544 Json(json!({"error": "InternalError"})), 545 ) 546 .into_response(); 547 } 548 549 info!(did = %auth.0.did, "Password removed - account is now passkey-only"); 550 (StatusCode::OK, Json(json!({"success": true}))).into_response() 551}