Verification

When a user wants to sign up for an account on your application, and you want to make sure they provide some way to contact them, it's a good idea to verify they actually are the owner of the contact information. It would be a problem if I signed up for 𝕏 with the email address of joe@whitehouse.gov, and then used that to impersonate the president.
Verification also helps you to reduce the number of spam accounts made on your site, since you can require that a user verify their email address before they can use it.
Finally, it reduces the number of typos that users make when entering their contact information. If you require that a user verify their email address before they can use it, then you (and they) can be sure that they entered it correctly.
But how you go about verifying the user's information is important. You need to make sure the only way to verify the information is if the user actually has access to it. This means you want to use a mechanism where making good or many guesses is difficult, and where the user can't be tricked into giving away the verification information.

Verification Codes

There are a few different approaches to verification, each with their own advantages and disadvantages. But most verification approaches involve sending the user a message with a code, and then asking them to enter that code into your application. For the code itself, you have options, but most prefer to send a short code that the user could easily enter in manually if they have to. Normally a six digit number is sufficient.
However, because this code is so short, there are only 1 million possible codes. Which may seem like a lot, but for a brute-force attack it's not at all. For this reason, it's important that you limit the number of guesses a user can make in a given time period to make it unfeasible to guess the code. This is called rate limiting, and it's important to do this for all verification codes.
In fact, rate limiting is an important part of any public API, especially for authentication (login, sign up, and verification). Good thing we put that in place earlier right?!

HMAC-based One Time Passwords

A good way to generate these short-lived codes is to use a one-time password following the HOTP algorithm. Feel free to dive into the RFC if that's your jam. We're going to use @epic-web/totp for generating our time-based one-time passwords based on this.
What's cool is doing things this way will make it much easier to use the same logic for adding two factor authentication later!
Here's an example of how this is done:
import { generateTOTP, verifyTOTP } from '@epic-web/totp'

// Here's how to use the default config. All the options are returned:
const {
	secret, // the unique secret for this verification
	period, // the number of seconds each code is valid for
	digits, // the number of digits in the code
	algorithm, // the algorithm used to generate the code (SHA1, SHA256, SHA512, etc.)
	charSet, // valid characters for the code (defaults to 0123456789)
	otp, // the current otp is generated and returned as a convenience
} = generateTOTP()

// now verify the code the user submits:
const code = await getVerificationCodeFromUser() // <-- however you do this
const isValid = verifyTOTP({
	otp: code,
	secret,
	period,
	digits,
	algorithm,
	charSet,
})
So for email or phone number verification, you can generate a code with a long period (like 30 minutes) and email the user the code. Once the time is up, it's no longer valid by design. And we can use the same thing for a password reset flow as well. Once the user verifies ownership, we can proceed knowing they are the owner of whatever it is we verified for them.
Then for 2FA, you can generate a code with a short period (like 30 seconds) and show the user the code in your app. Then they can enter it into their authenticator app (like 1Password) and have a new code generated every 30 seconds. But we'll get to that part later.
The most important part is that you need to verify the code the user submits with the same settings you used to generate the code. So we'll want to save all this information in the database when we generate the code. And then retrieve it again when we verify the code.

Clock Drift

Because the codes are dependent on time, it's important that the device generating the codes and the device verifying the codes are in sync. In an ideal world, both would be perfectly in sync. However, devices can sometimes be off by a bit, leading to valid OTPs being marked as invalid.
The standard approach to mitigating clock drift is to allow a certain number of time steps before and after the current time when verifying an OTP. For example, if your time step is 30 seconds, you might allow a 90-second window (one time step back and one time step forward).
@epic-web/totp supports this out of the box with the window option of the verifyTOTP utility. The return value is either null (if the code is invalid) or an object with a delta property. The delta represents how many codes off the given code is from where the server expected it to be. In practical situations, this will probably be 0 and should never be much more than 1 or 2.
However, this window option is only very useful for short-lived codes which have two separate devices for code generation and verification (like those used for two-factor authentication). For longer-lived codes (like those used for email or phone verification), it's better to just use a longer period and allow the user to request a new code if the old one has expired.