The Copenhagen Book

Multi-factor authentication (MFA)

Table of contents

Overview

MFA is when a user is required to input more than just a password to authenticate. There are mainly 5 types of factors:

  • Something you know: Passwords
  • Something you have: Device, email address, SMS
  • Something you are: Biometrics
  • Somewhere you are
  • Something you do

Time-based one-time passwords (TOTP)

TOTP is defined in RFC 6238, which is based on hash-based one-time passwords (HOTP), defined in RFC 4226.

Standard TOTP uses an authenticator app, usually installed on the user's mobile device, to generate a code for the user.

Each user has a secret key. This is shared with the user's authenticator app with a QR code. Using that secret and the current time, the authenticator app can generate a new OTP. Your app asks for the current OTP and it can validate it by generating one using the same parameters. Since the current time is used to generate the code, each code is only valid for a set period (usually 30 seconds).

Generate QR code

HMAC SHA-1 is used to generate TOTPs. The secret key is exactly 160 bits and it must be generated using a cryptographically-secure random generator. Each user must have its own secret and the secret should be stored in your server. The secret can be encrypted before storage if you're worried about accidentally leaking your database records. It's important to remember that encrypting data won't protect against attackers who have system-level access to your servers however.

To share the secret, generate a key URI and encode it into a QR code. The secret is base32 encoded.

You should verify that the user has correctly scanned the QR code by asking for the generated OTP.

otpauth://totp/example%20app:John%20Doe?secret=JBSWY3DPEHPK3PXP&issuer=Example%20App&digits=6&period=30

When a user requests for a new QR code, generate a new secret and invalidate the previous one.

Validate OTPs

To validate a TOTP, we need to generate one first.

HOTPs are generated by signing a counter value with HMAC. In HOTP, the counter is an integer that is incremented whenever a new code is generated. But in TOTP, the counter is the current UNIX time divided by the interval (usually 30 seconds) with the fractional part truncated.

The counter, which should be 8 bytes, is hashed with HMAC SHA-1. 4 bytes are extracted using an offset. Then the last 31 bits is extracted and converted into an integer. Finally, the last 6 digit is used as the OTP.

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/binary"
	"fmt"
	"math"
)

func generateTOTP(secret []byte) {
	digits := 6
	counter := time.Now().Unix() / 30

	// HOTP
	mac := hmac.New(sha1.New, secret)
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, uint64(counter))
	mac.Write(buf)
	HS := mac.Sum(nil)
	offset := HS[19] & 0x0f
	Snum := binary.BigEndian.Uint32(HS[offset:offset+4]) & 0x7fffffff
	D := Snum % int(math.Pow(10, float64(digits)))
	// Pad "0" to make it 6 digits.
	return fmt.Sprintf("%06d", D)
}

To validate an OTP, you can simply generate one on your end and check if it matches with what the user provided.

Throttling must be implemented. A basic example is blocking attempts for 15 to 60 minutes after the 5th consecutive failed attempt. The user should also be notified to change the password as well.

SMS

We discourage SMS based MFA as it can be intercepted and unreliable at times. However, it may be more accessible than using authenticator apps. See the Email verification code guide for a guideline on implementing verification codes. The code should be valid for around 5 minutes.

Throttling must be implemented. A basic example is blocking attempts for 15 to 60 minutes after the 5th consecutive failed attempt. The user should also be notified to change the password as well.

Passkeys

Passkeys allow you to use in-device authentication methods, such as biometrics and pin-codes. See the Passkeys guide.

Recovery codes

If your application uses MFA, we recommend issuing users with 1 or more recovery codes. These are single-use passwords that can be used instead of passkeys/OTPs to sign in and reset their second-factor when a user loses access to their devices. The codes must be generated using a cryptographically-secure random generator. They can be generated with only 40 bits of entropy (10 characters when encoded with hex) assuming proper throttling is implemented.

Unless you can securely store these codes, we recommend hashing them with your preferred password hashing algorithm (e.g. Argon2id). In this case, the codes are only visible the first time the user registers their second-factor. User should also be given the option to regenerate them if they have access to their second-factor.