A code is derived from a shared secret and the current time step, so the same code can reach the user three ways:
| Method | How the user gets the code | Secret lives |
|---|---|---|
app |
Scans a QR code once, then their device derives every code offline | Server and device |
email |
The server derives the code and sends it by email | Server only |
sms |
The server derives the code and sends it by SMS | Server only |
Generation and verification are the same two calls in all three cases.
getCode() is what lets the server derive the code it expects, so email and SMS need no second implementation.
app is the strongest of the three, because the code never travels. Offer the others to users who cannot or will not install an authenticator app.
A delivered code has to survive a mail queue or an SMS carrier, so it cannot expire after one time step of period seconds.
Widening verifyCode()'s $window is how its lifetime is extended, and it costs something: every step inside the window is a valid code, and a code stays valid for the whole window rather than being consumed on use.
Three measures pay for that, and none of them is optional.
The cooldown is per user. It does not slow an attacker spraying codes at many accounts, so rate limit the routes as well.