Experiments >

IdP: Multi-factor authentication

Experiment #23020th June, 2022by Joshua Nussbaum

Multi-factor auth (MFA) is an extra layer of protection so that if a password/e-mail address is comprimised, the attacker will need access to a second device, which makes attacking much harder.

It works by sending a user a challenge code, and there are mutliple ways of sending it. Most common are via e-mail, SMS or using an authenticator app.

Since e-mail and SMS are straightforward, I decided to investigate using an Authenticator app.

Authenticator QR code

Authenticator apps like Google Authenticator work by scanning a QR code to obtain a shared secret. The secret is embedded in a URL like:

otpauth://totp/<email-or-username>?secret=<secret>&issuer=<app-name>

For testing, a QR code can be generated via the CLI:

qrcode "otpauth://totp/user@example.com?secret=testtesttesttest&issuer=MyApp" | display

Then scan it with the app.

SVG QR codes

In a real web app, you probably don’t want to have to call a CLI to generate the QR code. Instead an endpoint can be used to generate it, and it’s a good idea to use SVG so it scales on all devices.

// src/routes/qr.svg.js
import QRCode from 'qrcode-svg'

const issuer = 'MyApp2'

export async function get({ request }) {
  const { user } = request.locals
  const url = `otpauth://totp/${user.email}?secret=${user.secret}&issuer=${issuer}`
  const qr = new QRCode(url)

  return {
    headers: {
      'content-type': 'image/svg+xml'
    },
    body: qr.svg()
  }
}

Verifying the code

Once the user submits their time based code (changes every 30 seconds), we can figure out what the expected code is using a package called totp-generator

// in src/routes/otp.js
import totp from 'totp-generator'

export async function post({ request }) {
  const data = await request.formData()
  const code = data.get('code')

  // generate expected code
  const user = request.locals.user
  const expected = totp(user.secret)

  if (expected == code) {
    // code matches, set cookie and redirect to homepage
    return {
      status: 303,
      headers: {
        'set-cookie': '...',
        location: '/'
      }
    }
  }

  return { status: 406 }
}
view all experiments

Stay tuned in

Learn how to add more experimentation to your workflow