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}