Verification
Loading "Intro to Verification"
Run locally for transcripts
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.