max / makenotwork
1 file changed,
+14 insertions,
-9 deletions
| @@ -85,21 +85,26 @@ pub(super) async fn confirm( | |||
| 85 | 85 | ||
| 86 | 86 | let totp = build_totp(&secret, &user.email)?; | |
| 87 | 87 | ||
| 88 | - | if !totp.check_current(&form.code).map_err(|e| { | |
| 89 | - | AppError::Internal(anyhow::anyhow!("TOTP check failed: {}", e)) | |
| 90 | - | })? { | |
| 88 | + | // Validate via the matched time step (not `check_current`) and apply the same | |
| 89 | + | // step-monotonicity gate the login path uses, so confirm and login share one | |
| 90 | + | // replay rule: a code is accepted only if its step is newer than the last | |
| 91 | + | // accepted step, and that step is then recorded. `setup` cleared the step to | |
| 92 | + | // NULL (defaults to 0), so a genuine first code (step ~= now/30) always wins. | |
| 93 | + | let now = chrono::Utc::now().timestamp() as u64; | |
| 94 | + | let matched_step = find_matching_step(&totp, &form.code, now); | |
| 95 | + | let last_step = db::totp::get_totp_last_used_step(&state.db, user.id) | |
| 96 | + | .await? | |
| 97 | + | .unwrap_or(0); | |
| 98 | + | let Some(step) = matched_step.filter(|&step| step > last_step) else { | |
| 91 | 99 | return Ok(( | |
| 92 | 100 | [("HX-Retarget", "#totp-confirm-status"), ("HX-Reswap", "innerHTML")], | |
| 93 | 101 | Html("<span class=\"save-error\">Invalid code. Please try again.</span>"), | |
| 94 | 102 | ) | |
| 95 | 103 | .into_response()); | |
| 96 | - | } | |
| 104 | + | }; | |
| 97 | 105 | ||
| 98 | - | // Record the matched step to prevent replay on the very first login | |
| 99 | - | let now = chrono::Utc::now().timestamp() as u64; | |
| 100 | - | if let Some(step) = find_matching_step(&totp, &form.code, now) { | |
| 101 | - | db::totp::set_totp_last_used_step(&state.db, user.id, step).await?; | |
| 102 | - | } | |
| 106 | + | // Record the matched step to prevent replay on the very first login. | |
| 107 | + | db::totp::set_totp_last_used_step(&state.db, user.id, step).await?; | |
| 103 | 108 | ||
| 104 | 109 | db::totp::enable_totp(&state.db, user.id).await?; | |
| 105 | 110 |