Secret Double Octopus – Custom Authenticator – Email OTP Codes

Custom Authenticators in Secret Double Octopus

Secret Double Octopus allows organizations to utilize third-party authenticators with their platform. These authenticators can be used for both classic MFA (Username + Password + Factor) as well as for Passwordless MFA. Secret Double Octopus includes support for many third-party authenticators out of the box, there is a table below outlining them. SDO also allows for customers to create their own authenticators within the system. The overall process requires you to build a JSON file that maps out fields and requirements for the authenticator and a JavaScript file that actually processes the authentication with the third-party authenticator.

As of version 5.8.2 of the Secret Double Octopus platform, there is native support for the following authenticators:

AuthenticatorPasswordless or Classic MFA?Authentication Type/Mechanism
Octopus AuthenticatorPasswordless and Classic MFAPush Notification, Phishing Resistant
FIDO2Passwordless and Classic MFAFIDO2, Phishing Resistant
OTP CodesPasswordless and Classic MFARotating 6 or 8 Digit OTP Codes, Not Phishing Resistant
Cisco DuoPasswordless and Classic MFAPush Notifications, Not Phishing Resistant
ForgerockPasswordless and Classic MFAPush Notification, Not Phishing Resistant
Okta VerifyPasswordless and Classic MFAPush Notifications/OTP Validator/Two-Step (SMS and Email),
Not Phishing Resistant
RSAPasswordless and Classic MFAPush Notifications, Not Phishing Resistant
RSA-AMClassic MFAOTP Validator, Not Phishing Resistant
ThalesClassic MFAPush Notification, Not Phishing Resistant
TwilioClassic MFATwo-Step Authentication (Email or SMS), Not Phishing Resistant
YubicoClassic MFAOTP Validator, Phishing Resistant

The Challenge – Email only OTP

I was recently working with a bank located on a small Caribbean Island. Being located on an island like this poses potential challenges for authentication when there is a high likelihood that your island/country could be cut off from the rest of the world during bad weather. The customers requirement for a solution was a system that could be run on-premises, provide Classic MFA today, and operate without any external/off island connectivity required. In addition, the customer indicated they wanted an option to have an OTP code sent to their email for MFA authentication. We decided on utilizing SDO as an OTP validator and allowing for OTP code enrollment using an authenticator app (Octopus Authenticator, Google Authenticator, Microsoft Authenticator etc) as well as using the Twilio plugin for the email component.

Fast forward to configuring our authenticator, specifically Twilio, and this is where we ran into our challenge. I had originally planned on using the Twilio authenticator and just inputting in the customers SMTP relay information and ignoring the Twilio specific components, but this does not work. The authenticator template requires a valid Twilio account to be able to do this, even if you won’t be sending data through Twilio for SMTP. We could have just set up a free Twilio account to get this working, but it still wouldn’t have met the customers requirement of not requiring access to resources off island to perform the authentication. In the event that internet was down to the island, the Twilio authenticator checks would have failed and gone offline within the SDO platform, which would have prevented it from being used, even with their local SMTP relay.

To solve this challenge, we wrote our own authenticator for SDO that is SMTP only. This authenticator lets you bring your own SMTP relay and so long as your SDO servers can reach the SMTP relay, the authenticator will remain online and functional.

Implementing the Code

There are two steps to implementing a custom authenticator within the SDO platform. First, a JSON file needs to be created that maps out the various options and fields required to successfully connect to the authenticator. Second, a JavaScript file needs to be created to actually perform the authentication. In our case here, we are implementing an SMTP relay authenticator.

Email Authenticator Template – JSON File

The JSON file for this email authenticator is below the steps required to upload to the SDO platform.

To upload the JSON to the SDO platform, follow these steps:

  1. 1. Save the code below to your machine as emailOTP_v2.json (The file name can be anything you want, this is what I used)
  2. 2. Log into the SDO Management console
  3. 3. In the left-hand navigation menu, click on SYSTEM SETTINGS and then click on Authenticators in the center top-line menu and finally, click on Templates in the authenticators tab options
Secret Double Octopus Authenticator Template Upload
  1. 4. Click on the UPLOAD TEMPLATE button and follow the prompts to upload the JSON file saved to your machine in step 1

Secret Double Octopus Authenticator Template Upload

Email Authenticator Template – JavaScript File

To implement the JavaScript file for this authenticator, follow these steps:

  1. 1. Save the below JavaScript code to a file named emailOTP_V2.js
  2. 2. SSH into your management console server
  3. 3. Create the new plugin file
    sudo vi /opt/sdo/plugins/emailOTP_V2.js
  4. 4. Paste the contents of the emailOTP_V2.js file you saved previously (or paste from the code block below). Write and Quit the file
  5. 5. Restart the SDO Management Service
    sudo systemctl restart sdomcbe

Configuring the emailOTP_V2 Authenticator

Now that we have our JSON template in place and the corresponding JavaScript code on the server, we can configure the authenticator! Follow these steps to configure the authenticator:

  1. 1. Log into the SDO Management Console web interface and navigate to System Settings –> Authenticators –> Authenticator List
  2. 2. Click on the ADD AUTHENTICATOR button
Configuring the Secret Double Octopus Authenticator
  1. 3. Enter a name for your authenticator, select the emailOTP_V2 authenticator template from the list and configure the SMTP Host section in the screen
Configuring the Secret Double Octopus Authenticator

  1. 4. Once you have filled in the relevant SMTP relay details, click the TEST CONNECTION button and confirm that you receive a successful connection, then click SAVE
Testing the Secret Double Octopus authenticator

  1. 5. To use your authenticator, you will need to configure your directory settings to utilize the authenticator. This is not covered in this blog post.

The Code

Standard disclaimers apply to the below code. This code is provided as is. It is up to you to review the code and to thoroughly test in your environment.

Authenticator JSON:

{
  "meta": {
    "methods": [
      "twoStepAuthenticator"
    ],
    "hideSendPassword": true
  },
  "fields": [
    {
      "value": "verificationMessage",
      "label": "Verification Message",
      "type": "text",
      "default": "Your verification code is %v",
      "description": "Authentication success message shown to users. The %v tag displays the code."
    },
    {
      "value": "codeLength",
      "label": "Verification Code Length",
      "type": "integer",
      "default": 6,
      "validators": {
        "required": true,
        "minimum": 4,
        "maximum": 8,
        "integer": true
      },
      "errorMessages": {
        "minimum": "Code length should be at least 4",
        "maximum": "Code length should not exceed 8"
      },
      "description": "Number of digits in the code"
    },
    {
      "value": "codeExpiration",
      "label": "Verification Code Expiration in Seconds",
      "type": "integer",
      "default": 60,
      "validators": {
        "required": true,
        "minimum": 30,
        "maximum": 600,
        "integer": true
      },
      "errorMessages": {
        "minimum": "Code expiration should be at least 30 seconds",
        "maximum": "Code expiration should not exceed 600 seconds"
      },
      "description": "Number of seconds for which the code is valid"
    },
    {
      "value": "divider1",
      "label": "div1",
      "type": "divider"
    },
    {
      "value": "host",
      "label": "SMTP Host",
      "type": "text",
      "validators": {
        "pattern": "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9](\\.?))$"
      },
      "errorMessages": {
        "pattern": "Not a valid SMTP host name"
      },
      "description": "IP address or hostname of the SMTP server"
    },
    {
      "value": "port",
      "label": "SMTP Port",
      "type": "integer",
      "description": "Port number for SMTP connection"
    },
    {
      "value": "fromEmail",
      "label": "From Address",
      "type": "text",
      "description": "From email displayed in account-generated mails"
    },
    {
      "value": "fromName",
      "label": "From Name",
      "type": "text",
      "description": "Sender name displayed in account-generated emails"
    },
    {
      "value": "subject",
      "label": "Mail Subject",
      "type": "text",
      "description": "Subject line displayed in account-generated mails"
    },
    {
      "value": "smtpAuth",
      "label": "SMTP Authentication",
      "type": "toggle",
      "description": "Toggle to enable/disable authentication. When enabled, enter the authentication credentials below."
    },
    {
      "value": "user",
      "label": "SMTP Username",
      "type": "text",
      "description": "Enter the authentication user credentials"
    },
    {
      "value": "pass",
      "label": "SMTP Password",
      "type": "password",
      "description": "Enter the authentication password credentials"
    },
    {
      "value": "security",
      "label": "SMTP Security",
      "type": "select",
      "options": [
        {
          "value": "None",
          "label": "None"
        },
        {
          "value": "SSL",
          "label": "SSL/TLS"
        },
        {
          "value": "STARTTLS",
          "label": "STARTTLS"
        }
      ],
      "description": "Select the appropriate security method for SMTP connection"
    }
  ]
}

Authenticator JavaScript:

const crypto = require('crypto');
const nodemailer = require('nodemailer');
const { v4: uuidV4 } = require('uuid');

module.exports = (conf) => {
  let smtpTransporter = createTransporter(conf);

  const authenticateTwoStep = async (loginInfo) => {
    if (loginInfo.challengeToken && loginInfo.answer) {
      // Handle the second step of verification
      return handleSecondStep(loginInfo.challengeToken, loginInfo.answer, conf.store);
    } else {
      // Generate and send a new OTP
      const code = generatePin(conf.codeLength);
      const message = conf.verificationMessage.replace(/%v/, code);
      const mailData = {
        from: `${conf.fromName} <${conf.fromEmail}>`,
        to: loginInfo.email,
        subject: conf.subject,
        text: message,
      };

      try {
        await smtpTransporter.sendMail(mailData);
        const token = uuidV4();
        await conf.store.set(token, { code, retries: 0 }, conf.codeExpiration);
        return { status: 'challenge', data: { token, challengeText: 'Enter the verification code sent to your email.', type: 'OTP', codeLength: conf.codeLength }};
      } catch (e) {
        return reject(3503, 'SMTP error: ' + e.message);
      }
    }
  };

  const test = async () => {
    try {
      await smtpTransporter.verify();
      return { ok: true };
    } catch (e) {
      return { ok: false, error: 'SMTP connection error' };
    }
  };

  return {
    test,
    authenticateTwoStep,
  };
};

async function handleSecondStep(token, inputCode, store) {
  try {
    const data = await store.get(token);
    if (!data) {
      return reject(3506, 'Invalid session');
    }
    const { code, retries } = data;
    if (code == inputCode) {
      await store.del(token);
      return { status: 'accept' };
    } else {
      if (retries < 2) {
        await store.update(token, { code, retries: retries + 1 });
      } else {
        await store.del(token);
      }
      return reject(3507, 'Verification code mismatch');
    }
  } catch (e) {
    return reject(3508, 'Failed to read code from store: ' + e.message);
  }
}

function generatePin(length) {
  let code = '';
  for (let i = 0; i < length; i++) {
    code += crypto.randomInt(10);
  }
  return code;
}

function reject(code, details) {
  return {
    status: 'reject',
    reason: {
      code,
      action: 3,
      text: 'Email error',
      details,
    },
  };
}

function createTransporter(conf) {
  const smtpConfig = {
    host: conf.host,
    port: conf.port,
    secure: false // Default to false unless specified otherwise
  };

  // Configure security settings based on the configuration
  if (conf.security === 'SSL') {
    smtpConfig.secure = true; // Use SSL/TLS
  } else if (conf.security === 'STARTTLS') {
    smtpConfig.secure = false;
    smtpConfig.requireTLS = true; // Upgrade connection with STARTTLS
  }

  // Apply authentication settings if SMTP authentication is enabled
  if (conf.smtpAuth) {
    smtpConfig.auth = {
      user: conf.user,
      pass: conf.pass
    };
  }

  return nodemailer.createTransport(smtpConfig);
}

That’s All Folks

I hope you found this post insightful and helpful. If you are interested in learning more about the Secret Double Octopus platform or if you would like assistance with implementing this code on your existing SDO installation, please feel free to reach out and Contact Us. We have additional information on the Secret Double Octopus platform located on our services page.