Two-factor auth for your Node project, without tears

  • How 2fa works with TOTP
  • Code
  • Adding bells and whistles
  • How secure is OTP really?

How 2fa works with TOTP

TOTP is short for time-based one-time password. This is a commonly used method for a second factor in two-factor authentication. The normal login flow for the user is then: first log in with username and password. Then you are presented with a second login form, where you have to enter a one-time password. Typically you will have an app like Google Authenticator or Microsoft Authenticator that will provide you with a one-time code that you can enter. If you have the right code, you will be authenticated and you gain access.

How does the web service know what the right one-time code is?

Consider a web application using two-factor authentication. The user has Google Authenticator on his or her phone to provide one-time passwords – but how does the app know what to compare with?

That comes from the setup: there is a pre-shared secret that is used to generate the tokens based on the current time. These tokens are valid for a limited time (typically 30, 60 or 120 seconds). The time here is “Unix time” – the number of seconds since midnight 1 January 1970. TOTP is a special case for a counter-based token created with the HOTP algorithm (HOTP = HMAC based one-time password). Both of these are described in lengthy detail in RFC documents, and the one for TOTP is RFC 6238. The main point is: both token generator (phone) and validator (web server) needs to know the current unix time and they need a pre-shared secret. Then the token can be calculated a function of the time and this secret.

A one-time password used for two-factor authentication is a function of the current time and a pre-shared key.

Details in RFC 6238
The basics of multi-factor authentication: something you know + something you have

How do I get this into my app?

Thanks to open source libraries, creating a 2fa flow for your app is not hard. Here’s an example made on Glitch for a NodeJS app: https://aerial-reward.glitch.me/login.

The source code for the example is available here: https://glitch.com/edit/#!/aerial-reward. We will go through the main steps in the code to make it easy to understand what we are doing.

Step 1: Choose a library for 2FA.

Open source libraries are great – but they also come with a risk. They may contain vulnerabilities or backdoors. Doing some due diligence up front is probably a good idea. In this case we chose speakeasy because it is popular and well-documented, and running npm audit does not show any vulnerabilities for the library although it hasn’t been updated in 4 years.

Step 2: Activate MFA using a QR code for the user

We assume you have created a user database, and that you have implemented username- and password based login (in a safe way). Now to the MFA part – how can we share the pre-shared secret with the device used to generate the token? This is what we use a QR code for. The user will then log in, be directed to a profile page where “Activate MFA” is an option. Clicking this link shows a QR code that can be scanned with an authenticator app. This shares the pre-shared key with the app. Hence: the QR code is sensitive data, so it should only be available when setting up the app, and not be stored permanently. The user should also be authenticated in order to see the QR code (using username and password).

In our example app, here’s the route for activating MFA after logging in.

app.get('/mfa/activate', (req, res) => {
  if (req.session.isauth) {
    var secret = speakeasy.generateSecret({name: 'Aerial Reward Demo'})
    req.session.mfasecret_temp = secret.base32;
    QRCode.toDataURL(secret.otpauth_url, function(err, data_url) {
      if (err) {
        res.render('profile', {uname: req.session.username, mfa: req.session.mfa, qrcode: '', msg: 'Could not get MFA QR code.', showqr: true})
      } else {
        console.log(data_url);
        // Display this data URL to the user in an <img> tag
        res.render('profile', {uname: req.session.username, mfa: req.session.mfa, qrcode: data_url, msg: 'Please scan with your authenticator app', showqr: true}) 
      }
    })
  } else {
    res.redirect('/login')
  }
})

What this does is the following:

  • Check that the user is authenticated using a session variable (set on login)
  • Create a temporary secret and store as a session variable, using the speakeasy library. This is our pre-shared key. We won’t store it in the user profile before having verified that the setup worked, to avoid locking out the user.
  • Generate a QRCode with the secret. To do this, you need to use a qrcode library, and we used the one qrcode, which seems to do the job OK. The speakeasy library generates an otpauth_url that can be used in the QR code. This otpauth_url contains the pre-shared secret.
  • Finally we are rending a template (the profile page for the user) and supplying the QR code data url to a template (res.render).

For rendering this to the end user we are using a Pug template.

html
  head
    title Login
    link(rel="stylesheet" href="/style.css")
  body
    a(href="/logout") Log out
    br
    h1 Profile for #{uname}
    p MFA: #{mfa}
    unless mfa
      br
      a(href="/mfa/activate") Activate multi-factor authentication
    if showqr
      p= msg
      img(src=qrcode)
      p  When you have added the code to your app, verify that it works here to activate.
      a(href="/mfa/verify") VERIFY MFA CODE
    if mfa
      img(src="https://media.giphy.com/media/81xwEHX23zhvy/giphy.gif")
      p Security is important. Thank you for using MFA!

The QR code is shown in the profile when the right route is used and the user is not already using MFA. This presents the user with a QR code to scan, and then he or she will need to enter a correct OTP code to verify that the setup works. Then we will save the TOTP secret in the user profile.

How it looks for the user

The profile page for the user “safecontrols” with the QR code embedding the secret.

Scanning the QR code on an authenticator app (many to choose from, FreeOTP from Red Hat is a good alternative), gives you OTP tokens. Now the user needs to verify by entering the OTP. Clicking the link “VERIFY MFA CODE” to do this brings up the challenge. Entering the code verifies that you have your phone. When setting things up, the verification will store the secret “permanently” in your user profile.

How do I verify the token then?

We created a route to verify OTP’s. The behavior depends on whether MFA has been set up yet or not.

app.post('/mfa/verify', (req, res) => {
  // Check that the user is authenticated
  var otp = req.body.otp
  if (req.session.isauth && req.session.mfasecret_temp) {
    // OK, move on to verify 2fa activation
    var verified = speakeasy.totp.verifyDelta({
      secret: req.session.mfasecret_temp,
      encoding: 'base32',
      token: otp,
      window: 6
    })
    console.log('verified', verified)
    console.log(req.session.mfasecret_temp)
    console.log(otp)
    if (verified) {
      db.get('users').find({uname: req.session.username}).assign({mfasecret: req.session.mfasecret_temp}).write()
      req.session.mfa = true
      req.session.mfarequired = true
      res.redirect('/profile')
    } else {
      console.log('OTP verification failed during activation')
      res.redirect('/profile')
    }
  } else if (req.session.mfarequired) {
    // OK, normal verification
    console.log('MFA is required for user ', req.session.username)
    var verified = speakeasy.totp.verifyDelta({
      secret: req.session.mfasecret,
      encoding: 'base32',
      token: otp,
      window: 6
    })
    console.log(verified)
    if (verified) {
      req.session.mfa = true
      res.redirect('/profile')  
    } else {
      // we are pretty harsh, thrown out after one try
      req.session.destroy(() => {
        res.redirect('/login')
      })
    }
  } else {
    // Not a valid 2fa challenge situation
    console.log('User is not properly authenticated')
    res.redirect('/')
  }
})

The first path is for the situation where MFA has not yet been set up (this is the activation step). This is checked that the user is authenticated and that there is a temporary secret stored in a session variable. This happens when the user clicks the “VERIFY…” link on the profile page after scanning the QR code, so this session variable will not be available in other cases.

The second path checks if there is a session variable mfarequired set to true. This happens when the user authenticates, if an MFA secret has been stored in the user profile.

The verification itself is done the speakeasy library functions. Note that you can use speakeasy.totp.verify (Boolean) or speakeasy.totp.verifyDelta (gives a time delta). The former did not work for some reason, whereas the Delta version did, which is the only reason for this choice in this app.

How secure is this then?

Nothing is unhackable, and this is no exception to that rule. The security of the OTP flow depends on your settings, as well as other defense mechanisms. How can hackers bypass this?

  • Stealing tokens (man-in-the-middle or stealing the phone)
  • Phishing with fast use of tokens
  • Brute-forcing codes has been reported as a possible attack on OTP’s but this depends on configuration

These are real attacks that can happen, so how to protect against them?

  • Always use https. Never transfer tokens over insecure connections. This protects against man-in-the-middle.
  • Phishing: this is more difficult, if someone obtains your password and a valid token and can use them on the real page before the token expires, they will get in. Using meta-data to calculate a risk-score can help: sign-in from new device requires confirmation by clicking a link sent in email, force password reset after x failed logins, etc. None of that is implemented here. That being said, OTP-based 2FA protects against most phishing attacks – but if you are a high-value target for organized crime of professional spies you probably should think about more secure patterns. Alternatives include push notifications or hardware tokens that avoid typing something into a form.
  • Brute-force: trying many OTP’s until you get it right is possible if the “window” is too long for when a code is considered valid, and you are not logged out after trying 1 or more wrong codes. In the code above the window parameter is set to 6, which is very long and potentially insecure, but the user is logged out if the OTP challenge fails, so brute-force is still not possible.

Creating a simple RSS reader with Python

I’ve tried to find a simple web based RSS reader but don’t really like any of the most popular ones. Too many ads, too much flashy graphics, and limits on number of feeds without paying.

Keeping track of many sources without a feed reader is cumbersome. This is why RSS is still relevant.

So, I started uilding one. This is work in progress, and is currently almost featureless. You can add private and public feeds. Tagging, search, etc is coming in the near future.

RSS feeds
RSS feeds

You can try it out by going to woodscreaming.com/rss.

Parsing RSS feeds with feedparser

RSS and atom feeds are XML files. It would be possible to parse these files and structure the contents from scratch – but fortunately we don’t need to do that. The excellent module feedparser can do it for us. Here’s what’s done in our little reader :

  • Parse it: fp = feedparser.parse(feed.url)
  • fp is now a dict containing the feed data. The most important fields are fp.title and fp.entries

Woodscreaming.com is a traditional Django app with minimal frontend processing. Creating the list of feeds is simply done by passing parsed feeds to a Django template.

Localization in Vue 2.0 single file components without using an i18n library

This post is a quick note on localization. Recently I needed to add some localization features to a frontend project running Vue 2.0. Why not just use an i18n package? That would probably have been a good thing to do, but those I found either had confusing documentation, a high number of old unresolved issues on Github, or warnings from npm audit. And my needs were very simple: translate a few text blocks.

Localization is not always easy – how hard is it to get it done without extra dependencies?

Starting simple

Let’s start with a simple Vue app in a single HTML file using a CDN to load Vue.

<html>
    <head>
        <!-- Import VueJS in dev version from CDN -->
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <title>Vue Translation</title>
    </head>
    <body>
        <div id="app">
            <h1>
                {{ translations.en.hello }}
            </h1>
            
        </div>
        <script>
            var app = new Vue({
                el: '#app',
                data: {
                    message: 'Hello Vue!',
                    translations: {
                        en: {
                            "hello": "Hello World"
                        },
                        no: {
                            "hello": "Hallo Verden"
                        },
                        de: {
                            "hello": "Guten Tag, Welt!"
                        }
                    },
                    locale: 'en'
                }
            })
        </script>
    </body>
</html>

This will render the English version of “Hello World”. Let us first consider how we are going to get the right text for our locale dynamically. We need a function to load that. Let’s create a methods section in our Vue app, and add a get_text() function:

 <h1>
   {{ get_text('hello') }}
</h1>
...
<script>
var app = new Vue({
  el: '#app',
  data: {
    ...
  },
  methods () {
    get_text (textbit) {
      return this.translations[this.locale][textbit]
    }
  }
})

OK – so this works, now we are only missing a way to set the preferred language. A simple way for this example is to add a button that sets it when clicked. To make the choice persistent we can save the choice to localStorage.

If you want the example file, you can download it from github.com. It is also published here so you can see it in action.

Translation – is this a good approach?

The answer to that is probably yes and no – it depends on your needs. Here’s what you will miss out on that a good i18n library would solve for you:

  • Dynamic tagging of translation links inline in templates. Making it easier to generate localization files for translators automatically.
  • Pluralization: getting grammar right can be hard. Going from one to several things is hard enough in English – but other languages may be much more difficult to translate because of more variation in the way words and phrases are used in different contexts.

The good things are: no dependencies, and you can use the data structure you want. Giving JSON files to translators works, just show them where to insert the translated strings.

Tip: use `string` for strings instead of ‘string’ or “string”. This makes multiline strings directly usable in your .vue files or .js files.

What about single-file components?

You can use this approach in single-file components without modification. A few notes:

  • Consider using a Vuex store to share state across components instead of relying on localStorage
  • It is probably a good idea to create a global translation file for things that are used in many components, to avoid repeating the same translation many times
  • Finally – if your translation needs are complicated, it is probably better to go with a well maintained i18n library. If you can find one.