- OTP attempt limits, constant-time compare, random reference codes - DB-backed rate limits (429) on sign-in, OTP, reset, register, onboarding - one generic sign-in failure message; reset request no longer reveals accounts - no password kept in the session; real status codes on failures
132 lines
6.1 KiB
PHP
132 lines
6.1 KiB
PHP
<?php
|
|
/**
|
|
* request_new_otp.php — Resend OTP during the 2-factor login flow
|
|
*
|
|
* Called by: login page AJAX "Resend OTP" button on the OTP input screen.
|
|
* Input: All data sourced from $_SESSION (written by login_otp.php).
|
|
* No new user input is accepted.
|
|
*
|
|
* This endpoint regenerates a fresh TOTP and resends the OTP email without
|
|
* requiring the user to re-enter their username and password. It is only
|
|
* reachable while a pending login exists: login_otp.php verified the password
|
|
* less than LOGIN_PENDING_SECONDS ago (password_verified_at). The password
|
|
* itself is never kept in the session, so it is not re-checked here.
|
|
*
|
|
* Full flow:
|
|
* 1. Require a fresh pending login; otherwise HTTP 401 (code "login_restart").
|
|
* 2. Throttle: per client IP, per user, and at most LOGIN_OTP_MAX_RESENDS
|
|
* resends per pending login (HTTP 429).
|
|
* 3. Fetch the user row (password hash for the OTP, email to send to).
|
|
* 4. Generate a fresh 6-digit TOTP (new timestamp → new OTP) and a random
|
|
* 6-letter reference number.
|
|
* 5. Send the OTP email with the same SMTP choice as login_otp.php: the
|
|
* default company's SMTP when configured, otherwise the system $SMTP.
|
|
* 6. Write the new OTP state (the wrong-attempt counter restarts).
|
|
* 7. Return { success: 1, message: "Login Complete!" }.
|
|
*
|
|
* Session keys read:
|
|
* login_user_id, password_verified_at, otp_resends
|
|
*
|
|
* Session keys overwritten:
|
|
* otp, otpTime, reference, user_email, otp_attempts, otp_resends
|
|
*/
|
|
|
|
require_once '../../../session.php';
|
|
require_once '../../../config.php';
|
|
require_once '../../../preset.php';
|
|
define('UNAUTHENTICATED_ROUTE', true);
|
|
require_once '../../../assets/utils/db_auth.php';
|
|
require_once '../../../assets/utils/rate_limit.php';
|
|
require_once '../login_helpers.php';
|
|
|
|
// ── Step 1: Require a pending login ───────────────────────────────────────────
|
|
if (!login_pending_valid()) {
|
|
$_SESSION = [];
|
|
login_fail(401, 'Your sign-in has expired. Please enter your username and password again.', ['code' => 'login_restart']);
|
|
}
|
|
$user_id = (int)$_SESSION["login_user_id"];
|
|
|
|
// ── Step 2: Throttle resends ──────────────────────────────────────────────────
|
|
if ((int)($_SESSION['otp_resends'] ?? 0) >= LOGIN_OTP_MAX_RESENDS) {
|
|
rate_limit_reject();
|
|
}
|
|
rate_limit_guard($pdo1, [
|
|
['otp_resend_ip', rate_limit_client_ip(), 10, 900],
|
|
['otp_resend_user', (string)$user_id, 5, 900],
|
|
]);
|
|
|
|
// ── Step 3: Fetch user record ─────────────────────────────────────────────────
|
|
$sth = $pdo1->prepare("select * from user where user_id = :user_id limit 1;");
|
|
$sth->execute([":user_id" => $user_id]);
|
|
$temp = $sth->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$temp) {
|
|
$_SESSION = [];
|
|
login_fail(401, 'Your sign-in has expired. Please enter your username and password again.', ['code' => 'login_restart']);
|
|
}
|
|
|
|
$user_email = $temp["email"];
|
|
|
|
// ── Step 4: Generate fresh 6-digit TOTP + random reference ────────────────────
|
|
// Same HMAC-SHA1 algorithm as login_otp.php and login_confirm.php.
|
|
// A new $otpTime is captured so the OTP window resets from this moment.
|
|
$otpTime = time();
|
|
$otp = login_generate_otp($temp["password"], $otpTime);
|
|
$reference_number = login_random_reference();
|
|
|
|
// ── Step 5: Send OTP email ────────────────────────────────────────────────────
|
|
// Company SMTP of the user's default company when configured, otherwise the
|
|
// system-level $SMTP from config.php.
|
|
$smtp_config = $SMTP;
|
|
$default_company = (int)($temp["default_company"] ?? 0);
|
|
if ($default_company > 0) {
|
|
$sth = $pdo1->prepare("SELECT * FROM company_smtp WHERE company_id = :cid LIMIT 1");
|
|
$sth->execute([":cid" => $default_company]);
|
|
$smtp_row = $sth->fetch(PDO::FETCH_ASSOC);
|
|
if (!empty($smtp_row)) {
|
|
$smtp_config = $smtp_row;
|
|
}
|
|
}
|
|
|
|
require "../../../assets/utils/module/mailer.php";
|
|
|
|
$mailer = new mailer(["pdo1" => $pdo1, "pdo2" => $pdo2]);
|
|
|
|
$mailer->send_email([
|
|
"company_id" => $smtp_config === $SMTP ? 0 : $default_company,
|
|
"smtp" => $smtp_config,
|
|
"subject" => "One Time Password (OTP) For reference number " . $reference_number,
|
|
"message" => implode("\n", [
|
|
"Dear WMS user,",
|
|
"",
|
|
"You requested a One-Time Password (OTP) to log in to WMS.",
|
|
"",
|
|
"Please use the OTP below to complete your request:",
|
|
"• OTP code: " . $otp,
|
|
"• Reference number: " . $reference_number,
|
|
"",
|
|
"Please note:",
|
|
"• This code will expire in 3 minutes. Please complete your action promptly.",
|
|
"• Do not share this code with anyone to keep your account secure.",
|
|
"• If you did not request this code, please ignore this email.",
|
|
]),
|
|
"channel_name" => "WMS LOGIN OTP ",
|
|
"to" => $user_email,
|
|
"key" => $pinkey,
|
|
]);
|
|
|
|
// ── Step 6: Write the new OTP state ───────────────────────────────────────────
|
|
// The pending-login keys (login_data, login_user_id, password_verified_at) stay
|
|
// as they are; only the OTP state is replaced.
|
|
$_SESSION["otp"] = $otp;
|
|
$_SESSION["otpTime"] = $otpTime; // new timestamp — login_confirm.php uses this
|
|
$_SESSION["reference"] = $reference_number;
|
|
$_SESSION["user_email"] = $user_email;
|
|
$_SESSION["otp_attempts"] = 0;
|
|
$_SESSION["otp_resends"] = (int)($_SESSION["otp_resends"] ?? 0) + 1;
|
|
|
|
// ── Step 7: Respond ───────────────────────────────────────────────────────────
|
|
$answer["success"] = 1;
|
|
$answer["message"] = "Login Complete!";
|
|
exit(json_encode($answer));
|