Many people falsely assuming that because the code rotates often, it is safe from brute force. This is not the case. If we assume 100 attempts per second and a code that is valid for 30 seconds, an attacker has 100*30 = 3000
guesses before the code rotates, this has a 3000 / 10^6 = 0.3%
chance of success. This may seem low but this is only for 30 seconds, an attacker can try again for subsequent codes. For a 50% chance of success, they need only try for ln(50%) / ln(99.7%) = 230.7
30 second periods, or about 2 hours.
If you're not sure why rotating codes doesn't solve this, try thinking of it this way: an attacker can guess the range 000000 to 002999 for each code, and a code has a 3000 / 1000000 = 0.3%
chance of being in that range, so after generating many codes there is a good chance that one will eventually fall in that range.
If you rate limit attempts to 1 every 5 seconds, it becomes ln(50%) / ln(99.9994%)
30 second periods, about 40 days. This still may not be good enough for a high value account, so I would definitely recommend additional measures to prevent brute force. What these measures should be though is harder to answer, as stated in the comments completely locking an account due to failed second factor attempts may allow DoS, and in general could be frustrating for users. It may be sufficient to log each attempt and warn the user to change their password if it passes a certain number of attempts per minute for the past day or so.
The Math
Let s be the chance of success for a single 30 second period
Let n be the number of 30 second periods
Let t be the total chance of success over all periods
If you have s
chance of success, then you have 1 - s
chance of failure. If you try multiple times, you can multiply the chance of failure for each try to get the total chance of failure, so (1 - s)^3
is the chance of failing with 3 tries, and 1 - (1 - s)^3
is the chance of succeeding in at least 1 of 3 tries. This allows us to come up with the following equation:
t = 1 - (1 - s)^n
Then we can solve for n
given s
and t
:
(1 - s)^n = 1 - t
ln( (1 - s)^n ) = ln(1 - t)
n * ln(1 - s) = ln(1 - t)
n = ln(1 - t) / ln(1 - s)
So for t = 0.5
and s = 0.003
:
n = ln(1 - 0.5) / ln(1 - 0.003)
= ln(0.5) / ln(0.997)
≈ 230.7
If you want to know how long it would take an attacker to have a 99% chance of success with 6 tries per period (1 every 5 seconds), s = 6/1000000 = 0.000006
and t = 0.99
, giving about 767,526 periods or 266.5 days. For a 1% chance at the same rate t = 0.01
, which gives about 1675 periods or 14 hours.