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}