How I implemented TOTP-based 2FA for my Markdown note app

How I implemented TOTP-based 2FA for my Markdown note app

Hey, what's up? This is Takuya. I'm building a Markdown note-taking app called Inkdrop. As of v5.11.0, it supports two-factor authentication with a time-based one-time password. Thank you so much for the feedback during the beta testing!

Inkdrop v5.11.0 - Two-factor authentication support and big launch speed improvement
Hey Inkdroppers, I’m happy to announce that Inkdrop v5.11.0 is officially available! 🔐 Two-factor authentication support You can now enable a time-based one-time password (TOTP) on your account to protect against unauthorized access to your account and notes. Check out the docs on how to enable 2FA. Thank you so much for testing the beta version of 2FA, @Lukas and @bundit_jianpinitnan! 💨 Big launch speed improvement for Windows It addresses the following reporte…

In this article, I'd like to share how I implemented this feature.

My case is a bit different from typical cases because my app supports end-to-end encryption (E2EE) for its data sync across devices and platforms. So, it can't simply rely on any IAM (identity and access management) platforms like Auth0 to authenticate users because they don't natively support E2EE mechanisms. They only have the responsibility to deal with credentials on your behalf securely. But this is not a big problem. There are a lot of libraries that let you support 2FA.

What is TOTP?

A time-based one-time password (TOTP) is a temporary passcode generated by an algorithm that uses the current time of day as one of its authentication factors (ref). It is a commonly used method for two-factor authentication, and perhaps you may have already used it on other web services.

Choosing an OTP Library: time2fa

I've tried several libraries –– speakeasy looked like the most popular choice but it is no longer maintained. otplib and node-2fa looked promising, but the last commit dates of them were 4 years ago.. 🤔 I saw that some articles published recently used these libraries without mentioning their last update dates. Is this normal?

Maybe that's because these libraries are simple enough, but I was nervous to rely on these old libraries in production.

Then, I came across time2fa:

GitHub - PlanetHoster/time2fa: A comprehensive Node.js package that simplifies the implementation of One-Time Password (OTP).
A comprehensive Node.js package that simplifies the implementation of One-Time Password (OTP). - PlanetHoster/time2fa

The maintainers are from a company called PlanetHoster, one of the hosting services. I looked into the repository. This library has zero dependencies, which is nice. It is written in TypeScript. Good. The docs looked adequate. And most importantly, the source code looked simple, so maybe I can maintain it myself just in case if it is abandoned in the future.

So, I decided to go with time2fa.

How to generate a key

It is super easy:

import { Totp, ValidTotpConfig } from 'time2fa'
import * as qrcode from 'qrcode'

export async function generate(userId: string): Promise<MFAKeyWithQrCode> {
  const key = Totp.generateKey({ user: userId, issuer: 'inkdrop' })
  const qrcodeData = await qrcode.toDataURL(key.url)
  
  return { key, qrcodeData }
}

// GenerateKey {
//   issuer: 'inkdrop',
//   user: 'foobar',
//   config: { algo: 'sha1', digits: 6, period: 30, secretSize: 10 },
//   secret: 'ABCDEFGHIJKLMN12',
//   url: 'otpauth://totp/N0C:johndoe%40n0c.com?issuer=N0C&period=30&secret=ABCDEFGHIJKLMN12'
// }

You should store secret in your database.

How to validate a passcode

export function validate(secret: string, testPasscode: string) {
  const isValid = Totp.validate(
    {
      secret: key.secret,
      passcode: testPasscode
    },
    newKey.config
  )
  
  return isValid
}

As you can see, it doesn't require any external services to implement TOTP.

Generate backup code

Many websites provide backup codes in case you lost access to your second factor device.
These codes are a single use only.

time2fa supports generating backup codes:

import { generateBackupCodes } from "time2fa";

const backupCodes = generateBackupCodes();

// [
//   '810550', '236884',
//   '979342', '815504',
//   '835313', '529942',
//   '263100', '882025',
//   '204896', '516248'
// ]

However, these look not so secure as they are just 6-digit numbers.
Instead, I implemented one that generates codes with the GitHub-style like so:

/**
 * Generates backup codes like:
 *
 * ```js
 * [
 *   '4ba45-a48a8',
 *   'e4e5d-8cb58',
 *   '66170-ef736',
 *   '28554-180c0',
 *   '53735-603e8',
 *   'f8271-19a2b'
 * ]
 * ```
 */
export function generateBackupCodes(count = 6): string[] {
  const CODE_LENGTH = 5
  const CHARSET = 'abcdefghjkmnpqrstuvwxyz23456789' // Excludes ambiguous chars

  function generateCodePart(length: number) {
    return Array.from({ length }, () =>
      CHARSET.charAt(crypto.randomInt(CHARSET.length))
    ).join('')
  }

  return Array.from(
    { length: count },
    () => `${generateCodePart(CODE_LENGTH)}-${generateCodePart(CODE_LENGTH)}`
  )
}

Using v0.dev to get UI inspiration

To implement UIs for setting up 2FA, I used v0 to get some inspiration. You can view my v0.dev mocks here:

TOTP setup page – v0 by Vercel
Create a page for enabling two factor authentication with TOTP that displays a QR code and an input for testing passcode generated with user’s authenticator…
  • Prompt: Create a page for enabling two factor authentication with TOTP that displays a QR code and an input for testing passcode generated with user's authenticator app. I'm using React and Semantic UI. Do not use supabase or any PaaS, just for static UI for mocking.

It generated the first page like this:

I didn't use the generated code because v0 naturally ignored my request on using Semantic UI but it used Next.js and TailwindCSS. Just for inspiration.

  • Prompt: ok, next, create a result page for after successfully enabling the 2FA.

Here is the second-page mock:

Alright, it looks good.

I've implemented UIs like so:

Implementing the login UI

The next step is to implement the login screen.

Here is the v0 mock:

Desktop (Electron)

Here is my implementation for the desktop app:

Mobile (React Native)

And the mobile app:

I've created a custom OTP input that consists of 6 input bars.
I've shared the code snippet on Inkdrop here:

OTP input for React Native
## Prerequisites - [Restyle](https://github.com/Shopify/restyle) ## Screenshot ![iOS](inkdrop://file:obJCxcYD) ## Code ```tsx import Re

Note that My UIs are implemented with Restyle.


Hope this helps for building your SaaS! 🙌

Get Inkdrop – A developer-focused Markdown note-taking app

Inkdrop - Note-taking App with Robust Markdown Editor
The Note-Taking App with Robust Markdown Editor