Implement two-factor authentication using sipgate.io

Daniel
20.10.2020 0 9:38 min

What is sipgate.io?

sipgate.io is a collection of APIs, which enables sipgate’s customers to build flexible integrations matching their individual needs.
Among other things, it provides interfaces for sending and receiving text messages and faxes, monitoring the call history, as well as initiating and manipulating calls.
In this tutorial we will use the capability of sending SMS to demonstrate how sipgate.io can be integrated as part of a two-factor authentication (2FA) service.

What is two-factor authentication and why is it important?

Traditionally authentication is simply performed by entering username/email and a secret password. This can be problematic when unauthorized actors get access to your credentials, since they can steal your identity.

To increase security and avoid a single point of failure, two-factor authentication can be used. It sends a temporary secret over a second channel, like SMS or email. Both, the credentials and the temporary secret, are necessary for a successful login. It is however advised to keep in mind that email is a less secure way to send temporary secrets as it is easier to compromise emails than mobile devices and their corresponding phone numbers. This does not mean that SMS is the most secure way of transmitting security tokens.

Another way of authentication is to send the secret via physical mail which will naturally take more time than the electronic variant. This practice is employed by banks and insurance companies alike, to name a few examples.

In this tutorial

This tutorial exemplarily covers the use of sipgate.io for sending authentication tokens via SMS as shown on our project page regarding two-factor authentication. For a complete 2FA service you will still need to integrate the code in this tutorial into your own service which will handle user authentication as well as generation, storage and verification of temporary tokens.

You can check out the whole project on GitHub.

With our SMS demo you can test the SMS capability of sipgate.io in your browser.

Choice of tools

We want to build a webservice which will be able to receive authentication tokens and recipient numbers, consequently sending a 2FA SMS to said recipient.
The SMS-sending capability will be provided by the sipgateio JavaScript library and we will use express to implement the server.

Setup

We will first create a new node.js project and create a server.js file containing our code.

npm init -y
touch server.js

After that we install our dependencies express and sipgateio.

npm install express
npm install sipgateio

Building the server and basic functions

Open the server.js in the editor of your choice. Our first step will be to import the dependencies we just installed.

const { createSMSModule, sipgateIO } = require("sipgateio");
const express = require("express");

Now we can create the server and configure it to accept JSON and to run on port 3000. We also set it up to deliver the content of the /html folder by using express.static().

const port = 3000;
const app = express();
app.use(express.json());
app.use("/", express.static(__dirname + "/html", { extensions: ["html"] }));

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

To use the sipgate.io library we need create the client with the login credentials we want use to send the SMS.

const tokenId = YOUR_SIPGATE_TOKEN_ID;
const token = YOUR_SIPGATE_TOKEN;
const smsExtension = "s0";

const client = sipgateIO({ tokenId, token });

We define the token credentials for authenticating to the REST API. The constant smsExtension contains the identifier of your sipgate SMS connection.
For more information about personal access tokens visit https://www.sipgate.io/rest-api/authentication#personalAccessToken.

You can find out what your extension is as follows:

  1. Log into your sipgate account
  2. Use the sidebar to navigate to the Connections (Anschlüsse) tab
  3. Click SMS (if this option is not displayed you might need to book the Web SMS feature from the Feature Store)
  4. The URL of the page should have the form https://app.sipgate.com/{webuserId}/connections/sms/{smsId} where {webuserId} is your webuser extension and {smsId} is the SMS extension that you are interested in.

The next step is the implementation of the sendAuthentificationSMS function.

async function sendAuthentificationSMS(number, token) {
  const message = `Hello, your two-factor authentification token is: ${token}`;

  const shortMessage = {
    message,
    to: number,
    smsId: SMS_EXTENSION,
  };
  const sms = createSMSModule(client);

  await sms.send(shortMessage);
}

In the function sendAuthentificationSMS we create an SMS module from our client object and use it to send the key. The function signature uses async in order to handle the asynchronous sms.send() function.

In order to send authentication tokens, we need to create them. The generateToken function simply returns a random number with 6 digits after saving it to our token storage.

function generateToken() {
  let token = "";
  for (let i = 0; i < TOKEN_DIGIT_COUNT; i++) {
    token += Math.floor(Math.random() * 10);
  }

  return token;
}

We also have to create a simple user database stored in database.json to log in later.

[{ "mail": "test@example.com", "phonenumber": "+4912345678901234" }]

In our server.js we want to implement the file loading as well:

const fs = require("fs");

const jsonFile = fs.readFileSync("database.json");
const userDatabase = JSON.parse(jsonFile);

const tokenStorage = {};

To read the JSON file we are using the fs module and parse the raw data into a JavaScript object called userDatabase. We will use the tokenStorage to save our generated tokens.

Note: In a real application you would connect your own database system.

Implementing the web application

This web application consists out of 3 HTML pages: index.html, verify.html and success.html which are placed inside the /html folder.

The entry page is the index.html:

<body>
  <h2>Login</h2>
  <form action="/login" method="POST">
    <input type="email" placeholder="Mail Address" name="mail" required />
    <input type="password" placeholder="Password" name="password" required />
    <input type="submit" name="submit" />
  </form>
  <p id="errorText"></p>
</body>
<script>
  const queryParams = new URLSearchParams(window.location.search);

  const errorTextElement = document.getElementById("errorText");
  const error = queryParams.get("error");
  if (error) {
    errorTextElement.innerText = error;
  }
</script>

With our stylesheet this might look like in the image below:

Entry page

In this file we are creating a simple form which consists of the input fields for the user mail address and password. This form will post its data to the url path /login. To keep it simple, errors will be handled by passing an error string through the query parameter error to the corresponding <p id=“errorText“></p> tag.

The next step is the implementation of our /login path.

app.post("/login", async (request, response) => {
  const { mail } = request.body;
  if (!mail) {
    response.redirect("/?error=Please enter a mail address");
    return;
  }

  const entry = userDatabase.find((entry) => mail === entry.mail);
  if (!entry) {
    response.redirect("/?error=Mail address not found");
    return;
  }

  const generatedToken = generateToken();
  tokenStorage[mail] = {
    token: String(generatedToken),
    date: new Date(),
  };

  try {
    await sendAuthentificationSMS(entry.phonenumber, generatedToken);
    response.redirect("/verify?mail=" + mail);
  } catch (error) {
    response.redirect(`/?error=error: ${error.message}`);
  }
});

First we define the POST-path /login using the express function app.post(). Inside the callback, we are referencing the submitted mail address by accessing the request.body.mail field.

After that we check if the mail is stored inside our database. To keep it simple we are not saving or checking the password. In your own code you should however verify your passwords against a database.

Once the basic authentication is taken care of, the next step is the token generation. We will then send the token generated by generateToken via SMS and redirect to the /verify page with the query parameter mail attached.

Now we create the verify.html page.

<body>
  <h2>Enter your 2FA token</h2>
  <form action="/verify" method="POST">
    <input type="hidden" value="" id="mail" name="mail" />
    <input type="text" placeholder="2FA token" name="token" />
    <input type="submit" name="submit" />
  </form>
  <p id="errorText"></p>
</body>
<script>
  const queryParams = new URLSearchParams(window.location.search);

  const mailFieldElement = document.getElementById("mail");
  const mailAddress = queryParams.get("mail");
  mailFieldElement.setAttribute("value", mailAddress);

  const errorTextElement = document.getElementById("errorText");
  const error = queryParams.get("error");
  if (error) {
    errorTextElement.innerText = error;
  }
</script>

Again, this looks like the following, if you are using our stylesheet:

Verify page

This page contains the form for submitting the token which the user received via SMS. We can now extract the mail address that we attached in the previous step from the url and place it into a hidden form field using a short JavaScript block. This is required in order to pass the mail address at form submission.

app.post("/verify", (request, response) => {
  let { mail, token } = request.body;
  if (!mail) {
    response.redirect("/verify?error=Mail address not set!");
    return;
  }

  if (!token) {
    response.redirect(`/verify?error=Token not set!&mail=${mail}`);
    return;
  }

  token = token.trim();
  const tokenPair = tokenStorage[mail];

  if (!tokenPair) {
    response.redirect(
      `/verify?error=No Token saved for given mail!&mail=${mail}`
    );
    return;
  }

  if (tokenPair.token != token) {
    response.redirect(`/verify?error=Token incorrect!&mail=${mail}`);
    return;
  }

  const expirationTime = new Date(tokenPair.date.getTime() + 5 * 60 * 1000);

  delete tokenStorage[mail];
  if (new Date() > expirationTime) {
    response.redirect(`/verify?error=Token expired!&mail=${mail}`);
    return;
  }

  response.redirect(`/success`);
});

On submitting the received token via the form to /verify we first check if the mail parameter was passed and exists as a key to a tokenPair in our database. We then check whether the token is correct and not expired. If everything is right, we redirect to a simple /success page. Otherwise, we respond to errors accordingly.

At this point, you may also consider to create dedicated OAuth tokens for your user sessions.

<body>
  <h1>Success!</h1>
  <p>Token validation was successful.</p>
</body>

After saving, you can now start up the server with npm run start.

Next steps

This blog post covers the use of sipgate.io for sending authentication tokens via SMS. For a complete 2FA service you will need to integrate your user database and a token generator, to link the user account with a temporary authentication token.

Additional notes

Keep in mind that you should avoid sending multiple SMS at once as this practice is not allowed by the sipgate service description.

Keine Kommentare


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert