this repo has no description
1use crate::api::ApiError; 2use crate::auth::BearerAuth; 3use crate::state::AppState; 4use axum::{ 5 Json, 6 extract::State, 7 http::{HeaderMap, StatusCode}, 8 response::{IntoResponse, Response}, 9}; 10use bcrypt::verify; 11use serde::{Deserialize, Serialize}; 12use serde_json::json; 13use tracing::{error, info, warn}; 14 15fn extract_client_ip(headers: &HeaderMap) -> String { 16 if let Some(forwarded) = headers.get("x-forwarded-for") { 17 if let Ok(value) = forwarded.to_str() { 18 if let Some(first_ip) = value.split(',').next() { 19 return first_ip.trim().to_string(); 20 } 21 } 22 } 23 if let Some(real_ip) = headers.get("x-real-ip") { 24 if let Ok(value) = real_ip.to_str() { 25 return value.trim().to_string(); 26 } 27 } 28 "unknown".to_string() 29} 30 31#[derive(Deserialize)] 32pub struct CreateSessionInput { 33 pub identifier: String, 34 pub password: String, 35} 36 37#[derive(Serialize)] 38#[serde(rename_all = "camelCase")] 39pub struct CreateSessionOutput { 40 pub access_jwt: String, 41 pub refresh_jwt: String, 42 pub handle: String, 43 pub did: String, 44} 45 46pub async fn create_session( 47 State(state): State<AppState>, 48 headers: HeaderMap, 49 Json(input): Json<CreateSessionInput>, 50) -> Response { 51 info!("create_session called"); 52 53 let client_ip = extract_client_ip(&headers); 54 if state.rate_limiters.login.check_key(&client_ip).is_err() { 55 warn!(ip = %client_ip, "Login rate limit exceeded"); 56 return ( 57 StatusCode::TOO_MANY_REQUESTS, 58 Json(json!({ 59 "error": "RateLimitExceeded", 60 "message": "Too many login attempts. Please try again later." 61 })), 62 ) 63 .into_response(); 64 } 65 66 let row = match sqlx::query!( 67 "SELECT u.id, u.did, u.handle, u.password_hash, k.key_bytes, k.encryption_version FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.handle = $1 OR u.email = $1", 68 input.identifier 69 ) 70 .fetch_optional(&state.db) 71 .await 72 { 73 Ok(Some(row)) => row, 74 Ok(None) => { 75 let _ = verify(&input.password, "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK"); 76 warn!("User not found for login attempt"); 77 return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into()).into_response(); 78 } 79 Err(e) => { 80 error!("Database error fetching user: {:?}", e); 81 return ApiError::InternalError.into_response(); 82 } 83 }; 84 85 let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 86 Ok(k) => k, 87 Err(e) => { 88 error!("Failed to decrypt user key: {:?}", e); 89 return ApiError::InternalError.into_response(); 90 } 91 }; 92 93 let password_valid = verify(&input.password, &row.password_hash).unwrap_or(false) 94 || sqlx::query!("SELECT password_hash FROM app_passwords WHERE user_id = $1", row.id) 95 .fetch_all(&state.db) 96 .await 97 .unwrap_or_default() 98 .iter() 99 .any(|app| verify(&input.password, &app.password_hash).unwrap_or(false)); 100 101 if !password_valid { 102 warn!("Password verification failed for login attempt"); 103 return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into()).into_response(); 104 } 105 106 let access_meta = match crate::auth::create_access_token_with_metadata(&row.did, &key_bytes) { 107 Ok(m) => m, 108 Err(e) => { 109 error!("Failed to create access token: {:?}", e); 110 return ApiError::InternalError.into_response(); 111 } 112 }; 113 114 let refresh_meta = match crate::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) { 115 Ok(m) => m, 116 Err(e) => { 117 error!("Failed to create refresh token: {:?}", e); 118 return ApiError::InternalError.into_response(); 119 } 120 }; 121 122 if let Err(e) = sqlx::query!( 123 "INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)", 124 row.did, 125 access_meta.jti, 126 refresh_meta.jti, 127 access_meta.expires_at, 128 refresh_meta.expires_at 129 ) 130 .execute(&state.db) 131 .await 132 { 133 error!("Failed to insert session: {:?}", e); 134 return ApiError::InternalError.into_response(); 135 } 136 137 Json(CreateSessionOutput { 138 access_jwt: access_meta.token, 139 refresh_jwt: refresh_meta.token, 140 handle: row.handle, 141 did: row.did, 142 }).into_response() 143} 144 145pub async fn get_session( 146 State(state): State<AppState>, 147 BearerAuth(auth_user): BearerAuth, 148) -> Response { 149 match sqlx::query!("SELECT handle, email FROM users WHERE did = $1", auth_user.did) 150 .fetch_optional(&state.db) 151 .await 152 { 153 Ok(Some(row)) => Json(json!({ 154 "handle": row.handle, 155 "did": auth_user.did, 156 "email": row.email, 157 "didDoc": {} 158 })).into_response(), 159 Ok(None) => ApiError::AuthenticationFailed.into_response(), 160 Err(e) => { 161 error!("Database error in get_session: {:?}", e); 162 ApiError::InternalError.into_response() 163 } 164 } 165} 166 167pub async fn delete_session( 168 State(state): State<AppState>, 169 headers: axum::http::HeaderMap, 170) -> Response { 171 let token = match crate::auth::extract_bearer_token_from_header( 172 headers.get("Authorization").and_then(|h| h.to_str().ok()) 173 ) { 174 Some(t) => t, 175 None => return ApiError::AuthenticationRequired.into_response(), 176 }; 177 178 let jti = match crate::auth::get_jti_from_token(&token) { 179 Ok(jti) => jti, 180 Err(_) => return ApiError::AuthenticationFailed.into_response(), 181 }; 182 183 match sqlx::query!("DELETE FROM session_tokens WHERE access_jti = $1", jti) 184 .execute(&state.db) 185 .await 186 { 187 Ok(res) if res.rows_affected() > 0 => Json(json!({})).into_response(), 188 Ok(_) => ApiError::AuthenticationFailed.into_response(), 189 Err(e) => { 190 error!("Database error in delete_session: {:?}", e); 191 ApiError::AuthenticationFailed.into_response() 192 } 193 } 194} 195 196pub async fn refresh_session( 197 State(state): State<AppState>, 198 headers: axum::http::HeaderMap, 199) -> Response { 200 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 201 if !state.distributed_rate_limiter.check_rate_limit( 202 &format!("refresh_session:{}", client_ip), 203 60, 204 60_000, 205 ).await { 206 if state.rate_limiters.refresh_session.check_key(&client_ip).is_err() { 207 tracing::warn!(ip = %client_ip, "Refresh session rate limit exceeded"); 208 return ( 209 axum::http::StatusCode::TOO_MANY_REQUESTS, 210 axum::Json(serde_json::json!({ 211 "error": "RateLimitExceeded", 212 "message": "Too many requests. Please try again later." 213 })), 214 ).into_response(); 215 } 216 } 217 218 let refresh_token = match crate::auth::extract_bearer_token_from_header( 219 headers.get("Authorization").and_then(|h| h.to_str().ok()) 220 ) { 221 Some(t) => t, 222 None => return ApiError::AuthenticationRequired.into_response(), 223 }; 224 225 let refresh_jti = match crate::auth::get_jti_from_token(&refresh_token) { 226 Ok(jti) => jti, 227 Err(_) => return ApiError::AuthenticationFailedMsg("Invalid token format".into()).into_response(), 228 }; 229 230 let mut tx = match state.db.begin().await { 231 Ok(tx) => tx, 232 Err(e) => { 233 error!("Failed to begin transaction: {:?}", e); 234 return ApiError::InternalError.into_response(); 235 } 236 }; 237 238 if let Ok(Some(session_id)) = sqlx::query_scalar!( 239 "SELECT session_id FROM used_refresh_tokens WHERE refresh_jti = $1 FOR UPDATE", 240 refresh_jti 241 ) 242 .fetch_optional(&mut *tx) 243 .await 244 { 245 warn!("Refresh token reuse detected! Revoking token family for session_id: {}", session_id); 246 let _ = sqlx::query!("DELETE FROM session_tokens WHERE id = $1", session_id) 247 .execute(&mut *tx) 248 .await; 249 let _ = tx.commit().await; 250 return ApiError::ExpiredTokenMsg("Refresh token has been revoked due to suspected compromise".into()).into_response(); 251 } 252 253 let session_row = match sqlx::query!( 254 r#"SELECT st.id, st.did, k.key_bytes, k.encryption_version 255 FROM session_tokens st 256 JOIN users u ON st.did = u.did 257 JOIN user_keys k ON u.id = k.user_id 258 WHERE st.refresh_jti = $1 AND st.refresh_expires_at > NOW() 259 FOR UPDATE OF st"#, 260 refresh_jti 261 ) 262 .fetch_optional(&mut *tx) 263 .await 264 { 265 Ok(Some(row)) => row, 266 Ok(None) => return ApiError::AuthenticationFailedMsg("Invalid refresh token".into()).into_response(), 267 Err(e) => { 268 error!("Database error fetching session: {:?}", e); 269 return ApiError::InternalError.into_response(); 270 } 271 }; 272 273 let key_bytes = match crate::config::decrypt_key(&session_row.key_bytes, session_row.encryption_version) { 274 Ok(k) => k, 275 Err(e) => { 276 error!("Failed to decrypt user key: {:?}", e); 277 return ApiError::InternalError.into_response(); 278 } 279 }; 280 281 if crate::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() { 282 return ApiError::AuthenticationFailedMsg("Invalid refresh token".into()).into_response(); 283 } 284 285 let new_access_meta = match crate::auth::create_access_token_with_metadata(&session_row.did, &key_bytes) { 286 Ok(m) => m, 287 Err(e) => { 288 error!("Failed to create access token: {:?}", e); 289 return ApiError::InternalError.into_response(); 290 } 291 }; 292 293 let new_refresh_meta = match crate::auth::create_refresh_token_with_metadata(&session_row.did, &key_bytes) { 294 Ok(m) => m, 295 Err(e) => { 296 error!("Failed to create refresh token: {:?}", e); 297 return ApiError::InternalError.into_response(); 298 } 299 }; 300 301 match sqlx::query!( 302 "INSERT INTO used_refresh_tokens (refresh_jti, session_id) VALUES ($1, $2) ON CONFLICT (refresh_jti) DO NOTHING", 303 refresh_jti, 304 session_row.id 305 ) 306 .execute(&mut *tx) 307 .await 308 { 309 Ok(result) if result.rows_affected() == 0 => { 310 warn!("Concurrent refresh token reuse detected for session_id: {}", session_row.id); 311 let _ = sqlx::query!("DELETE FROM session_tokens WHERE id = $1", session_row.id) 312 .execute(&mut *tx) 313 .await; 314 let _ = tx.commit().await; 315 return ApiError::ExpiredTokenMsg("Refresh token has been revoked due to suspected compromise".into()).into_response(); 316 } 317 Err(e) => { 318 error!("Failed to record used refresh token: {:?}", e); 319 return ApiError::InternalError.into_response(); 320 } 321 Ok(_) => {} 322 } 323 324 if let Err(e) = sqlx::query!( 325 "UPDATE session_tokens SET access_jti = $1, refresh_jti = $2, access_expires_at = $3, refresh_expires_at = $4, updated_at = NOW() WHERE id = $5", 326 new_access_meta.jti, 327 new_refresh_meta.jti, 328 new_access_meta.expires_at, 329 new_refresh_meta.expires_at, 330 session_row.id 331 ) 332 .execute(&mut *tx) 333 .await 334 { 335 error!("Database error updating session: {:?}", e); 336 return ApiError::InternalError.into_response(); 337 } 338 339 if let Err(e) = tx.commit().await { 340 error!("Failed to commit transaction: {:?}", e); 341 return ApiError::InternalError.into_response(); 342 } 343 344 match sqlx::query!("SELECT handle FROM users WHERE did = $1", session_row.did) 345 .fetch_optional(&state.db) 346 .await 347 { 348 Ok(Some(u)) => Json(json!({ 349 "accessJwt": new_access_meta.token, 350 "refreshJwt": new_refresh_meta.token, 351 "handle": u.handle, 352 "did": session_row.did 353 })).into_response(), 354 Ok(None) => { 355 error!("User not found for existing session: {}", session_row.did); 356 ApiError::InternalError.into_response() 357 } 358 Err(e) => { 359 error!("Database error fetching user: {:?}", e); 360 ApiError::InternalError.into_response() 361 } 362 } 363}