Report: Implementing Pseudo 2FA for Web Apps using Google Apps Script

Gists

Abstract

In Google Apps Script, there is the Web Apps. When Web Apps is used, the users can execute Google Apps Script using HTML and Javascript. This can be applied to various applications. When the Web Apps is deployed with “Anyone”, anyone can access the Web Apps. And, there is the case that Web Apps deployed with “Anyone” is required to be used. Under this condition, when 2 Factor Authentication (2FA) can be implemented, it is considered that the security can be higher and it leads to giving various directions for the applications using Web Apps. In this report, I would like to introduce the method for implementing the pseud 2FA for Web Apps deployed with “Anyone” using Google Apps Script.

Introduction

This is a method for implementing the pseud 2 Factor Authentication (2FA) for Web Apps deployed with “Anyone” using Google Apps Script. About the Web Apps created by Google Apps Script, when the Web Apps is deployed as “Execute the app as: Anyone with Google account”, the user is required to login to Google account. In this case, when the user has set 2FA in the Google account, that is used. But, when it is deployed as “Execute the app as: Anyone”, the user is not required to login to Google account. Namely, all users can access the Web Apps. By this, when 2FA is required to be implemented by the developer. Recently, I have reported Creating User’s Dashboard by Inputting Name and Password using Web Apps with Google Apps Script. In this case, when the user name and the password are known, another user can access the user’s dashboard. But, unfortunately, in the current stage, the Web Apps with “Execute the app as: Anyone” cannot directly use 2FA. So, in this post, I would like to introduce implementing the pseudo 2FA to the Web Apps deployed with “Execute the app as: Anyone”. There is the case that it is required to use the Web Apps deployed as “Execute the app as: Anyone”. This report might be useful for this situation.

In this sample, I implemented 2FA to this post “Creating User’s Dashboard by Inputting Name and Password using Web Apps with Google Apps Script”.

Usage

1. Prepare a standalone Google Apps Script project

In order to test this method, please create a standalone Google Apps Script project.

2. Sample script

Please copy and paste the following script to the script editor of the created Google Apps Script project.

Google Apps Script: Code.gs

Please set userObj. In this script, an email including the authorization code for 2FA is sent to email. In this case, email addresses except for Gmail and Google email can be also used.

// Please set the user's name and password and the sample value of the user.
// To use the sample value is a sample situation for explaining this method.
const userObj = [
  {
    email: "email address 1",
    password: "samplePassword1",
    sampleValue: "sample value 1",
  },
  {
    email: "email address 2",
    password: "samplePassword2",
    sampleValue: "sample value 2",
  },
  {
    email: "email address 3",
    password: "samplePassword3",
    sampleValue: "sample value 3",
  },
  ,
  ,
  ,
];

const limit = 600; // Expiration time of 2FA. Unit is seconds.

// Create key for each user.
const encodeValue_ = ({ email, pass }) =>
  Utilities.base64Encode(JSON.stringify([email, pass]));

// Show main HTML after the login and the confirmation of authorization code.
// If you want to change the returned value for each user, please modify "html.data = user.sampleValue;" of this function.
function showMainHTML_(user) {
  const htmlFilename = "showData";

  const html = HtmlService.createTemplateFromFile(htmlFilename);

  // This is a sample value. Please modify this for your actual situation.
  html.data = user.sampleValue;

  return html;
}

// Generate authorization code. A text of 5 letters.
function generateCode_(c, o) {
  const htmlFilename = "2FA";

  const newCode = Math.random().toString().slice(2, 7);
  c.put(encodeValue_(o), newCode, 3600);
  MailApp.sendEmail({
    to: o.email,
    subject: "Authorization code for 2FA of sample Web Apps.",
    body: `Authorization code is ${newCode}`,
  });
  const html = HtmlService.createTemplateFromFile(htmlFilename);
  html.url = ScriptApp.getService().getUrl();
  html.obj = JSON.stringify(o);
  return html;
}

// Show HTML of login.
function showLoginHTML_(error) {
  const htmlFilename = "login";

  const html = HtmlService.createTemplateFromFile(htmlFilename);
  html.url = ScriptApp.getService().getUrl();
  html.error = error;
  return html;
}

// This is a handler for using 2FA for Web Apps.
function handler_(e) {
  const { email, pass, code } = e.parameter;
  const c = CacheService.getScriptCache();
  let error = false;
  if (email && pass) {
    const user = userObj.find((o) => o.email == email && o.password == pass);
    if (user) {
      if (user.password == pass) {
        const currentCode = c.get(encodeValue_({ email, pass }));
        if (code && currentCode && currentCode == "AUTHORIZED") {
          return showMainHTML_(user);
        } else if (!code || code != "error") {
          return generateCode_(c, { email, pass });
        }
      }
    }
    error = true;
  }
  if (error && code == "error") {
    c.remove(encodeValue_({ email, pass }));
  }
  return showLoginHTML_(
    error
      ? code != "error"
        ? "Login error"
        : "Code error: Code is regenerated. Please log in and check email again."
      : ""
  );
}

// Get current stored code. This function is called from Javascript side.
function getCode(obj) {
  return CacheService.getScriptCache().get(encodeValue_(obj)) || "";
}

// Check inputted code. This function is called from Javascript side.
function checkCode({ email, pass, code }) {
  const c = CacheService.getScriptCache();
  const key = encodeValue_({ email, pass });
  const currentCode = c.get(key);
  if (currentCode && currentCode == code) {
    c.put(key, "AUTHORIZED");
    return true;
  }
  c.remove(key);
  return false;
}

// This is called as Web Apps.
function doGet(e) {
  return handler_(e)
    .evaluate()
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
  • In this sample, the expiration time of the authorization code of 2FA is 10 minutes from const limit = 600;. So, after you logged in and authorized the code, the authorization code is not required to be used for 10 minutes. If you want to change this, please modify it.

HTML: login.html

<p><?!= error ?></p>
<input type="email" id="email" placeholder="Please input your email address." />
<input type="password" id="pass" placeholder="Please input login password." />
<input type="button" value="login" onclick="sample()" />
<script>
  function sample() {
    const url = "<?!= url ?>";
    const [email, pass] = ["email", "pass"].map((e) =>
      document.getElementById(e).value.trim()
    );
    google.script.run
      .withSuccessHandler((code) => {
        window.open(
          code != ""
            ? `${url}?email=${email}&pass=${pass}&code=${code}`
            : `${url}?email=${email}&pass=${pass}`,
          "_top"
        );
      })
      .getCode({ email, pass });
  }
</script>

HTML: 2FA.html

<p>
  An email including the authorization code was sent to your email address.
  Please check it and input the code to the following input tag.
</p>
<input type="text" id="code" placeholder="Please input your code." />
<input type="button" value="login" onclick="sample()" />
<script>
  function sample() {
    const url = "<?!= url ?>";
    const obj = <?!= obj ?>;
    const code = document.getElementById("code").value.trim();
    obj.code = code;
    google.script.run.withSuccessHandler(e => {
      if (e) {
        window.open(`${url}?code=${obj.code}&email=${obj.email}&pass=${obj.pass}`, "_top");
        return;
      }
      window.open(`${url}?code=error&email=${obj.email}&pass=${obj.pass}`, "_top");
    }).checkCode(obj);
  }
</script>

HTML: showData.html

<?!= data ?>

3. Deploy Web Apps.

The detailed information can be seen at the official document.

Please set this using the new IDE of the script editor.

  1. On the script editor, at the top right of the script editor, please click “click Deploy” -> “New deployment”.
  2. Please click “Select type” -> “Web App”.
  3. Please input the information about the Web App in the fields under “Deployment configuration”.
  4. Please select “Me” for “Execute as”.
  5. Please select “Anyone” for “Who has access”.
  6. Please click the “Deploy” button.
  7. Copy the URL of the Web App. It’s like https://script.google.com/macros/s/###/exec.
  • When you use this method, please access the retrieved URL using your browser. By this, you can see the login page.

  • When you modify the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful about this.

  • You can see the detail of this in the report “Redeploying Web Apps without Changing URL of Web Apps for new IDE”.

4. Testing

When this method is used, the following sample situation is obtained.

In this sample, after you logged in and authorized the code, you can access your dashboard in Web Apps without the code for 10 minutes. After 10 minutes, the authorization code is reset. By this, it is required to authorize the code again.

When the malicious user accesses Web Apps without authorizing the code, even when the user knows your email and password, the user cannot authorize the code because the user cannot see your email box. By this, your dashboard cannot be seen by the malicious user.

Note

  • In this sample script, when the name, the password, and the authorization code are sent to the Google Apps Script side, those values are not encrypted as a sample. I selected this for the readability of the flow of this method. But, when you use this script in your actual situation, I would like to recommend encrypting the values.

References

 Share!