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}