this repo has no description
1use crate::auth::BearerAuth;
2use crate::auth::totp::{
3 decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64,
4 generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format,
5 verify_backup_code, verify_totp_code,
6};
7use crate::state::{AppState, RateLimitKind};
8use axum::{
9 Json,
10 extract::State,
11 http::StatusCode,
12 response::{IntoResponse, Response},
13};
14use chrono::Utc;
15use serde::{Deserialize, Serialize};
16use serde_json::json;
17use tracing::{error, info, warn};
18
19const ENCRYPTION_VERSION: i32 = 1;
20
21#[derive(Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct CreateTotpSecretResponse {
24 pub secret: String,
25 pub uri: String,
26 pub qr_base64: String,
27}
28
29pub async fn create_totp_secret(State(state): State<AppState>, auth: BearerAuth) -> Response {
30 let existing = sqlx::query_scalar!("SELECT verified FROM user_totp WHERE did = $1", auth.0.did)
31 .fetch_optional(&state.db)
32 .await;
33
34 if let Ok(Some(true)) = existing {
35 return (
36 StatusCode::CONFLICT,
37 Json(json!({
38 "error": "TotpAlreadyEnabled",
39 "message": "TOTP is already enabled for this account"
40 })),
41 )
42 .into_response();
43 }
44
45 let secret = generate_totp_secret();
46
47 let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", auth.0.did)
48 .fetch_optional(&state.db)
49 .await;
50
51 let handle = match handle {
52 Ok(Some(h)) => h,
53 Ok(None) => {
54 return (
55 StatusCode::NOT_FOUND,
56 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
57 )
58 .into_response();
59 }
60 Err(e) => {
61 error!("DB error fetching handle: {:?}", e);
62 return (
63 StatusCode::INTERNAL_SERVER_ERROR,
64 Json(json!({"error": "InternalError"})),
65 )
66 .into_response();
67 }
68 };
69
70 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
71 let uri = generate_totp_uri(&secret, &handle, &hostname);
72
73 let qr_code = match generate_qr_png_base64(&secret, &handle, &hostname) {
74 Ok(qr) => qr,
75 Err(e) => {
76 error!("Failed to generate QR code: {:?}", e);
77 return (
78 StatusCode::INTERNAL_SERVER_ERROR,
79 Json(json!({"error": "InternalError", "message": "Failed to generate QR code"})),
80 )
81 .into_response();
82 }
83 };
84
85 let encrypted_secret = match encrypt_totp_secret(&secret) {
86 Ok(enc) => enc,
87 Err(e) => {
88 error!("Failed to encrypt TOTP secret: {:?}", e);
89 return (
90 StatusCode::INTERNAL_SERVER_ERROR,
91 Json(json!({"error": "InternalError"})),
92 )
93 .into_response();
94 }
95 };
96
97 let result = sqlx::query!(
98 r#"
99 INSERT INTO user_totp (did, secret_encrypted, encryption_version, verified, created_at)
100 VALUES ($1, $2, $3, false, NOW())
101 ON CONFLICT (did) DO UPDATE SET
102 secret_encrypted = $2,
103 encryption_version = $3,
104 verified = false,
105 created_at = NOW(),
106 last_used = NULL
107 "#,
108 auth.0.did,
109 encrypted_secret,
110 ENCRYPTION_VERSION
111 )
112 .execute(&state.db)
113 .await;
114
115 if let Err(e) = result {
116 error!("Failed to store TOTP secret: {:?}", e);
117 return (
118 StatusCode::INTERNAL_SERVER_ERROR,
119 Json(json!({"error": "InternalError"})),
120 )
121 .into_response();
122 }
123
124 let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret);
125
126 info!(did = %auth.0.did, "TOTP secret created (pending verification)");
127
128 Json(CreateTotpSecretResponse {
129 secret: secret_base32,
130 uri,
131 qr_base64: qr_code,
132 })
133 .into_response()
134}
135
136#[derive(Deserialize)]
137pub struct EnableTotpInput {
138 pub code: String,
139}
140
141#[derive(Serialize)]
142#[serde(rename_all = "camelCase")]
143pub struct EnableTotpResponse {
144 pub backup_codes: Vec<String>,
145}
146
147pub async fn enable_totp(
148 State(state): State<AppState>,
149 auth: BearerAuth,
150 Json(input): Json<EnableTotpInput>,
151) -> Response {
152 if !state
153 .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did)
154 .await
155 {
156 warn!(did = %auth.0.did, "TOTP verification rate limit exceeded");
157 return (
158 StatusCode::TOO_MANY_REQUESTS,
159 Json(json!({
160 "error": "RateLimitExceeded",
161 "message": "Too many verification attempts. Please try again in a few minutes."
162 })),
163 )
164 .into_response();
165 }
166
167 let totp_row = sqlx::query!(
168 "SELECT secret_encrypted, encryption_version, verified FROM user_totp WHERE did = $1",
169 auth.0.did
170 )
171 .fetch_optional(&state.db)
172 .await;
173
174 let totp_row = match totp_row {
175 Ok(Some(row)) => row,
176 Ok(None) => {
177 return (
178 StatusCode::BAD_REQUEST,
179 Json(json!({
180 "error": "TotpNotSetup",
181 "message": "Please call createTotpSecret first"
182 })),
183 )
184 .into_response();
185 }
186 Err(e) => {
187 error!("DB error fetching TOTP: {:?}", e);
188 return (
189 StatusCode::INTERNAL_SERVER_ERROR,
190 Json(json!({"error": "InternalError"})),
191 )
192 .into_response();
193 }
194 };
195
196 if totp_row.verified {
197 return (
198 StatusCode::CONFLICT,
199 Json(json!({
200 "error": "TotpAlreadyEnabled",
201 "message": "TOTP is already enabled"
202 })),
203 )
204 .into_response();
205 }
206
207 let secret = match decrypt_totp_secret(&totp_row.secret_encrypted, totp_row.encryption_version)
208 {
209 Ok(s) => s,
210 Err(e) => {
211 error!("Failed to decrypt TOTP secret: {:?}", e);
212 return (
213 StatusCode::INTERNAL_SERVER_ERROR,
214 Json(json!({"error": "InternalError"})),
215 )
216 .into_response();
217 }
218 };
219
220 let code = input.code.trim();
221 if !verify_totp_code(&secret, code) {
222 return (
223 StatusCode::UNAUTHORIZED,
224 Json(json!({
225 "error": "InvalidCode",
226 "message": "Invalid verification code"
227 })),
228 )
229 .into_response();
230 }
231
232 let backup_codes = generate_backup_codes();
233 let mut tx = match state.db.begin().await {
234 Ok(tx) => tx,
235 Err(e) => {
236 error!("Failed to begin transaction: {:?}", e);
237 return (
238 StatusCode::INTERNAL_SERVER_ERROR,
239 Json(json!({"error": "InternalError"})),
240 )
241 .into_response();
242 }
243 };
244
245 if let Err(e) = sqlx::query!(
246 "UPDATE user_totp SET verified = true, last_used = NOW() WHERE did = $1",
247 auth.0.did
248 )
249 .execute(&mut *tx)
250 .await
251 {
252 error!("Failed to enable TOTP: {:?}", e);
253 return (
254 StatusCode::INTERNAL_SERVER_ERROR,
255 Json(json!({"error": "InternalError"})),
256 )
257 .into_response();
258 }
259
260 if let Err(e) = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", auth.0.did)
261 .execute(&mut *tx)
262 .await
263 {
264 error!("Failed to clear old backup codes: {:?}", e);
265 return (
266 StatusCode::INTERNAL_SERVER_ERROR,
267 Json(json!({"error": "InternalError"})),
268 )
269 .into_response();
270 }
271
272 for code in &backup_codes {
273 let hash = match hash_backup_code(code) {
274 Ok(h) => h,
275 Err(e) => {
276 error!("Failed to hash backup code: {:?}", e);
277 return (
278 StatusCode::INTERNAL_SERVER_ERROR,
279 Json(json!({"error": "InternalError"})),
280 )
281 .into_response();
282 }
283 };
284
285 if let Err(e) = sqlx::query!(
286 "INSERT INTO backup_codes (did, code_hash, created_at) VALUES ($1, $2, NOW())",
287 auth.0.did,
288 hash
289 )
290 .execute(&mut *tx)
291 .await
292 {
293 error!("Failed to store backup code: {:?}", e);
294 return (
295 StatusCode::INTERNAL_SERVER_ERROR,
296 Json(json!({"error": "InternalError"})),
297 )
298 .into_response();
299 }
300 }
301
302 if let Err(e) = tx.commit().await {
303 error!("Failed to commit transaction: {:?}", e);
304 return (
305 StatusCode::INTERNAL_SERVER_ERROR,
306 Json(json!({"error": "InternalError"})),
307 )
308 .into_response();
309 }
310
311 info!(did = %auth.0.did, "TOTP enabled with {} backup codes", backup_codes.len());
312
313 Json(EnableTotpResponse { backup_codes }).into_response()
314}
315
316#[derive(Deserialize)]
317pub struct DisableTotpInput {
318 pub password: String,
319 pub code: String,
320}
321
322pub async fn disable_totp(
323 State(state): State<AppState>,
324 auth: BearerAuth,
325 Json(input): Json<DisableTotpInput>,
326) -> Response {
327 if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, &auth.0.did).await {
328 return crate::api::server::reauth::legacy_mfa_required_response(&state.db, &auth.0.did)
329 .await;
330 }
331
332 if !state
333 .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did)
334 .await
335 {
336 warn!(did = %auth.0.did, "TOTP verification rate limit exceeded");
337 return (
338 StatusCode::TOO_MANY_REQUESTS,
339 Json(json!({
340 "error": "RateLimitExceeded",
341 "message": "Too many verification attempts. Please try again in a few minutes."
342 })),
343 )
344 .into_response();
345 }
346
347 let user = sqlx::query!("SELECT password_hash FROM users WHERE did = $1", auth.0.did)
348 .fetch_optional(&state.db)
349 .await;
350
351 let password_hash = match user {
352 Ok(Some(row)) => row.password_hash,
353 Ok(None) => {
354 return (
355 StatusCode::NOT_FOUND,
356 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
357 )
358 .into_response();
359 }
360 Err(e) => {
361 error!("DB error fetching user: {:?}", e);
362 return (
363 StatusCode::INTERNAL_SERVER_ERROR,
364 Json(json!({"error": "InternalError"})),
365 )
366 .into_response();
367 }
368 };
369
370 let password_valid = password_hash
371 .as_ref()
372 .map(|h| bcrypt::verify(&input.password, h).unwrap_or(false))
373 .unwrap_or(false);
374 if !password_valid {
375 return (
376 StatusCode::UNAUTHORIZED,
377 Json(json!({
378 "error": "InvalidPassword",
379 "message": "Password is incorrect"
380 })),
381 )
382 .into_response();
383 }
384
385 let totp_row = sqlx::query!(
386 "SELECT secret_encrypted, encryption_version, verified FROM user_totp WHERE did = $1",
387 auth.0.did
388 )
389 .fetch_optional(&state.db)
390 .await;
391
392 let totp_row = match totp_row {
393 Ok(Some(row)) if row.verified => row,
394 Ok(Some(_)) | Ok(None) => {
395 return (
396 StatusCode::BAD_REQUEST,
397 Json(json!({
398 "error": "TotpNotEnabled",
399 "message": "TOTP is not enabled for this account"
400 })),
401 )
402 .into_response();
403 }
404 Err(e) => {
405 error!("DB error fetching TOTP: {:?}", e);
406 return (
407 StatusCode::INTERNAL_SERVER_ERROR,
408 Json(json!({"error": "InternalError"})),
409 )
410 .into_response();
411 }
412 };
413
414 let code = input.code.trim();
415 let code_valid = if is_backup_code_format(code) {
416 verify_backup_code_for_user(&state, &auth.0.did, code).await
417 } else {
418 let secret =
419 match decrypt_totp_secret(&totp_row.secret_encrypted, totp_row.encryption_version) {
420 Ok(s) => s,
421 Err(e) => {
422 error!("Failed to decrypt TOTP secret: {:?}", e);
423 return (
424 StatusCode::INTERNAL_SERVER_ERROR,
425 Json(json!({"error": "InternalError"})),
426 )
427 .into_response();
428 }
429 };
430 verify_totp_code(&secret, code)
431 };
432
433 if !code_valid {
434 return (
435 StatusCode::UNAUTHORIZED,
436 Json(json!({
437 "error": "InvalidCode",
438 "message": "Invalid verification code"
439 })),
440 )
441 .into_response();
442 }
443
444 let mut tx = match state.db.begin().await {
445 Ok(tx) => tx,
446 Err(e) => {
447 error!("Failed to begin transaction: {:?}", e);
448 return (
449 StatusCode::INTERNAL_SERVER_ERROR,
450 Json(json!({"error": "InternalError"})),
451 )
452 .into_response();
453 }
454 };
455
456 if let Err(e) = sqlx::query!("DELETE FROM user_totp WHERE did = $1", auth.0.did)
457 .execute(&mut *tx)
458 .await
459 {
460 error!("Failed to delete TOTP: {:?}", e);
461 return (
462 StatusCode::INTERNAL_SERVER_ERROR,
463 Json(json!({"error": "InternalError"})),
464 )
465 .into_response();
466 }
467
468 if let Err(e) = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", auth.0.did)
469 .execute(&mut *tx)
470 .await
471 {
472 error!("Failed to delete backup codes: {:?}", e);
473 return (
474 StatusCode::INTERNAL_SERVER_ERROR,
475 Json(json!({"error": "InternalError"})),
476 )
477 .into_response();
478 }
479
480 if let Err(e) = tx.commit().await {
481 error!("Failed to commit transaction: {:?}", e);
482 return (
483 StatusCode::INTERNAL_SERVER_ERROR,
484 Json(json!({"error": "InternalError"})),
485 )
486 .into_response();
487 }
488
489 info!(did = %auth.0.did, "TOTP disabled");
490
491 (StatusCode::OK, Json(json!({}))).into_response()
492}
493
494#[derive(Serialize)]
495#[serde(rename_all = "camelCase")]
496pub struct GetTotpStatusResponse {
497 pub enabled: bool,
498 pub has_backup_codes: bool,
499 pub backup_codes_remaining: i64,
500}
501
502pub async fn get_totp_status(State(state): State<AppState>, auth: BearerAuth) -> Response {
503 let totp_row = sqlx::query!("SELECT verified FROM user_totp WHERE did = $1", auth.0.did)
504 .fetch_optional(&state.db)
505 .await;
506
507 let enabled = match totp_row {
508 Ok(Some(row)) => row.verified,
509 Ok(None) => false,
510 Err(e) => {
511 error!("DB error fetching TOTP status: {:?}", e);
512 return (
513 StatusCode::INTERNAL_SERVER_ERROR,
514 Json(json!({"error": "InternalError"})),
515 )
516 .into_response();
517 }
518 };
519
520 let backup_count_row = sqlx::query!(
521 "SELECT COUNT(*) as count FROM backup_codes WHERE did = $1 AND used_at IS NULL",
522 auth.0.did
523 )
524 .fetch_one(&state.db)
525 .await;
526
527 let backup_count = backup_count_row.map(|r| r.count.unwrap_or(0)).unwrap_or(0);
528
529 Json(GetTotpStatusResponse {
530 enabled,
531 has_backup_codes: backup_count > 0,
532 backup_codes_remaining: backup_count,
533 })
534 .into_response()
535}
536
537#[derive(Deserialize)]
538pub struct RegenerateBackupCodesInput {
539 pub password: String,
540 pub code: String,
541}
542
543#[derive(Serialize)]
544#[serde(rename_all = "camelCase")]
545pub struct RegenerateBackupCodesResponse {
546 pub backup_codes: Vec<String>,
547}
548
549pub async fn regenerate_backup_codes(
550 State(state): State<AppState>,
551 auth: BearerAuth,
552 Json(input): Json<RegenerateBackupCodesInput>,
553) -> Response {
554 if !state
555 .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did)
556 .await
557 {
558 warn!(did = %auth.0.did, "TOTP verification rate limit exceeded");
559 return (
560 StatusCode::TOO_MANY_REQUESTS,
561 Json(json!({
562 "error": "RateLimitExceeded",
563 "message": "Too many verification attempts. Please try again in a few minutes."
564 })),
565 )
566 .into_response();
567 }
568
569 let user = sqlx::query!("SELECT password_hash FROM users WHERE did = $1", auth.0.did)
570 .fetch_optional(&state.db)
571 .await;
572
573 let password_hash = match user {
574 Ok(Some(row)) => row.password_hash,
575 Ok(None) => {
576 return (
577 StatusCode::NOT_FOUND,
578 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
579 )
580 .into_response();
581 }
582 Err(e) => {
583 error!("DB error fetching user: {:?}", e);
584 return (
585 StatusCode::INTERNAL_SERVER_ERROR,
586 Json(json!({"error": "InternalError"})),
587 )
588 .into_response();
589 }
590 };
591
592 let password_valid = password_hash
593 .as_ref()
594 .map(|h| bcrypt::verify(&input.password, h).unwrap_or(false))
595 .unwrap_or(false);
596 if !password_valid {
597 return (
598 StatusCode::UNAUTHORIZED,
599 Json(json!({
600 "error": "InvalidPassword",
601 "message": "Password is incorrect"
602 })),
603 )
604 .into_response();
605 }
606
607 let totp_row = sqlx::query!(
608 "SELECT secret_encrypted, encryption_version, verified FROM user_totp WHERE did = $1",
609 auth.0.did
610 )
611 .fetch_optional(&state.db)
612 .await;
613
614 let totp_row = match totp_row {
615 Ok(Some(row)) if row.verified => row,
616 Ok(Some(_)) | Ok(None) => {
617 return (
618 StatusCode::BAD_REQUEST,
619 Json(json!({
620 "error": "TotpNotEnabled",
621 "message": "TOTP must be enabled to regenerate backup codes"
622 })),
623 )
624 .into_response();
625 }
626 Err(e) => {
627 error!("DB error fetching TOTP: {:?}", e);
628 return (
629 StatusCode::INTERNAL_SERVER_ERROR,
630 Json(json!({"error": "InternalError"})),
631 )
632 .into_response();
633 }
634 };
635
636 let secret = match decrypt_totp_secret(&totp_row.secret_encrypted, totp_row.encryption_version)
637 {
638 Ok(s) => s,
639 Err(e) => {
640 error!("Failed to decrypt TOTP secret: {:?}", e);
641 return (
642 StatusCode::INTERNAL_SERVER_ERROR,
643 Json(json!({"error": "InternalError"})),
644 )
645 .into_response();
646 }
647 };
648
649 let code = input.code.trim();
650 if !verify_totp_code(&secret, code) {
651 return (
652 StatusCode::UNAUTHORIZED,
653 Json(json!({
654 "error": "InvalidCode",
655 "message": "Invalid verification code"
656 })),
657 )
658 .into_response();
659 }
660
661 let backup_codes = generate_backup_codes();
662 let mut tx = match state.db.begin().await {
663 Ok(tx) => tx,
664 Err(e) => {
665 error!("Failed to begin transaction: {:?}", e);
666 return (
667 StatusCode::INTERNAL_SERVER_ERROR,
668 Json(json!({"error": "InternalError"})),
669 )
670 .into_response();
671 }
672 };
673
674 if let Err(e) = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", auth.0.did)
675 .execute(&mut *tx)
676 .await
677 {
678 error!("Failed to clear old backup codes: {:?}", e);
679 return (
680 StatusCode::INTERNAL_SERVER_ERROR,
681 Json(json!({"error": "InternalError"})),
682 )
683 .into_response();
684 }
685
686 for code in &backup_codes {
687 let hash = match hash_backup_code(code) {
688 Ok(h) => h,
689 Err(e) => {
690 error!("Failed to hash backup code: {:?}", e);
691 return (
692 StatusCode::INTERNAL_SERVER_ERROR,
693 Json(json!({"error": "InternalError"})),
694 )
695 .into_response();
696 }
697 };
698
699 if let Err(e) = sqlx::query!(
700 "INSERT INTO backup_codes (did, code_hash, created_at) VALUES ($1, $2, NOW())",
701 auth.0.did,
702 hash
703 )
704 .execute(&mut *tx)
705 .await
706 {
707 error!("Failed to store backup code: {:?}", e);
708 return (
709 StatusCode::INTERNAL_SERVER_ERROR,
710 Json(json!({"error": "InternalError"})),
711 )
712 .into_response();
713 }
714 }
715
716 if let Err(e) = tx.commit().await {
717 error!("Failed to commit transaction: {:?}", e);
718 return (
719 StatusCode::INTERNAL_SERVER_ERROR,
720 Json(json!({"error": "InternalError"})),
721 )
722 .into_response();
723 }
724
725 info!(did = %auth.0.did, "Backup codes regenerated");
726
727 Json(RegenerateBackupCodesResponse { backup_codes }).into_response()
728}
729
730async fn verify_backup_code_for_user(state: &AppState, did: &str, code: &str) -> bool {
731 let code = code.trim().to_uppercase();
732
733 let backup_codes = sqlx::query!(
734 "SELECT id, code_hash FROM backup_codes WHERE did = $1 AND used_at IS NULL",
735 did
736 )
737 .fetch_all(&state.db)
738 .await;
739
740 let backup_codes = match backup_codes {
741 Ok(codes) => codes,
742 Err(e) => {
743 warn!("Failed to fetch backup codes: {:?}", e);
744 return false;
745 }
746 };
747
748 for row in backup_codes {
749 if verify_backup_code(&code, &row.code_hash) {
750 let _ = sqlx::query!(
751 "UPDATE backup_codes SET used_at = $1 WHERE id = $2",
752 Utc::now(),
753 row.id
754 )
755 .execute(&state.db)
756 .await;
757 return true;
758 }
759 }
760
761 false
762}
763
764pub async fn verify_totp_or_backup_for_user(state: &AppState, did: &str, code: &str) -> bool {
765 let code = code.trim();
766
767 if is_backup_code_format(code) {
768 return verify_backup_code_for_user(state, did, code).await;
769 }
770
771 let totp_row = sqlx::query!(
772 "SELECT secret_encrypted, encryption_version, verified FROM user_totp WHERE did = $1",
773 did
774 )
775 .fetch_optional(&state.db)
776 .await;
777
778 let totp_row = match totp_row {
779 Ok(Some(row)) if row.verified => row,
780 _ => return false,
781 };
782
783 let secret = match decrypt_totp_secret(&totp_row.secret_encrypted, totp_row.encryption_version)
784 {
785 Ok(s) => s,
786 Err(_) => return false,
787 };
788
789 if verify_totp_code(&secret, code) {
790 let _ = sqlx::query!("UPDATE user_totp SET last_used = NOW() WHERE did = $1", did)
791 .execute(&state.db)
792 .await;
793 return true;
794 }
795
796 false
797}
798
799pub async fn has_totp_enabled(state: &AppState, did: &str) -> bool {
800 has_totp_enabled_db(&state.db, did).await
801}
802
803pub async fn has_totp_enabled_db(db: &sqlx::PgPool, did: &str) -> bool {
804 let result = sqlx::query_scalar!("SELECT verified FROM user_totp WHERE did = $1", did)
805 .fetch_optional(db)
806 .await;
807
808 matches!(result, Ok(Some(true)))
809}