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