prepare("select * from user where user_id = :user_id limit 1;"); $sth->execute([":user_id" => $user_id]); $temp = $sth->fetch(PDO::FETCH_ASSOC); // ── Step 3: Re-derive expected OTP ──────────────────────────────────────────── // Uses $_SESSION['otpTime'] (set when the OTP was generated) as the TOTP // counter base. This is the same algorithm used in login_otp.php and // request_new_otp.php — any change to one must be reflected in all three. function generateOTP($sercet_key, $time_step = 180, $length = 6) { $counter = floor($_SESSION["otpTime"] / $time_step); $data = pack("NN", 0, $counter); $hash = hash_hmac('sha1', $data, $sercet_key, true); $offset = ord(substr($hash, -1)) & 0x0F; $value = unpack("N", substr($hash, $offset, 4)); $otp = ($value[1] & 0x7FFFFFFF) % pow(10, $length); return str_pad(strval($otp), $length, '0', STR_PAD_LEFT); } $otp = generateOTP($temp["password"]); // ── Step 3b: Calculate elapsed time since OTP was issued ────────────────────── // otpTime is the Unix timestamp stored by login_otp.php when the OTP was sent. // The diff is computed in minutes for the 5-minute validity window check. $otp_time = isset($_SESSION['otpTime']) ? (int)$_SESSION['otpTime'] : 0; $now = time(); $otp_diff_seconds = max(0, $now - $otp_time); $otp_diff_minutes = $otp_diff_seconds / 60.0; // Store for debug convenience — visible in $_SESSION on the session inspect page $_SESSION["now"] = $now; $_SESSION["diff"] = $otp_diff_minutes; // ── Step 4: Validate OTP value and expiry ───────────────────────────────────── // Fails if either the code doesn't match OR more than 5 minutes have elapsed // since the OTP was issued. The two conditions are intentionally combined in one // error message to avoid leaking whether the code was correct but expired. if ($data["otp"] != $otp || $otp_diff_minutes > 5) { $answer["message"] = "Wrong OTP! Please try again. (Our OTP is valid for 5 minute)"; exit(json_encode($answer)); } // ── Step 5a: Regenerate session ID ──────────────────────────────────────────── // session_regenerate_id(true) issues a brand-new session ID and deletes the old // session file, preventing session fixation attacks where an attacker pre-sets // a session ID before the user logs in. session_regenerate_id(true); // ── Step 5b: Issue CSRF token ───────────────────────────────────────────────── // A fresh 256-bit token is generated here and stored in session. All subsequent // POST requests from the authenticated app must include this token in the // X-CSRF-Token header (validated by individual engine endpoints). $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // ── Step 5c: Write authenticated login session ──────────────────────────────── // These keys are read by db_auth.php on every subsequent request to gate access. // login_company_id is the user's default_company — used to scope all DB queries. $_SESSION["login_status"] = 1; $_SESSION["login_user_id"] = (int)$temp["user_id"]; $_SESSION["login_username"] = $temp["username"]; $_SESSION["login_name"] = $temp["name"]; $_SESSION["login_surname"] = $temp["surname"]; $_SESSION["login_company_id"] = $temp["default_company"]; $_SESSION["login_profile_picture"] = $temp["profile_picture"] ?? ''; $_SESSION["login_license"] = $temp["license"] ?? 'user'; // license='owner' means the user holds their own subscription — use user.app_access. // license='user' means they were invited — use company_map_user.app_access instead. $_SESSION["login_app_access"] = $temp["app_access"] ?? 'wms'; $role_sth = $pdo1->prepare( "SELECT role, app_access FROM company_map_user WHERE company_id = :company_id AND user_id = :user_id LIMIT 1" ); $role_sth->execute([ ':company_id' => $_SESSION["login_company_id"], ':user_id' => $_SESSION["login_user_id"], ]); $map_row = $role_sth->fetch(PDO::FETCH_ASSOC); $_SESSION["login_role"] = $map_row['role'] ?? 'viewer'; if (($temp['license'] ?? 'owner') !== 'owner') { $_SESSION["login_app_access"] = $map_row['app_access'] ?? 'wms'; } // ── Step 6: Respond ─────────────────────────────────────────────────────────── $answer["success"] = 1; $answer["message"] = "Login Complete!"; exit(json_encode($answer));