Yesterday I briefly mentioned a two-factor authentication scheme when separating my services and moving the secrets JSON file with encrypted data into its own separate database. Two-Step Verification requires an additional piece of information (tangible object or biometrics) in addition to the standard username and password to log into a system.
In this comprehensive exploration, I’ll delve into the multifaceted realm of Two-Factor Authentication (2FA), covering both hardware and software-based methods. Together, we’ll uncover the functionality of QR codes in securely storing provisioning secrets, while also addressing the vital topic of security concerns when displaying such codes. Additionally, we’ll navigate the intricate process of generating Time-Based and Counter-Based one-time passwords programmatically, tackling challenges like time drift and counter resynchronization along the way. Moreover, we’ll discuss strategies for mitigating security risks associated with large verification windows, including the utilization of multiple sequential tokens processed in batches. Throughout our journey, we’ll explore alternative recovery measures such as backup verification codes and examine the implications of assigning multiple 2FA tokens to a single account. Furthermore, we’ll navigate potential limitations inherent in 64-bit versus 32-bit programming, providing insights into the Year 2036 bug and the handling of large initialization counters. Lastly, we’ll delve into the nuances of prompting end-users to enter OTP codes, addressing accessibility concerns and the pitfalls of disabling password managers. Join me as we unravel the complexities of Two-Factor Authentication and equip ourselves with the knowledge to enhance digital security.
2FA protects the account further in case the credentials are compromised. My professional IT career begin working on a system where X.509 certificates stored on Smart Cards were used to log into a website. (Cutting edge at the time as Smart Cards were not mainstream in 1998, today everyones credit card has a smart card chip embedded in them).
2FA Prompt: Microsoft Designer
Show how two-factor authentication protects a users account when logging into a website. Usually a piece of software creates a six digit code every 30 seconds that is entered after the account credentials are verified. This one-time password (OTP) changes each time the user logs into the website. It’s based on a secret that was provisioned into an authentication app that only the user has access to. It prevents the account from being logged into if the account credentials were compromised in a data breach, since many users use the same passwords on multiple sites.
Rather than a static token, my focus today is primarily on tokens that change with each use, often referred to as One Time Passwords/Passcodes. Similar to NOnce, but not random. Since the token keeps changing, it’s made to be computationally difficult to work out what the next value will be without the secret that the OTP is based off of.
This process goes by many names and has a few associated terms:
- Two-Factor Authentication (2FA)
- Two-Step Verification
- Multi-Factor Authentication (MFA)
- Universal 2nd Factor (U2F)
- The three “Somethings”
- Something you know (password)
- Something you have (token)
- Something you are (biometrics)
- Security Token
- One-Time Password (OTP)
- Software Token
- Hardware Token
- SMS Token
- Email Token
- Authentication Methods
- Time-Based One-Time Password (TOTP)
- HMAC-Based One-Time Password (HOTP)
- Biometric Authentication
- WebAuthn
- Delivery Methods
- Email / Recovery Email
- SMS / Mobile Phone
- Phone Call
- Push Notification
- Google Authenticator
Two-factor authentication has often intrigued me. My first interaction with it was being issued a RSA SecurID used to log into my companies VPN. It was the size of a credit card about an eighth inch thick with an LCD screen that changed twice a minute. This is known as a time-based OTP. It lasted a few years before the batteries went out. Later in 2006, I was playing a game, Project Entropia (Now known as Entropia Universe), which tied its currency value in with the US dollar. As a safeguard to protect accounts, they offered a gold card, which was a counter-based 2FA. In March of 2008, they offered the card free to anyone with over 5,000 PED (Project Entropia Dollars) in their account. It ran much longer by only operating when a card was inserted. If the batteries died, you just got a new device to generate tokens, but used the same card. This is known as a counter-based OTP.
Project Entropia Experience
My interaction with Project Entropia is fairly limited. You didn’t just create an account and login. You had to be approved first. I was approved October 29, 2005. The world seemed empty. I was intrigued by the possibility of actually earning in-world currency that could be used to pay for the account or sold for real US cash. The currency was hard to acquire and I felt like I couldn’t progress much within the world. In addition, I didn’t care about its “instance” apartments in tall towers where everyone walked up to the same building and entered a shard of their home.
I remember “sweating” and a mentoring system that was difficult to figure out.
When I upgraded to Windows Vista 64-bit, Project Entropia stopped working. Unfortunately they deleted my account due to inactivity on March 10, 2012 with the promise I could activate it again and get my items back. I immediately reacted and found that I could not log in or restore my account with the link provided. I sent in a request for help with the promise that they’ll get back to me within 48 hours. I’m still waiting for a response…
It’s been over 12 years. As a joke I replied to the email and asked what the status was. I jokingly stated that it’s been over 48 hours.
On November 28, 2005 I created an account on Second Life, which also offered the ability to sell in-world currency for cash, but at market rates. I had spent many hours creating various gadgets and programming their interactivity. Project Entropia was more of a game, and couldn’t compete with the features of Second Lifes sandbox environment. At one time I had made the equivalent of 500 US$ in a month with in-world currency Linden Dollars. Without cashing out, the economy eventually tanked and the money I had been making wasn’t enough to cover the monthly cost of the “sim” that I was paying for.
Another piece of hardware that I’ve used are YubiKeys. You simply slide them into your USB port and touch them to activate the key. You don’t manually enter a key. It’s transferred on your behalf.
The idea behind 2FA is that you are not relying on the end-users password alone, but also a token that only they have access to. The problem that I have with hardware tokens is that they can stop working, they can be damaged, lost, or stolen. Without a token, the end-user is unable to perform their work. Once the device is no longer usable, you either need to have another device issued to you, or go through a lengthy process to legally prove who you are to either remove the 2FA or have another hardware token issued to you.
As mobile phones became smart phones, eventually Google came along and introduced the Google Authenticator app. It was a cheaper solution that operated on software alone without the overhead of purchasing, tracking, and mailing out physical hardware. The provisioning process could be automated. A website creates a random secret and creates a special link that opens the authenticator app to import the secret. Usually this link is conveyed briefly within a QR code that the user is able to photograph with a camera on their device. The QR code enables the secret to be transferred quickly without the possibility of typing it wrong. Once the end-user entered a matching token, you had confirmation that they are able to use it without issue.
Google Authenticator has its drawbacks. If the phone is lost, stolen, or inoperable, the end-user is stuck in a hard place where they can no longer use the system that it’s implemented on. The end-user isn’t (or wasn’t) able to backup/transfer the original secret. Many websites started implementing 2FA and not only offered the QR code, but you could also view the base32 encoded secret below the code. Since the majority of them are only Time-Based, I often stored these in 1Password as a backup solution. Eventually 1Password started supporting One-Time Passwords as well where it would generate the code for you, and even fill out the OTP value in forms when prompted during authentication. I’ve used many password managers on a daily bases in the past such as LastPass and KeyPass. My preference for 1Password stems from how it works on multiple operating systems and may use dropbox for storage. It’s easier to recover in many scenarios.


I had previously implemented a weak solution in JavaScript (See Schmuck Miser) using the npm packages otplib and qrcode. It was weak because it depended on the OTP secret being stored with the data it was protecting. This time I was interested in a pure coding solution to understand what was going on under the hood.
A similar concept to the One-Time Password used in 2FA is Rolling Codes with physical security systems, such as an automobiles keyless entry. However, the term is specific to these systems, where as 2FA is specific to user authentication.
Generating 2FA Secrets
Standard secrets are a randomly generated set of 80 bits (10 bytes). They are then encoded as base32 (as 16 characters), which only consists of upper-case letters and digits 2 through 7. The key length usually ranges between 80 to 256 bits. Although an app may have a default size for its secrets, many apps support variable sizes. Smaller key lengths (80/120 bits) help balance interoperability allowing the keys to be compatible with most software. Larger key lengths are more difficult to manually transfer the secret without a QR code, which would be larger to convey the larger key size. Higher sizes increase security while smaller key sizes improve ease of use.
| Software / App | Bits |
|---|---|
| 1Password | 256 |
| Authy | 80 |
| FreeOTP | 160 |
| Cisco Duo Mobile | 160 |
| Google Authenticator | 80 |
| LastPass Authenticator | 128 |
| Microsoft Authenticator | 128 |
Why is the minimum size 80 bits? I suspect that this has to do with how a 32-bit value is parsed from a hashed value anywhere between the first and 80th bit (… technically its between the 2nd and 80th bit as the signed value is discarded). Having a key that is also at least 80 bits long ensures that all values within this range don’t potentially have the secret repeated as the key. In that past, I’ve seen some algorithms make up for a short key length by repeating the key over and over again until the length matches what’s needed to be applied to the block of data.
Provisioning Secrets
Provisioning Secrets Prompt: Microsoft Designer
Display a digram where a secret is provisioned for two-factor authentication. The secret is embedded within a URL such as otpauth://totp/account-name?secret=ABCDEFGHIJ – furthermore, instead of displaying a URL, users are often shown QR codes that an Authenticator Application can scan. This provisioning process may include the account name, issuer, initial counter, period in seconds (15, 30, or 60), hashing algorithms (sha1, sha256, and sha512), and the number of digits to display. It also indicates whether it generates a time-based (totp) or counter based (hotp) one-time password.
The secret is provisioned as a url. It passes the secret as the base32 encoded value. Because encryption is not present, the link itself it should be sent over a secured transmission such as a website using https. Here is a simple link:
otpauth://totp/lewie?secret=AAAAAAAAAAAAAAAA
The link starts with the otp authentication protocol, so that the device knows that it’s for use in an authenticator app. The next value indicates the authentication method is a time-based OTP (totp). Next is a label for the user to know what the token is associated with, and last we have the base32 encoded secret. For a counter-based OTP it would look like this:
otpauth://hotp/lewie?secret=AAAAAAAAAAAAAAAA&counter=1
Notice that the method changed to hotp, indicating it is an HMAC OTP. It also includes the initial value to start counting from. Your server will not only need to securely store the secret, but it also needs to store the initial counter and update it each time the user provides the correct OTP. (It shouldn’t be updated if they fail the OTP validation).
Label
The label is url encoded and displayed to the end-user. It may be just an account name, or be prefixed with an issuer. The colon used to separate the two values is not url encoded. In this example, the issuer is “A B” and the account is “lewie”:
otpauth://totp/A%20B:lewie?secret=AAAAAAAAAAAAAAAA
Options
Not all options are recognized by authentication apps. Test and verify before using. Of all options, issuer is strongly recommended. I haven’t seen websites that make use of these other options when backing up secrets, so I’m strongly discouraging their use for the sake of portability/compatibility. If you use these options, you must reveal this information to the user if they choose to manually backup the secret.
| Name | Default | Values |
|---|---|---|
| issuer | should match label issuer if provided | |
| algorithm | sha1 | sha1, sha256, sha512 |
| digits | 6 | 6, 7, 8 |
| period | 30 | 15, 30, 60 |
otpauth://totp/A%20B:lewie?secret=AAAAAAAAAAAAAAAA&algorithm=sha256&digits=8&period=15&issuer=A%20B
QR Codes
For the time being, I’m skipping QR codes. If you wish to use QR codes, make sure to securely generate the code. Do not pass the secret as a query string parameter to another web page, as that will show up in website traffic logs. You can use a JavaScript library (npm qrcode) on the client side to generate the code on their end, or store the provisioned code in a database and pass the id to another web page to generate the code. Many solutions are available.
TOTP vs HOTP
Before we generate the OTP code, we need to determine what the value is that will be hashed. For a counter-based HOTP, this value is often stored in a database associated with the user. For a time-based OTP, we get the current Unix timestamp and divide it by the period of seconds (15, 30, 60). This number is then used for generating the OTP code.
Limitations
By now you’ve probably already worked out that 2FA has some serious limitations if not written correctly, and TOTP specifically could become inoperable on some systems in about 13 years – depending on if it is being used with a 32-bit or 64-bit timestamp.
TOTP: Similar to the Y2K bug and Mayan Long Count calendar, TOTP tokens can overflow when we run out of time. If using a 32-bit Unix timestamp, it could theoretically end January 19, 2038, at 03:14:07 UTC. It is referred to in many ways: 2038 problem or Unix Y2K38 problem. It’s not too far in the distant future. By comparison, a 64-bit timestamp ends on a Thursday, December 4, 292,277,026,596 AD 20:10:55 UTC.
HOTP: Counter based OTP’s have a different but similar problem. Depending on your initial counter value, you may be limiting the number of times the token will be usable if it’s close to rolling over 0x7FFFFFFF (2,147,483,647) or 0xFFFFFFFF (4,294,967,295) on a 32-bit signed/unsigned platform. If you provision a token with a counter of 2,147,483,647 – what will happen after they login? Will the counter roll over to -2,147,483,648? Will the code throw an exception? Will the database store the updated value correctly? Should 2FA be discarded and the user notified? Should we refuse to increment the counter? What happens to the token within the users software? The CPU, operating system, and programming language may support 64-bit integers, but was the application coded properly to handle 64-bit values?
Thankfully the algorithm itself has already addressed 64 bit values composed of 8 bytes. It is for this reason that you should store counters as 64 bit integers in your database, if you are choosing random values. If your counters are initially set to 1, then it’s “usually” safe to assume that the OTP wouldn’t be used more than 2,147,483,647 times during in its lifetime. You’ll need to evaluate the potential edge cases given how your system works with 2FA. The high usage would likely be related to automation or attacks.
It’s often the implementation in the given language that introduces the 32-bit limitation.
Let’s test that theory
I don’t always like to say something in theory and leave people hanging. I also like to verify. And verify we shall do. For all edge case testing, I set my secret at AAAAAAAAAAAAAAAA and tested with HOTP counters.
| Description | Counter | int32/64 big endian | OTP |
|---|---|---|---|
| -1 | -1 | 0xFFFFFFFF | 566304 |
| 0 | 0 | 0x00000000 | 328482 |
| 1 | 1 | 0x00000001 | 812658 |
| 2 | 2 | 0x00000002 | 073348 |
| Min int32 | -2,147,483,648 | 0x80000000 | 800111 |
| Max int32 | 2,147,483,647 | 0x7FFFFFFF | 691324 |
| Max int32+1 | -2,147,483,648 | 0x80000000 | 800111 |
| prior as uint32 | 2,147,483,648 | 0x80000000 | 427756 |
| Max uint32 | 4,294,967,295 | 0xFFFFFFFF | 844632 |
| max uint32+1 as int64 | 4,294,967,296 | 0x0000000100000000 | 001693 |
| Max int64 | 9,223,372,036,854,775,807 | 0x7FFFFFFFFFFFFFFF | 382334 |
| Min int64 | -9,223,372,036,854,775,808 | 0x8000000000000000 | 943357 |
| Min int64+1 | -9,223,372,036,854,775,807 | 0x8000000000000001 | 577156 |
| Max uint64 | 8,446,744,073,709,551,615 Got: 1.844674407371E+19 | 0xFFFFFFFFFFFFFFFF | Failed. Type checking saw float |
Well, it turns out that my PHP code is interpreting integers as signed 64 bit integers. Adding 1 does not overflow to a negative value, but results in an expression instead.
PHP Edge Case Testing
<?php
require_once '../common/Otp.php';
$secretKey = "AAAAAAAAAAAAAAAA";
$otpManager = new Otp($secretKey, 'hotp');
function test_otp($value, $message) {
global $otpManager;
echo "<li>$message<ol>";
echo "<li>Value: $value";
echo "<li>int32 big endian: 0x". strtoupper(unpack('H*', pack('N', $value))[1]);
echo "<li>int64 big endian: 0x". strtoupper(unpack('H*', pack('J', $value))[1]);
if(!is_int($value)) {
echo "<li>Not an integer. Got ".gettype($value);
}
try {
if(is_int($value)) {
echo "<li>URL: <a href=\"".$otpManager->generate_provisioning_uri($value)."\">Import</a>";
echo "<li>OTP: ".$otpManager->counter_based_otp($value);
}
} catch(Exception $e) {
echo "<li>OTP Exception: ".$e->getMessage();
}
echo "</ol></li>";
}
function main()
{
test_otp(-1, "Negative One");
test_otp(0, "Zero");
test_otp(1, "One");
test_otp(2, "Two");
test_otp(-2147483648, "Min 32 bit signed integer");
test_otp(2147483647, "Max 32 bit signed integer");
test_otp(2147483648, "1 + Max 32 bit signed integer");
test_otp(4294967295, "Max 32 bit unsigned integer");
test_otp(4294967296, "1 + Max 32 bit unsigned integer");
test_otp(9223372036854775807, "Max 64 bit signed integer");
test_otp(-9223372036854775807 - 1, "Min 64 bit signed integer");
test_otp(18446744073709551615, "Max 64 bit unsigned integer");
}
try {
main();
echo "<br>Done";
} catch (Exception $e) {
echo "<br>Error: " . $e->getMessage();
}
?>
Let’s test 1Password with these high counters. Nope. 1Password only works with TOTP tokens. Let’s try Google Authenticator… Google authenticator does not allow me to enter an initial counter when manually setting up a new token. It defaults to 1 as the initial counter. I decided to generate the provision links and open them with my mobile device. On iOS, it kept opening Settings/Passwords and kept failing to import, stating that a verification code could not be created from the URL. Okay… let’s try creating QR codes and import into Google Authenticator with the camera. Finding a “Free” QR Generator online that doesn’t inject its own website into the data is proving difficult. Eventually I found that I could use the following formula to generate a “pure” QR code that only contained the data encoded:
=IMAGE("https://api.qrserver.com/v1/create-qr-code/?size=150x150&data="&ENCODEURL(A1))

?secret=AAAAAAAAAAAAAAAA
&counter=2147483647
Yes – the Google Authenticator created a code based on the initial counter value. The OTP value matches what my code generated (844632). Now add one… um. It gave me an unexpected number. 427756. I tried relaxing the rules on my OTP Authenticator and see what zero and negative values would provide. Nothing came close. Either I am doing something wrong when converting the number to an 8 byte binary string, or the Google Authenticator app is only compatible with 32 bit integers.
It’s not you, It’s me…

I was doing something wrong. I forgot to remove the most significant bit after unpacking a 32 bit number from the hash, before performing a modulus operation on it. The OTP 427756 is for a counter with the value of 2147483648, which is the max value of a 32 bit signed integer + 1 (0x80000000).
Since this means Google Authenticator was correct, let’s try the max 32 bit unsigned integer… yes! The OTP matches the PHP generated OTP. Adding one to the value resulted in the 64 bit OTP. Next, lets try the max signed 64 bit number. Works fine. Next OTP – 943357… what is that for? I suspect it is the min value of a 64 bit integer. PHP thinks the type is a double, but type checking thinks it is a float. Let’s try to remove the type checking… yes. I’m getting past the error and I’m getting the same OTP for the minimum value for a signed 64 bit integer.


Upon further review, I found that PHP has problems interpreting the minimum value of a 64 bit integer as an integer, but adding 1 is fine. The OTP code matched the next value of 577156
It’s not me… It’s you

It looks like I just stumbled upon a bug in PHP itself (See php/php-src issue #14589, Mini Repo). I am unable to assign the literal value -9223372036854775808 to a variable and have it be interpreted as an integer. It seems like a parsing error since the work-around is to assign -9223372036854775807 and subtract 1 from it.
I think we’ve figured out what we need to know. Password managers don’t like counter-based OTP – probably because they need to retain the counter state in addition to the secret. The counter-state can get out of synchronization if using the password manager on multiple devices. To retain the state, the password manager would need to synchronize a central location each time the counter changes. This opens a potential risk if someone is monitoring the users internet traffic.
To support manual entry in authenticator apps, stick with initial counter values of one. Counters can go all the way to the max value of 64 bit integers (9,223,372,036,854,775,807) and roll over to a negative value (-9,223,372,036,854,775,808 or -9.2233720368548E+18). Using the minimum 64 bit negative integer causes problems with PHP’s type declarations for function arguments as it becomes an exponent value and interprets it as a float.
Generating OTP
We now have a number to hash. The algorithm is very simple: cast a 64-bit integer to 8 bytes, hash with raw secret, parse offset, parse offset data as 32 bit unsigned integer, apply modulus, pad with zero.
We start with decoding our secret into its raw form of bytes.
$key = base32_decode($secret);
Next we convert our input integer (counter/time) as an unsigned 64 bit binary value in big endian byte order. A value of 1 becomes 0x00 00 00 00 00 00 00 01 (8 bytes). Now we are ready for hashing.
$data = pack('J*', $input);
Basically, we are just using a keyed hash method known as HMAC (Hash-based Message Authentication Code). How does this differ from adding a salt value to hash your data? With HMAC, both parties share a secret key, and you are only validating that a hashed value (OTP) provided to you matches the hash result. With hashed salted values (passwords), you don’t have access to the original value being hashed – only the salt. You only know that combining the incoming secret with the salt should result in a specific hash.
$hash = hash_hmac('sha256', $data, $key, true);
We hashed our input with the secret. Next we grab the last nibble (4 bits) of the hashed value and get the offset (0 to 7).
$offset = ord(substr($hash, -1)) & 0xF
We then grab 4 bytes starting at the offset (0 = bytes 0 to 3, 7 = bytes 7 to 10). Note: This part of grabbing bytes at specific positions requires our hash algorithm to return a hash that is at least 80 bits (10 bytes) in length. This is probably why the default binary secrets were 80 bits when Google Authenticator made its debut. Afterwards, we need to unpack the four bytes as an unsigned 32 bit integer.
$value = unpack('N', substring($hash, $offset, 4))[1] & 0x7FFFFFFF
| Byte | Bit placement |
|---|---|
| 0 | Remove most significant bit 31 to 24 ($bytes[0] & 0b01111111) << 24 |
| 1 | 16 to 23 $bytes[1] << 16 |
| 2 | 8 to 15 $bytes[2] << 8 |
| 3 | 0 to 7 $bytes[3] |
We now need to convert the value into a N-digit number that the user needs to enter. We run a modulus check with 10 to the power of digit count. ie 10^6 for six digits.
$otp = $value % pow(10, $digits);
We then convert our number to a string, and pad it with zeros on the left to match the number of digits expected.
return str_pad($otp, $digits, '0', STR_PAD_LEFT);
That’s it. That’s the entire OTP generation in a nut shell. The veil has been lifted. The magic is gone. It’s very simple.
PHP Otp Class to generate secrets, provisioning urls, and OTPs
<?php
/*
* Otp Class
*
* This class provides functionality for generating and validating one-time passwords (OTPs)
* for two-factor authentication (2FA) using Time-based One-Time Passwords (TOTP) and
* Counter-based One-Time Passwords (HOTP) algorithms.
*/
class Otp
{
private static string $base32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
private static array $valid_algorithms = ['sha1', 'sha256', 'sha512'];
private static array $valid_digits = [6, 7, 8];
private static array $valid_periods = [15, 30, 60];
private static array $valid_types = ['totp', 'hotp'];
private string $type;
private string $algorithm;
private int $digits;
private int $period;
private string $secret;
/**
* Constructor for the Otp class.
*
* Initializes a new instance of the Otp class with the provided parameters.
*
* @param string $secret The secret key used for OTP generation. It must be in Base32 format.
* @param string $type The type of OTP authentication. Defaults to 'totp' (Time-based OTP). Valid values are 'totp' and 'hotp'.
* @param int $period The time period in seconds for TOTP authentication. Defaults to 30 seconds.
* @param int $digits The number of digits in the generated OTP. Defaults to 6 digits. Valid values are 6 to 8.
* @param string $algorithm The cryptographic hash algorithm used for OTP generation.
* Defaults to 'sha1'. Valid values are 'sha1', 'sha256', and 'sha512'.
* @throws Exception If the secret key is empty, invalid, or less than 80 bits.
* @throws Exception If the specified digits, algorithm, type, or period is invalid.
*/
public function __construct(
#[SensitiveParameter] string $secret,
string $type = 'totp',
int $period = 30,
int $digits = 6,
string $algorithm = 'sha1'
) {
self::checkSecret($secret);
if (!in_array($digits, self::$valid_digits)) {
throw new Exception("Invalid digits. Expected: " . implode(', ', self::$valid_digits));
}
if (!in_array($algorithm, self::$valid_algorithms)) {
throw new Exception("Invalid algorithm. Expected: " . implode(', ', self::$valid_algorithms));
}
if (!in_array($type, self::$valid_types)) {
throw new Exception("Invalid type. Expected: " . implode(', ', self::$valid_types));
}
if (!in_array($period, self::$valid_periods)) {
throw new Exception("Invalid periods. Expected: " . implode(', ', self::$valid_periods));
}
$this->secret = $secret;
$this->period = $period;
$this->digits = $digits;
$this->algorithm = $algorithm;
$this->type = $type;
}
public static function checkSecret(#[SensitiveParameter] string $secret)
{
if (empty($secret)) {
throw new Exception("Missing secret.");
}
$bits = strlen($secret) * 5;
if ($bits < 80) {
throw new Exception("Expected 80 bit secret or higher.");
}
if (!self::valid_base32($secret)) {
throw new Exception("Invalid secret.");
}
}
/**
* Generate a random secret key for OTP authentication.
*
* This method generates a random secret key with a specified number of bits,
* which is used for OTP authentication. The secret key is generated using
* cryptographically secure random bytes and encoded in Base32 format.
*
* @param int $bits The number of bits for the secret key. Must be 80 or higher
* and a multiple of 8. Default is 128.
* @return string The generated random secret key.
* @throws Exception If the number of bits is less than 80 or not a multiple of 8.
*/
public static function generate_secret(int $bits = 128)
{
if ($bits < 80) {
throw new Exception("Expected 80 bit secret or higher.");
} else if ($bits % 8 !== 0) {
throw new Exception("Bits must be a multiple of 8.");
}
$length = ceil($bits / 5);
$secret = '';
$chars = self::$base32_chars;
$bytes = random_bytes($length);
for ($i = 0; $i < $length; $i++) {
$index = ord($bytes[$i]) % 32;
$secret .= $chars[$index];
}
return $secret;
}
/**
* Retrieve the OTP at a specified offset.
*
* This method calculates the OTP at an offset relative to the current time or counter.
* For TOTP (Time-based One-Time Password), it calculates the OTP at an offset of time
* from the current time. For HOTP (Counter-based One-Time Password), it calculates the
* OTP at an offset of counter steps from the current counter value.
*
* @param int $offset The offset at which to retrieve the OTP.
* @param int|null $input The 64 bit base time or counter value. If not provided, the current time
* (for TOTP) or counter value (for HOTP) is used.
* @return string The calculated OTP.
* @throws Exception If the offset or input value is not an integer.
* @throws Exception If the input is not specified for HOTP.
* @throws Exception If the secret contains invalid Base32 characters.
*/
public function get_relative_otp(int $offset, int $input = null)
{
if (!is_int($offset)) {
throw new Exception("Expected offset to be integer.");
}
if ($this->type === 'totp') {
if ($input === null) {
$input = time();
}
$input += $offset * $this->period;
return $this->time_based_otp($input);
} else {
if ($input === null) {
throw new Exception("Expected input to be integer.");
}
return $this->counter_based_otp($input + $offset);
}
}
/**
* Generate a One-Time Password (OTP).
*
* This method generates a One-Time Password (OTP) based on the configured type
* of OTP algorithm. If the type is TOTP (Time-based One-Time Password), it generates
* a password based on the current time (or provided time if specified). If the type
* is HOTP (Counter-based One-Time Password), it generates a password based on the
* provided counter value.
*
* @param int|null $input The 64 bit base time (for TOTP) or counter value (for HOTP).
* If not provided, the current time (for TOTP) or counter value
* (for HOTP) is used.
* @return string The generated One-Time Password (OTP).
* @throws Exception If the input is not specified for HOTP.
* @throws Exception If the input is less than zero to TOTP.
* @throws Exception If the secret contains invalid Base32 characters.
*/
public function otp(int $input = null)
{
if ($this->type === 'totp') {
return $this->time_based_otp($input);
} else {
return $this->counter_based_otp($input);
}
}
/**
* Generate a Time-based One-Time Password (TOTP).
*
* This method generates a Time-based One-Time Password (TOTP) based on the specified
* time or the current time if not provided. The TOTP is calculated based on the time
* divided by the configured period and then passed to the generate_otp method for
* OTP generation.
*
* @param int|null $time The 64-bit Unix timestamp representing the base time for OTP generation.
* If not provided, the current time is used.
* @return string The generated Time-based One-Time Password (TOTP).
* @throws Exception If the secret contains invalid Base32 characters.
*/
public function time_based_otp(int $time = null)
{
if (empty($time)) {
$time = time();
}
$timestamp = floor($time / $this->period);
return $this->generate_otp($timestamp);
}
/**
* Generate a Counter-based One-Time Password (HOTP).
*
* This method generates a Counter-based One-Time Password (HOTP) based on the specified
* counter. The HOTP is calculated based on the provided counter and then passed to the
* generate_otp method for OTP generation.
*
* @param int $counter The 64-bit counter value used for OTP generation.
* @return string The generated Counter-based One-Time Password (HOTP).
* @throws Exception If the counter is not specified.
*/
public function counter_based_otp(int $counter = null)
{
if ($counter === null) {
throw new Exception("Counter not specified.");
}
return $this->generate_otp($counter);
}
/**
* Generate a One-Time Password (OTP) based on the input value.
*
* This method generates a One-Time Password (OTP) based on the provided input value.
* The input value is first checked for validity and then used to calculate the OTP
* using the HMAC-based One-Time Password (HOTP) algorithm. The resulting OTP is then
* returned after padding it to match the specified number of digits.
*
* @param int $input The 64-bit value used for OTP generation.
* @return string The generated One-Time Password (OTP).
* @throws Exception If the input value is not specified or is less than zero.
* @throws Exception If the secret contains invalid Base32 characters.
*/
private function generate_otp(int $input)
{
$data = pack('J*', $input);
$key = $this->base32_decode($this->secret);
$hash = hash_hmac($this->algorithm, $data, $key, true);
$offset = ord(substr($hash, -1)) & 0xF;
$value = unpack('N', substr($hash, $offset, 4))[1] & 0x7FFFFFFF;
$otp = $value % pow(10, $this->digits);
return str_pad($otp, $this->digits, '0', STR_PAD_LEFT);
}
/**
* Generate a URI for provisioning a new OTP token.
*
* This method generates a URI that can be used to provision a new OTP token
* with the specified parameters. The generated URI follows the Key URI Format
* specified by the Google Authenticator documentation. It includes parameters
* such as the secret key, algorithm, digits, period (for TOTP), counter (for HOTP),
* account name, and issuer name.
*
* @param int|null $counter The 64 bit initial counter value for HOTP tokens.
* @param string|null $account The account name associated with the OTP token.
* @param string|null $issuer The issuer name associated with the OTP token.
* @return string The URI for provisioning the OTP token.
* @throws Exception If there are any validation errors or invalid parameter values.
*/
public function generate_provisioning_uri(int $counter = null, string $account = null, string $issuer = null)
{
$url = 'otpauth://';
$url .= urlencode($this->type) . '/';
// Label Prefix
if (!empty($issuer)) {
if (strpos($issuer, ':') !== false) {
throw new Exception("Invalid issuer contains a colon.");
}
$url .= urlencode($issuer);
}
// Label Suffix
if (!empty($account)) {
if (strpos($account, ':') !== false) {
throw new Exception("Invalid account contains a colon.");
}
if (!empty($issuer)) {
$url .= ":";
}
$url .= urlencode($account);
}
$url .= "?secret=" . urlencode($this->secret);
if (!empty($issuer)) {
$url .= "&issuer=" . urlencode($issuer);
}
// Optional Parameters are excluded if they match default values
if ($this->algorithm !== "sha1") {
$url .= "&algorithm=" . urlencode($this->algorithm);
}
if ($this->digits !== 6) {
$url .= "&digits=" . urlencode($this->digits);
}
switch ($this->type) {
case 'totp':
if ($this->period !== 30) {
$url .= "&period=" . urlencode($this->period);
}
if ($counter !== null) {
throw new Exception('Initial counter is only for provisioning HOTP.');
}
break;
case 'hotp':
if ($count === null) {
throw new Exception('Initial counter is required for HOTP.');
}
$url .= "&counter=" . urlencode($counter);
break;
default:
throw new Exception("Invalid type: $this->type.");
}
return $url;
}
/**
* Decode a Base32 encoded string.
*
* This method decodes a Base32 encoded string into its original binary data.
* It is used internally for decoding secrets provided in Base32 format.
*
* @param string $encoded The Base32 encoded string to decode.
* @return string The decoded binary data.
* @throws Exception If the input contains invalid Base32 characters.
*/
private static function base32_decode(string $encoded)
{
$decoded = '';
$length = strlen($encoded);
$buffer = 0;
$bufferSize = 0;
$chars = self::$base32_chars;
for ($i = 0; $i < $length; $i++) {
$char = $encoded[$i];
$value = strpos($chars, $char);
if ($value === false) {
throw new Exception('Invalid base32 value.');
}
$buffer = ($buffer << 5) | $value;
$bufferSize += 5;
if ($bufferSize >= 8) {
$decoded .= chr(($buffer >> ($bufferSize - 8)) & 0xFF);
$bufferSize -= 8;
}
}
return $decoded;
}
/**
* Check if a string is a valid Base32 encoded string.
*
* This method verifies whether a given string is a valid Base32 encoded string.
* It checks if all characters in the input string belong to the Base32 character set.
*
* @param string $encoded The string to validate as Base32 encoded.
* @return bool True if the string is a valid Base32 encoded string, false otherwise.
*/
public static function valid_base32(string $encoded)
{
$length = strlen($encoded);
$chars = self::$base32_chars;
for ($i = 0; $i < $length; $i++) {
$char = $encoded[$i];
$value = strpos($chars, $char);
if ($value === false) {
return false;
}
}
return true;
}
}
Verifying OTP
With time-based OTP, we need to account for time drift. This is where either the server or the clients internal clocks differ. Many clocks are synchronized occasionally network time protocols (NTP). Somewhere down the line with various servers synchronizing, there is a GPS satellite with an atomic clock, a radio station broadcast (WWVB in Colorado) or a NIST time server.
While devices are keeping time on their own via a real time clock (RTC), the quartz crystal oscillator used to maintain the accuracy of time could drift based on temperature or aging. Although this is rare for quartz crystals since they have a low temperature coefficient. Quartz commonly oscillates at 32,768 Hz, allowing hardware to divide number of vibrations by 2^15 to indicate that one second has passed. A 15-stage binary counter is used to signal the next second. In addition to quartz, many mobile phones will periodically synchronize the clocks with the carrier, preventing any potential for time drift.
Another issue that affects time drift is the time it take the user to manually input the numbers. This is especially the case when taking accessibility into account for blindness, motor impairments, and dislexia. We aren’t as precise as a machine. In the time that it takes us to enter the last digit and click the submit button, the window may have closed. We could allow a user to increase the period up to 60 seconds, ensure the input is compatible with an authenticator app to fill out the OTP on the users behalf, or offer an alternative authentication method.
Last – the time on the users machine may be set incorrectly. Manually setting time to the exact second is something people don’t often focus on. Now that almost everyones device is connected to the internet, this is a non-issue for most people.
A 30 second window is fairly short already. To address time drift issues, many time based implementations will accept the prior and next OTP value as valid. Just remember that you need to add/remove one periods worth of seconds (15, 30, or 60) based on how you configured OTP.
Time Drift Prompt with Microsoft Designer
Demonstrate the concept of time drift where the internal clock of a device may not match the time on a server. A device uses the vibration of crystal oscillators to maintain time with a high precision, but may be affected by temperature and age.
$otp = $posted->getValue('otp');
$secret = $db->getScalar("CALL sp_otp_get_secret(?)", "s", $username);
$otpAuth = new OtpAuth($secret);
$time = time();
if($otp === $otpAuth->time_based_otp($time)) return true;
if($otp === $otpAuth->time_based_otp($time - 30)) return true;
if($otp === $otpAuth->time_based_otp($time + 30)) return true;
return false;
What about counter based OTP? You need to account for the number of attempts the user made while trying to log in. Each time the user generates a new OTP, the counter on their device increments. If they type the code incorrectly, they don’t get to use the same code again. Maybe they accidentally generated a code when they weren’t going to use it. If it’s a piece of hardware, maybe it dropped and bumped a button to generate a new code. Perhaps a child was playing with the counter, not knowing the impact it would have. Some custom applications may be able to synchronize the server with the internally stored counter on the device to keep the two in sync behind the scenes, but you still need to account for this drift.
Whatever the case, we can account for the drift. The first option is to account for the drift within a small window. The standard window size is between five to ten tokens. Whichever token matches gives us an indication of what the users internal counter value is.
$otp = $posted->getValue('otp');
$secret = $db->getScalar("CALL sp_otp_get_secret(?)", "s", $username);
$otpAuth = new OtpAuth($secret);
$counter = $db->getScalar("CALL sp_otp_get_counter(?)", "s", $username);
for($offset = 0; $offset < 10; $offset++) {
if($otp === $otpAuth->counter_based_otp($counter + $offset)) {
$db->execute("CALL sp_otp_set_counter(?, ?)", "si", $username, $counter + $offset + 1);
return true;
}
}
return false;
Just remember, the larger your window, the less secure your system is.
What if the token is outside of the window? Maybe an employee found a hardware token and decided to prank their coworker by generating 11 tokens when they weren’t looking.
If the end-user keeps failing to login with 2FA, the account needs to be locked to prevent further attempts. Someone may have compromised the users credentials through an alternative data breach, but is now just guessing the token. To unlock the account due to a 2FA failure, you could implement an automated resynchronization within a large window. In this scenario, you could prompt the user to enter multiple sequential tokens. The larger the window, the more tokens you should ask for to clamp down on security. You could scan through the next 100 or more tokens to see if all sequential tokens match.
$otp1 = $posted->getValue('otp1');
$otp2 = $posted->getValue('otp2');
$otp3 = $posted->getValue('otp3');
$secret = $db->getScalar("CALL sp_otp_get_secret(?)", "s", $username);
$otpAuth = new OtpAuth($secret);
$counter = $db->getScalar("CALL sp_otp_get_counter(?)", "s", $username);
for($offset = 0; $offset < 1000; $offset++) {
if(
$otp1 === $otpAuth->counter_based_otp($counter + $offset) &&
$otp2 === $otpAuth->counter_based_otp($counter + $offset + 1) &&
$otp3 === $otpAuth->counter_based_otp($counter + $offset + 2)
) {
$db->execute("CALL sp_otp_set_counter(?, ?)", "si", $username, $counter + $offset + 1);
return true;
}
}
return false;
With a large window of OTP’s, it could prove to be computationally expensive to process such a large number of OTP’s at once. It may be more ideal to process them in small batches and notify the end-user by email, SMS, or a push notification that a match was found, the account has been unlocked, and they can proceed to try again. If they fail the OTP again, then the account should be locked as well as the resynchronization process.
Why not go backwards, starting with the initial counter? Finding match in the past would prove that the device or OTP secret has been compromised. OTP should be used one time and no more. It’s in the name – One Time Password (OTP). An attacker with malware installed on the users device could monitor what OTP values were used over time and then reuse the earliest of three sequential values to synchronize the account to the earlier value. The attacker could then login with any recorded OTP token in the past after those three sequential tokens.
Attacks
2FA Bypass Attacks utilize a range of methods, such as phishing, social engineering, SIM swapping, brute force attacks, or exploiting authentication vulnerabilities.
- Phishing Attacks: replicas of websites or emails trick users into revealing 2FA codes. (Tycoon 2FA 2024)
- SIM Swapping: telecom providers are tricked into swapping a users phone number to another SIM card that the hacker owns, thereby receiving a users 2FA codes over their own phone. (Twitter 2020, Binance 2023)
- Man-in-the-Middle (MITM) Attack: Attackers see traffic between the user and the authentication server to obtain 2FA codes (Bitfinex 2016)
- Brute Force Attack: Attackers guess the users 2FA code by trying numerous combinations (Apple iCloud 2014)
As a software engineer, mitigating these risks involves educating users about proactive security measures. Implementing account recovery options allows users to regain control. Engineers also detect suspicious activity, implement account lockouts after failed OTP attempts, monitor for automated attacks, and counter them with rate limiting and CAPTCHA challenges. Additional measures may include recording IP addresses or locations to detect unrecognized logins and issuing extra challenges to users for verification. As a last measure, monitor known databases for breaches of your website, or your users. If an email address matches a user account, it could be recommended for them to change their password and 2FA. Following the breach of password management software (ie LastPass 2022, 1Password 2023), evaluate if your own users account credentials could have been affected and possibly require or request that all users to change their 2FA secrets after they have successfully logged in and verified their OTP.
Failed Synchronization
In this case, you need to revert back to another means to verify that the user is who they say they are. For automation, this could be an OTP sent to email, SMS, or push notification. If these options fail, it may be up to support staff to provision a new secret over the phone, in person, postal mail, courier, or a mail service. The secrets are short enough that they can be conveyed verbally and confirmed with a new OTP so that the account never has 2FA disabled.
Backup Verification Codes
Many websites provide a set of single-use backup codes when setting up two-factor authentication to provide emergency access as a fallback method. Potential situations would cover the loss of a mobile device that generates the OTP tokens. Backup codes provide a method of automated account recovery when 2FA fails.
Although backup codes are used with 2FA account recovery, the codes themselves are cryptographically random, alpha numeric and range from 16 to 32 characters. They may have separators to improve readability (ie 4F96-E8CF-732E-DBA0). You’ll typically see five to ten recovery codes. Too many recovery codes may overwhelm the user. Too few may not allow for enough backup options. These codes should be stored securely (encrypted) in the storage medium of your choice (Typically a database), and record when they are used to prevent reuse.
The codes are unique to the users account. No other account should have the same key. If the two-factor authentication is removed on a users account, then the backup codes should be removed as well – otherwise its a potential attack vector to gain access to the users account if they throw a printed copy of the codes into the trash after disabling 2FA. The backup codes are generated after the end-user supplies a valid OTP after setting up 2FA. The codes are only displayed during this time, and users are often prompted to confirm that they have saved the backup codes in a secure location (printer, encrypted file, password manager).
If the user is at-risk of running out of backup codes, they should be encouraged to revoke the existing codes and generate new ones.
Multiple 2FA Authentication
In some cases, a user may wish to have multiple authentication methods associated with their account. Similar to checking for multiple OTP tokens within a window, you can also check for multiple OTP tokens with multiple secrets. However, this essentially widens the window of valid tokens and becomes an easier attack vector. It’s preferable to only check against one secret. In some cases, an account will have a default 2FA token. In the case of a failure, you can let the user select from a list of tokens to use, having previously been assigned a nickname by the user to tell them apart. In this situation, before providing an OTP, the user should also be permitted to choose a token other than the default one associated with the account. This reduces the attack vector back down to only check OTP tokens agains a specific secret, rather than multiple secrets.
In another case, a strict security policy may actually require two separate 2FA secrets to be used during a login process, or a separate 2FA secret is used for critical or destructive operations. In this case, each prompt for an OTP token should make it clear which token is being prompted for.
Alternative 2FA Tokens
In some cases, you may want to provide your end-users with the ability to verify who they are by other means. Many sites offer alternative 2FA solutions by sending an OTP token to something the user has control of, with a larger window allowing for the time it takes for various systems to deliver the code to the user. Although beneficial to the end-user, you must be aware of how these systems can be compromised. Once an attacker knows that the target has changed, they can try to compromise and acquire the account or hardware instead.

| Target | Attack | Mitigation |
|---|---|---|
| Phishing, social engineering, network sniffing, etc. | Monitor failed login attempts, requests to change passwords and reset via email, or access from an alternative location. Notify the user of failed login attempts and changed account settings. Use breach monitoring services to detect if the users email account or username appeared in a breach. Prompt with multiple (3) security questions before sending an email to reset the account. | |
| SMS | SIM Swapping or intercepting messages in transit. Banners may display messages without logging into the phone, allowing housemates to see the OTP. | Encourage users to protect their mobile phone with a PIN. Protect their mobile carrier account with a PIN, password protection, or 2FA. Instruct how to disable text messages on a locked phone on popular devices (iOS, Android) |
| Instant Messanger | Compromise credentials on the platform. Monitor unencrypted traffic on the local network (ie Roommate is trying to monitor network traffic to see OTP). | Avoid. Not all platforms offer encryption. Many may not have sufficient security policies in place. On top of that, they often only require single-factor authentication to gain access. |
| Device Notification / Push Notification | Device theft, man-in-the-middle attack, malware | Allow users to nickname each device used in 2FA, and allow them to remove the device from having access. |
| Smart Cards | Stolen, use of card reader. | See hardware |
| NFC | Can be read by NFC readers (Flipper Zero) | See hardware |
| Hardware | Physical theft, loss, or firmware vulnerability/exploit | Allow users to report lost or stolen hardware. Monitor if the hardware is used, and turnover logs / ip addresses to the authorities. |
| Biometric | Spoofed with fake fingerprints, high-quality images for facial recognition, data on any authentication system compromised | Allow users to remove biometrics. Lockout biometrics if multiple failures occur and notify the user of the failed attempts. |
| Backup Codes | Stolen if not stored securely. Intercepted network traffic. | Ensure transmission is secure (https) and user instructed to store in secure location. |
| Voice Recognition | Playback pre-recorded message of each digit from prior conversation. Use generative AI to simulate the users voice patterns. | Use a dynamic challenge and response rather than repeating digits. Evaluate voice characteristics such as pitch, tone, and cadence. Listen for background noise. Continually authenticate during the entire conversation – even after voice was verified. |
Password Managers
Many password managers offer the ability to auto-populate the OTP value for the user. Depending on your security policy, you may or may not want to prevent this. In terms of accessibility, it may be a perfect solution – especially for people who have difficulty with seeing the tokens or moving quick enough to fill out the token within the period provided. Keep in mind, the end-user may not be aware that they have more than 30 seconds based on your time-drift implementation. At best, they only see a counter that quickly runs out every 30 seconds.
Accessible OTP fields
A password manager such as 1Password automatically associates a field with the name or id of “otp” as an OTP field and will show a list of accounts that have one-time password secrets that can generate the OTP.

Inaccessible OTP fields
Many websites like to make the OTP fields pretty. In some cases, you may want to create six separate input tags to hold each digit. From an accessibility point of view, this is a nightmare to setup to account for every possible situation. You may want to automatically focus the next input element as the end-user types in each digit. However, most users seeing multiple input fields will (by habit), tab to the next field – inadvertently skipping the second digits field. To guard against this behavior, you would start writing some javascript to disable the use of tabs in these fields (a big accessibility no-no). Not to mention, every fields is going to need a bunch of aria attributes to convey something that makes sense to visually impaired users. In addition, the multiple input fields wrecks havoc with authenticator apps.
If you are going to display an OTP input like this, you may as well use a third party that dedicated themselves to the task.
My recommendation is to stick with one input field and style it instead. CSS has come a long way. In just a minute, I added a background to delineate each cell, rounded the corners and increased the size. It needs a bit of work to align the numbers and handle visual appearance as each individual number is entered – but still, it moves in a direction that’s accessible to most password managers.


.otp {
display: block;
height: 10vh;
font-size: 10vh;
letter-spacing: 4vh;
width: 60vh;
border-radius: 4vh;
text-align: center;
outline: none;
box-sizing: border-box;
border: 1px solid silver;
font-family: monospace;
background: repeating-linear-gradient(to right, white 0, white calc(100% / 6 - .2vh), silver calc(100% / 6 - .2vh), silver calc(100% / 6));
}
Disabled OTP fields
As the author of the HTML, you can make an attempt to disable this functionality. Keep in mind that most users who have become accustomed to this capability may find that your website is difficult to use. I strongly discourage you from turning off this capability. Some security policies go so far as to disable pasting the OTP as well. Again – I highly discourage you from disabling this functionality as it affects usability and becomes highly irritable that the website is too aggressive in its security policy. Just because it increases security doesn’t mean you should do it. There is a limit where you can be too secure for your own good. This is why we don’t have 1 Gigabit encryption keys.
Stringent security policies affect user retention and encourage risky behavior
I refuse to use an app that prevents me from copy & pasting my credentials. One app (Dunkin’ Donuts) was so tightly locked down, that every time I switched between my password manager to look at the very long password password and back to manually type in the next set of characters, it had reset the form. The only way around it would be to reset the password and change it to a shorter/memorable password or use a separate device to view the password while typing it in. I was on the road and didn’t have time to work out the problem and didn’t bother using the app again (Dunkin’ Donuts Mobile App – Case #11468373 September 10, 2018).
Upon trying out the app just now, it looks like they fixed the issue as I can now log in using my password manager directly. In addition, if I start typing the password and switch to another application, the password is still there when I switch back to the app. One final touch for user accessibility is that they added the eye of providence to reveal the password at the users discretion. Good Job Dunkin’. I still have a balance. Maybe I’ll go buy a donut tomorrow.
Different password managers behave differently. They’ve changed in how they behave over the years as well, to the point that you have to explicitly call out their software to ignore the field or give it an odd name doesn’t represent what the underlying data consists of.
You need to understand that the authors of password managers are in the business of making accessing websites painless and more secure by assigning random and complex passwords to each website. A password manager often increases the security of a website with strong/unique passwords across multiple website. If another website has their data breached, you can rest easier knowing that most of your users that use password managers will not risk having their accounts compromised on your own website since they do not use the same credentials. It is in the authors of password managers best interest to ensure that your website works with their software. If you work against them, they will work harder to alleviate their users frustration.
Here are the attributes to add that disable populating fields that password managers/authenticators interact with. I recommend only doing this for fields that are NOT OTP fields, but are being modified by a password manager as if they were.
autocomplete="off": Original method, but no longer works with most password managersdata-1p-ignore: Only ignored in 1Passworddata-lpignore: Only ignored in LastPass
One tough player: <input size="6" id="otp" name="otp" autocomplete="off" data-1p-ignore data-lpignore />
Conclusion
Today, I’ve explored both hardware and software-based Two-Factor Authentication methods. I delved into the functionality of QR codes in securely storing provisioning secrets, while also highlighting security concerns when displaying such codes. My exploration extended to the intricate workings of generating Time-Based and Counter-Based one-time passwords programmatically. I examined challenges such as time drift and counter resynchronization. Additionally, I addressed security risks associated with large verification windows and proposed mitigating strategies, including the utilization of multiple sequential tokens processed in batches to prevent system overload. Furthermore, I discussed alternative recovery measures such as backup verification codes and the assignment of multiple 2FA tokens to a single account. Lastly, I navigated through potential limitations inherent in 64-bit versus 32-bit programming, particularly in anticipation of the Year 2036 bug and large initialization counters. I verified that counter-based OTP’s have a limitation of signed 32-bit integers and should be avoided when provisioning a new 2FA secret. When prompting the end-user to enter an OTP code, I went over accessibility concerns as well as how to disable password managers from interacting with the fields, and why it’s a bad practice to disable password managers interaction with an OTP field.



















One response to “Two-Factor Authentication”
[…] to rethink this problem. I need to have a separate backup key to encrypt the files. I can use the base32 encoding used in 2FA to encode the backup key and store it within my own password manager as printable ascii characters, […]