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