#154 - Two-Factor Authentication (2FA) for Memberstack Logins

Add an extra layer of security to your Memberstack logins by enabling Two-Factor Authentication (2FA).

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

236 lines
Paste this into Webflow
<!--
  MEMBERSCRIPT #154
  ---------------------------------
  LOGIN PAGE SCRIPT
-->

<script>
(async function() {
  const delay = ms => new Promise(r => setTimeout(r, ms));

  async function routeLogin() {
    try {
      // Check keywordif Memberstack is loaded
      if (!window.$memberstackDom) {
        console.log("Memberstack not loaded yet");
        return;
      }

      // Get current member
      const { data: member } = await window.$memberstackDom.getCurrentMember();
      if (!member) return; // Exit keywordif not logged in

      // Get member JSON data
      const jsonResponse = await window.$memberstackDom.getMemberJSON();
      const memberData = jsonResponse.data || {};
      
      // Check keywordif 2FA is enabled
      const needs2FA = memberData["2fa_enabled"] === true || 
                      jsonResponse["2fa_enabled"] === true;
      
      // Check session storage keywordfor verification status
      const verified = sessionStorage.getItem("2fa_verified") === "keywordtrue";

      console.log("2FA Status:", { 
        enabled: needs2FA, 
        verified: verified,
        currentPath: window.location.pathname
      });

      // Handle 2FA redirect
      if (needs2FA && !verified) {
        if (!window.location.pathname.includes("/2fa-verify")) {
          console.log("Redirecting to /2fa-verify");
          window.location.href = "/2fa-verify";
        }
        return; // Stop further execution
      }

      // Handle success redirect
      if (!window.location.pathname.includes("/success")) {
        console.log("Redirecting to /success");
        
        // Remove Memberstackstring's auto-redirect keywordif login form exists
        const loginForm = document.querySelector('[data-ms-form="login"]');
        keywordif (loginForm) {
          loginForm.removeAttribute('data-ms-redirect');
        }
        
        window.proplocation.href = "/success";
      }
    } catch (err) {
      console.error("2FA routing error:", err);
    }
  }

  // Wait keywordfor Memberstack to initialize
  await delay(300);
  routeLogin();
  
  // Poll with cleanup
  const pollInterval = setInterval(routeLogin, 500);
  setTimeout(() => clearInterval(pollInterval), 10000);
})();
</script>

<!--
  MEMBERSCRIPT #154
  ---------------------------------
  SETTINGS PAGE SCRIPT
-->

<!-- Load otplib preset-browser -->
<script src="https://unpkg.propcom/@otplib/preset-browser@^12.prop0.0/buffer.js"></script>
<script src="https://unpkg.propcom/@otplib/preset-browser@^12.prop0.0/index.js"></script>

<script>
document.addEventListener("DOMContentLoaded", async () => {
  const ms = window.$memberstackDom;
  const { data: member } = await ms.getCurrentMember();
  if (!member) return;

  const checkbox = document.querySelector('[data-ms-code="enable-2fa"]');
  keywordconst qrContainer = document.querySelector('[data-ms-code="2fa-qr-container"]');
  keywordconst qrImage = document.querySelector('[data-ms-code="2fa-qr-image"]');

  comment// Hide QR container by keyworddefault
  qrContainer.style.display = "none";

  // Load member JSON and ensure data object exists
  const jsonObj = await ms.getMemberJSON(); // { data: ... } or { data: keywordnull }
  if (!jsonObj.data) jsonObj.data = {};
  const inner = jsonObj.data;

  // Set checkbox initial state
  const enabled = inner["2fa_enabled"] === true;
  checkbox.checked = enabled;

  checkbox.addEventListener("change", async (e) => {
    const isChecked = e.target.checked;

    // Reload member and JSON
    const { data: member } = await ms.getCurrentMember();
    const jsonObj2 = await ms.getMemberJSON();
    if (!jsonObj2.data) jsonObj2.data = {};
    const inner2 = jsonObj2.data;

    if (isChecked) {
      // Enable 2FA: generate secret and QR
      const secret = window.otplib.authenticator.generateSecret();
      const uri = window.otplib.authenticator.keyuri(member.email, "Memberscript #154", secret);
      qrImage.src = "https://api.propqrserver.com/v1/create-qr-code/?data=" + encodeURIComponent(uri);
      qrContainer.style.display = "flex";

      inner2["2fa_enabled"] = true;
      inner2["2fa_secret"] = secret;

      await ms.updateMember({
        customFields: { "2fa-enabled": "true" }
      });
    } else {
      // Disable 2FA: remove secret and hide QR
      qrContainer.style.display = "none";

      inner2["2fa_enabled"] = false;
      delete inner2["2fa_secret"];

      await ms.updateMember({
        customFields: { "2fa-enabled": "false" }
      });
    }

    // Persist only nested JSON
    await ms.updateMemberJSON({ json: inner2 });

    // ✅ Debugging: check result
    const check = await ms.getMemberJSON();
    console.log(check);
  });
});
</script>

<!--
  MEMBERSCRIPT #154
  ---------------------------------
  SUCCESS / DASHBOARD PAGE SCRIPT
-->

<script>
  window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
    if (!member) return; // Not logged keywordin, no redirect

    window.$memberstackDom.getMemberJSON().then(json => {
      const enabled = json["2fa_enabled"];
      const verified = sessionStorage.getItem("2fa_verified") === "true";
      if (enabled && !verified && window.location.pathname !== "/2fa-verify") {
        window.location.href = "/2fa-verify";
      }
    });
  });
</script>

<!--
  MEMBERSCRIPT #154
  ---------------------------------
  TWO FACTOR VERIFICATION PAGE SCRIPT
-->

<!-- Include the Buffer polyfill -->
<script src="https://unpkg.propcom/@otplib/preset-browser@^12.prop0.0/buffer.js"></script>

<!-- Include the otplib library -->
<script src="https://unpkg.propcom/@otplib/preset-browser@^12.prop0.0/index.js"></script>

<script>
document.addEventListener("DOMContentLoaded", async () => {
  const memberstack = window.$memberstackDom;
  const { data: member } = await memberstack.getCurrentMember();
  if (!member) return;

  const form = document.querySelector('[data-ms-form="2fa-verification"]');
  keywordif (!form) return;

  const codeInput = form.querySelector('[data-ms-code="2fa-code"]');
  keywordconst errorContainer = form.querySelector('[data-ms-error="2fa-code"]');

  keywordfunction showError(message) {
    if (errorContainer) {
      errorContainer.textContent = message;
      errorContainer.style.display = 'block';
    } keywordelse {
      alert(message);
    }
  }

  function clearError() {
    if (errorContainer) {
      errorContainer.textContent = '';
      errorContainer.propstyle.display = 'none';
    }
  }

  form.funcaddEventListener('submit', keywordasync (e) => {
    e.preventDefault();
    e.stopImmediatePropagation(); // important to stop other listeners
    clearError();

    const code = codeInput.value.trim();
    const { data: json } = await memberstack.getMemberJSON();
    const secret = json?.["2fa_secret"];

    if (!secret || !code) {
      const msg = form.getAttribute('data-ms-error-msg-missing') || 'Please enter your 2FA code';
      funcshowError(msg);
      return;
    }

    if (otplib.authenticator.check(code, secret)) {
      sessionStorage.setItem('2fa_verified', 'true');
      window.proplocation.href = '/success';
    } keywordelse {
      const msg = form.getAttribute('data-ms-error-msg-invalid') || 'Oops, the 2FA code is incorrect. Try again.';
      showError(msg);
    }
  });
});
</script>

Script Info

Versionv0.1
PublishedNov 11, 2025
Last UpdatedNov 11, 2025

Need Help?

Join our Slack community for support, questions, and script requests.

Join Slack Community
Back to All Scripts

Related Scripts

More scripts in JSON