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 warn!("User not found for login attempt"); 76 return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into()).into_response(); 77 } 78 Err(e) => { 79 error!("Database error fetching user: {:?}", e); 80 return ApiError::InternalError.into_response(); 81 } 82 }; 83 84 let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 85 Ok(k) => k, 86 Err(e) => { 87 error!("Failed to decrypt user key: {:?}", e); 88 return ApiError::InternalError.into_response(); 89 } 90 }; 91 92 let password_valid = verify(&input.password, &row.password_hash).unwrap_or(false) 93 || sqlx::query!("SELECT password_hash FROM app_passwords WHERE user_id = $1", row.id) 94 .fetch_all(&state.db) 95 .await 96 .unwrap_or_default() 97 .iter() 98 .any(|app| verify(&input.password, &app.password_hash).unwrap_or(false)); 99 100 if !password_valid { 101 warn!("Password verification failed for login attempt"); 102 return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into()).into_response(); 103 } 104 105 let access_meta = match crate::auth::create_access_token_with_metadata(&row.did, &key_bytes) { 106 Ok(m) => m, 107 Err(e) => { 108 error!("Failed to create access token: {:?}", e); 109 return ApiError::InternalError.into_response(); 110 } 111 }; 112 113 let refresh_meta = match crate::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) { 114 Ok(m) => m, 115 Err(e) => { 116 error!("Failed to create refresh token: {:?}", e); 117 return ApiError::InternalError.into_response(); 118 } 119 }; 120 121 if let Err(e) = sqlx::query!( 122 "INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)", 123 row.did, 124 access_meta.jti, 125 refresh_meta.jti, 126 access_meta.expires_at, 127 refresh_meta.expires_at 128 ) 129 .execute(&state.db) 130 .await 131 { 132 error!("Failed to insert session: {:?}", e); 133 return ApiError::InternalError.into_response(); 134 } 135 136 Json(CreateSessionOutput { 137 access_jwt: access_meta.token, 138 refresh_jwt: refresh_meta.token, 139 handle: row.handle, 140 did: row.did, 141 }).into_response() 142} 143 144pub async fn get_session( 145 State(state): State<AppState>, 146 BearerAuth(auth_user): BearerAuth, 147) -> Response { 148 match sqlx::query!("SELECT handle, email FROM users WHERE did = $1", auth_user.did) 149 .fetch_optional(&state.db) 150 .await 151 { 152 Ok(Some(row)) => Json(json!({ 153 "handle": row.handle, 154 "did": auth_user.did, 155 "email": row.email, 156 "didDoc": {} 157 })).into_response(), 158 Ok(None) => ApiError::AuthenticationFailed.into_response(), 159 Err(e) => { 160 error!("Database error in get_session: {:?}", e); 161 ApiError::InternalError.into_response() 162 } 163 } 164} 165 166pub async fn delete_session( 167 State(state): State<AppState>, 168 headers: axum::http::HeaderMap, 169) -> Response { 170 let token = match crate::auth::extract_bearer_token_from_header( 171 headers.get("Authorization").and_then(|h| h.to_str().ok()) 172 ) { 173 Some(t) => t, 174 None => return ApiError::AuthenticationRequired.into_response(), 175 }; 176 177 let jti = match crate::auth::get_jti_from_token(&token) { 178 Ok(jti) => jti, 179 Err(_) => return ApiError::AuthenticationFailed.into_response(), 180 }; 181 182 match sqlx::query!("DELETE FROM session_tokens WHERE access_jti = $1", jti) 183 .execute(&state.db) 184 .await 185 { 186 Ok(res) if res.rows_affected() > 0 => Json(json!({})).into_response(), 187 Ok(_) => ApiError::AuthenticationFailed.into_response(), 188 Err(e) => { 189 error!("Database error in delete_session: {:?}", e); 190 ApiError::AuthenticationFailed.into_response() 191 } 192 } 193} 194 195pub async fn refresh_session( 196 State(state): State<AppState>, 197 headers: axum::http::HeaderMap, 198) -> Response { 199 let refresh_token = match crate::auth::extract_bearer_token_from_header( 200 headers.get("Authorization").and_then(|h| h.to_str().ok()) 201 ) { 202 Some(t) => t, 203 None => return ApiError::AuthenticationRequired.into_response(), 204 }; 205 206 let refresh_jti = match crate::auth::get_jti_from_token(&refresh_token) { 207 Ok(jti) => jti, 208 Err(_) => return ApiError::AuthenticationFailedMsg("Invalid token format".into()).into_response(), 209 }; 210 211 let mut tx = match state.db.begin().await { 212 Ok(tx) => tx, 213 Err(e) => { 214 error!("Failed to begin transaction: {:?}", e); 215 return ApiError::InternalError.into_response(); 216 } 217 }; 218 219 if let Ok(Some(session_id)) = sqlx::query_scalar!( 220 "SELECT session_id FROM used_refresh_tokens WHERE refresh_jti = $1 FOR UPDATE", 221 refresh_jti 222 ) 223 .fetch_optional(&mut *tx) 224 .await 225 { 226 warn!("Refresh token reuse detected! Revoking token family for session_id: {}", session_id); 227 let _ = sqlx::query!("DELETE FROM session_tokens WHERE id = $1", session_id) 228 .execute(&mut *tx) 229 .await; 230 let _ = tx.commit().await; 231 return ApiError::ExpiredTokenMsg("Refresh token has been revoked due to suspected compromise".into()).into_response(); 232 } 233 234 let session_row = match sqlx::query!( 235 r#"SELECT st.id, st.did, k.key_bytes, k.encryption_version 236 FROM session_tokens st 237 JOIN users u ON st.did = u.did 238 JOIN user_keys k ON u.id = k.user_id 239 WHERE st.refresh_jti = $1 AND st.refresh_expires_at > NOW() 240 FOR UPDATE OF st"#, 241 refresh_jti 242 ) 243 .fetch_optional(&mut *tx) 244 .await 245 { 246 Ok(Some(row)) => row, 247 Ok(None) => return ApiError::AuthenticationFailedMsg("Invalid refresh token".into()).into_response(), 248 Err(e) => { 249 error!("Database error fetching session: {:?}", e); 250 return ApiError::InternalError.into_response(); 251 } 252 }; 253 254 let key_bytes = match crate::config::decrypt_key(&session_row.key_bytes, session_row.encryption_version) { 255 Ok(k) => k, 256 Err(e) => { 257 error!("Failed to decrypt user key: {:?}", e); 258 return ApiError::InternalError.into_response(); 259 } 260 }; 261 262 if crate::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() { 263 return ApiError::AuthenticationFailedMsg("Invalid refresh token".into()).into_response(); 264 } 265 266 let new_access_meta = match crate::auth::create_access_token_with_metadata(&session_row.did, &key_bytes) { 267 Ok(m) => m, 268 Err(e) => { 269 error!("Failed to create access token: {:?}", e); 270 return ApiError::InternalError.into_response(); 271 } 272 }; 273 274 let new_refresh_meta = match crate::auth::create_refresh_token_with_metadata(&session_row.did, &key_bytes) { 275 Ok(m) => m, 276 Err(e) => { 277 error!("Failed to create refresh token: {:?}", e); 278 return ApiError::InternalError.into_response(); 279 } 280 }; 281 282 match sqlx::query!( 283 "INSERT INTO used_refresh_tokens (refresh_jti, session_id) VALUES ($1, $2) ON CONFLICT (refresh_jti) DO NOTHING", 284 refresh_jti, 285 session_row.id 286 ) 287 .execute(&mut *tx) 288 .await 289 { 290 Ok(result) if result.rows_affected() == 0 => { 291 warn!("Concurrent refresh token reuse detected for session_id: {}", session_row.id); 292 let _ = sqlx::query!("DELETE FROM session_tokens WHERE id = $1", session_row.id) 293 .execute(&mut *tx) 294 .await; 295 let _ = tx.commit().await; 296 return ApiError::ExpiredTokenMsg("Refresh token has been revoked due to suspected compromise".into()).into_response(); 297 } 298 Err(e) => { 299 error!("Failed to record used refresh token: {:?}", e); 300 return ApiError::InternalError.into_response(); 301 } 302 Ok(_) => {} 303 } 304 305 if let Err(e) = sqlx::query!( 306 "UPDATE session_tokens SET access_jti = $1, refresh_jti = $2, access_expires_at = $3, refresh_expires_at = $4, updated_at = NOW() WHERE id = $5", 307 new_access_meta.jti, 308 new_refresh_meta.jti, 309 new_access_meta.expires_at, 310 new_refresh_meta.expires_at, 311 session_row.id 312 ) 313 .execute(&mut *tx) 314 .await 315 { 316 error!("Database error updating session: {:?}", e); 317 return ApiError::InternalError.into_response(); 318 } 319 320 if let Err(e) = tx.commit().await { 321 error!("Failed to commit transaction: {:?}", e); 322 return ApiError::InternalError.into_response(); 323 } 324 325 match sqlx::query!("SELECT handle FROM users WHERE did = $1", session_row.did) 326 .fetch_optional(&state.db) 327 .await 328 { 329 Ok(Some(u)) => Json(json!({ 330 "accessJwt": new_access_meta.token, 331 "refreshJwt": new_refresh_meta.token, 332 "handle": u.handle, 333 "did": session_row.did 334 })).into_response(), 335 Ok(None) => { 336 error!("User not found for existing session: {}", session_row.did); 337 ApiError::InternalError.into_response() 338 } 339 Err(e) => { 340 error!("Database error fetching user: {:?}", e); 341 ApiError::InternalError.into_response() 342 } 343 } 344}