If you care about your security on the web, you probably use a Two-Factor authentication (2FA) method to protect your accounts. There are various 2FA methods available out there, a combination of password + fingerprint, for example, is one of them. However, since not so many people have a fingerprint reader available all the time, one of the most popular 2FA methods today is to use an authenticator app on your cellphone to generate a temporary password that expires within a minute or even less. But, how does this temporary password, called Time-Based One-Time Password (TOTP) works, and how can I implement that on my own service? An abstract view This kind of authentication is not hard. It consists basically of issuing a secret key on your server and reading it on your phone, or any other device (generally using a QR code), then using this secret key to generate the passwords. That's why it works even when your phone is offline, because the secret key stored in your phone, and therefore it is perfectly capable of generating a TOTP for you. is Generating the shared secret key The TOTP algorithm is defined on the IETF , where it says the shared key " ". This key must be encrypted to be securely stored and should be decrypted only on two occasions: when validating a password that comes in and when exposing itself to be copied by another device, that should keep it encrypted too. To generate it, we can use 's secrets. RFC 6238 should be chosen at random or using a cryptographically strong pseudorandom generator properly seeded with a random value Python secrets secrets.token_hex( ) import -> str: def generate_shared_secret () return 16 # >> e8fb1a2faf331bfffe8670ca20447fae Note that this secret should be unique for every user on your database, this is what will guarantee that one user cannot generate a TOTP for another. Generating and validating a one-time password Now that we have a shared secret, we can generate and validate an OTP. The formula is simple: , where is the secret key we just generated and is a time step. In other words, we will encrypt the timestamp with our shared secret, but a raw timestamp wouldn't work, because the timeframe for the user to read and input the password would be zero. For this reason, we use a "step" factor, so the user gets more time. RFC 6238 recommends a step of 30 seconds, that may be sufficient for usability and security constraints. TOTP = HOTP(K, T) K T hashlib hmac math time now_in_seconds = math.floor(time.time()) step_in_seconds = t = math.floor(now_in_seconds / step_in_seconds) hash = hmac.new( bytes(shared_key, encoding= ), t.to_bytes(length= , byteorder= ), hashlib.sha256, ) dynamic_truncation(hash, length) import import import import -> str: def generate_totp (shared_key: str, length: int = ) 6 30 "utf-8" 8 "big" return We could just return the HMAC hash, but the output is way too long for the user to type (even more when there are only 30 seconds to do this). For this reason, we use the to get a sample of it, usually of six digits. It was developed for the predecessor of TOTP, at and consists of four steps: dynamic truncation algorithm RFC 4226 Convert the hash (base 16) into a binary string (base 2) Get the last four bits as an integer (base 10) Use this integer as an offset and get the next 32 bits of the binary string Convert this 32 bits to integer and get the last X digits, where X is the length you want to use bitstring = bin(int(raw_key.hexdigest(), base= )) last_four_bits = bitstring[ :] offset = int(last_four_bits, base= ) chosen_32_bits = bitstring[offset * : offset * + ] full_totp = str(int(chosen_32_bits, base= )) full_totp[-length:] -> str: def dynamic_truncation (raw_key: hmac.HMAC, length: int) 16 # >> 11010100000110011101010100010001100100011111001010111010001010110110000010111101000101011110111111010111101100011101001111100001011111101100001011110111100111001111100000100010011001010110101111100010111001001000010000011000000010010111100110101100011 -4 # >> 0011 2 # >> 3 8 8 32 # >> 01000100011001000111110010101110 2 # >> 1147436206 return # >> 436206 For the example above, 436206 is the temporary password the user would use. Now, how to validate a password on the backend? It is exactly the same. We generate the TOTP on the server using the shared key and check if it matches the input. totp == generate_totp(shared_key) -> bool: def validate_totp (totp: str, shared_key: str) return Conclusion Implementing 2FA is not hard, but must be taken seriously to avoid breaches. No security protocol is perfect, there is no silver bullet, but why not make invaders lives harder? Do a favor for you and your users: Don't implement this by hand. Instead, prefer using a library that is constantly updated with the best security practices. This article is to understand what is going on behind the scenes, hope you've enjoyed. The code snippets are available at my . GitHub