Python 3 and a terminal, plus one library for step 3. No account of yours is touched
and nothing connects to the internet. macOS and Linux include Python; on Windows install
it from python.org with “Add python.exe to PATH” ticked. Step 3
needs the cryptography library, installed once with
python3 -m pip install cryptography; the other four steps need nothing extra.
If you have not yet turned on two-factor authentication anywhere, do that first and read this afterwards. Everything below compares the different kinds and shows where the weaker ones fail — but the gap between “no second factor” and “the weakest second factor” is far larger than any gap discussed here.
What Is Two-Factor Authentication?
Two-factor authentication (2FA) adds a second verification step when you log in to an account. Instead of relying solely on your password (something you know), 2FA requires a second factor from a different category: something you have (like your phone or a hardware key) or something you are (like a fingerprint). Even if an attacker steals your password, they cannot access your account without the second factor.
Think of it like a bank vault that requires both a key and a combination. Having one without the other is useless. 2FA works the same way -- your password is the combination, and your second factor is the key. An attacker who obtains your password through a data breach, phishing, or guessing is still locked out.
With billions of passwords exposed in data breaches, a password alone is no longer sufficient protection for any account that matters to you. 2FA is the single most effective defense against account takeover attacks. If a service offers it and you are not using it, you are leaving the door unlocked.
Types of Two-Factor Authentication
Not all 2FA methods are created equal. They range from basic (better than nothing) to highly secure (resistant to sophisticated attacks). Understanding the differences helps you choose the best option available for each account.
SMS Codes
The service sends a one-time code to your phone number via text message. This is the most common form of 2FA and the weakest. SMS messages can be intercepted through SIM swapping attacks, where an attacker convinces your phone carrier to transfer your number to their SIM card. SS7 protocol vulnerabilities also allow interception of SMS messages in transit. Despite these weaknesses, SMS 2FA is still significantly better than no 2FA at all.
TOTP (Authenticator Apps)
Time-based One-Time Password (TOTP) is an algorithm that generates a six-digit code that changes every 30 seconds. The code is generated locally on your device using a shared secret that was exchanged when you set up 2FA. Because the codes are generated offline, they cannot be intercepted in transit like SMS messages. This is the recommended method for most people.
How TOTP works:
1. Setup: The service gives you a secret key (usually as a QR code)
Secret: JBSWY3DPEHPK3PXP
2. Your authenticator app combines:
Secret key + Current time (rounded to 30-second intervals)
↓
HMAC-SHA1 hash
↓
6-digit code: 482903
3. The server performs the same calculation independently.
If the codes match, access is granted.
4. After 30 seconds, both sides generate a new code automatically.
Hardware Security Keys
Physical USB or NFC devices (such as YubiKey or SoloKeys) that perform cryptographic authentication. You plug the key in or tap it when prompted during login. Hardware keys are the strongest form of 2FA because they are immune to phishing -- the key verifies the website's identity before responding, so a fake site gets nothing. They are also immune to remote attacks since the attacker would need to physically possess the key.
Push Notifications
Some services send a push notification to their mobile app asking you to approve or deny the login attempt. This is convenient but can be vulnerable to "MFA fatigue" attacks, where an attacker repeatedly triggers login attempts hoping you will eventually tap "Approve" just to make the notifications stop. Services that show a number to match (like Microsoft Authenticator) are more resistant to this technique.
If you receive an unexpected push notification or 2FA request, someone is trying to log in to your account. Do not approve it. Instead, immediately change your password for that account. The unexpected prompt means your password is already compromised.
Setting Up an Authenticator App
An authenticator app is the best balance of security and convenience for most people. Here is how to set one up from scratch.
Choosing an App
- Aegis Authenticator (Android) -- Free, open-source, supports encrypted backups. The recommended choice for Android users.
- Raivo OTP (iOS) -- Free, open-source, iCloud sync. A strong choice for iPhone users.
- KeePassXC (Desktop) -- Your password manager can also store TOTP secrets, keeping everything in one encrypted vault.
- Google Authenticator -- Widely known but lacks encrypted backup and export features. Acceptable if no better option is available.
Adding an Account to Your Authenticator
- Step 1: Go to the security settings of the account you want to protect (e.g., your Google Account, GitHub, or any service that supports 2FA)
- Step 2: Find the two-factor authentication or two-step verification option and select "Authenticator app" as the method
- Step 3: The service will display a QR code. Open your authenticator app and scan it
- Step 4: The app will start generating 6-digit codes. Enter the current code on the website to confirm setup
- Step 5: The service will provide backup codes. Save these immediately (covered in the next section)
When a service shows you the QR code, there is usually an option to view the secret key as text (a string like JBSWY3DPEHPK3PXP). Save this text in your password manager alongside the account password. If you lose your phone, you can re-add the account to a new authenticator app using this secret without needing to go through the service's recovery process.
Enabling 2FA on Popular Services
Most major online services now support 2FA. Here is where to find the setting on commonly used platforms. The exact menu names may change over time, but the general location remains the same.
Service Where to find 2FA settings
--------- ----------------------------
Google Security → 2-Step Verification
Microsoft Security → Advanced security options → Two-step verification
Apple Settings → [Your Name] → Sign-In & Security → Two-Factor Authentication
GitHub Settings → Password and authentication → Two-factor authentication
Facebook Settings → Security and Login → Two-Factor Authentication
Instagram Settings → Security → Two-Factor Authentication
Twitter/X Settings → Security and account access → Security → Two-factor authentication
Amazon Account → Login & security → Two-Step Verification
Discord User Settings → My Account → Enable Two-Factor Auth
Reddit User Settings → Safety & Privacy → Two-factor authentication
Start with your email account -- it is the master key to everything else because password reset links go there. Then secure your financial accounts, cloud storage, social media, and any service where a compromise would cause significant damage. Work outward from the most critical to the least critical.
Backup Codes and Recovery
When you enable 2FA, most services generate a set of one-time backup codes. These are your emergency access method if you lose your phone, your authenticator app, or your hardware key. Without backup codes, losing your second factor can permanently lock you out of your own account.
How to Store Backup Codes
- In your password manager -- Store them as a note attached to the account entry. This is the most practical option for most people.
- Printed on paper -- Print them and store the paper in a safe, a locked drawer, or a safety deposit box. Not on a sticky note next to your computer.
- In an encrypted file -- If you use encrypted storage (like a LUKS volume or VeraCrypt container), you can store a text file with all your backup codes there.
- Never in plain text on your computer -- A text file on your desktop called "backup codes.txt" defeats the purpose of 2FA entirely.
Each backup code can only be used once. After you use one, cross it off your list or delete it from your records. When you are running low on unused codes, generate a new set from the service's security settings. Some services only provide 8-10 codes, so using them carelessly can leave you with no recovery option.
Hardware Security Keys
Hardware security keys are the gold standard of 2FA. They are small USB or NFC devices that provide cryptographic proof of your identity. Unlike TOTP codes, they are completely immune to phishing because the key checks the website's identity before responding.
How They Work
When you register a security key with a service, the key creates a unique cryptographic key pair for that specific website. During login, the website sends a challenge, the key signs it with the private key, and the website verifies the signature. The key also checks that the requesting website matches the one it was registered with -- so a phishing site at "g00gle.com" cannot trigger a response meant for "google.com."
Recommended Hardware Keys
- YubiKey 5 Series -- Supports USB-A, USB-C, NFC, and Lightning. Widely compatible with FIDO2, U2F, TOTP, and more. The most popular and well-supported option.
- SoloKeys -- Open-source hardware and firmware. Supports FIDO2 and U2F. Good for users who prefer open-source solutions.
- Google Titan -- USB-A, USB-C, and Bluetooth models. Built by Google, well-integrated with Google accounts.
Best Practices for Hardware Keys
- Register two keys per account -- Keep one on your keychain for daily use and one in a safe as backup. If you lose one, you can still log in with the other.
- Always set up a backup method -- Even with hardware keys, configure backup codes or an authenticator app as a fallback.
- Test both keys after registration to make sure each one works independently.
Common 2FA Mistakes
Enabling 2FA is an important step, but the way you manage it matters. These common mistakes can undermine your protection or lock you out of your own accounts.
- Not saving backup codes -- The most common mistake. People enable 2FA, skip the backup codes, and then lose their phone. Result: permanent account lockout.
- Using only SMS 2FA when better options exist -- If a service offers TOTP or hardware keys, use those instead. SMS should be your last resort, not your first choice.
- Storing TOTP secrets on the same device as your password manager -- If both your passwords and TOTP codes are on a phone that gets stolen, the attacker has both factors. Consider keeping them on separate devices or using a hardware key.
- Using the same phone number for 2FA on every account -- A single SIM swap attack compromises all of them. Diversify your 2FA methods where possible.
- Approving MFA prompts without thinking -- If you receive a push notification you did not expect, it means someone has your password. Deny the request and change your password immediately.
- Not updating 2FA when switching phones -- Before wiping or selling your old phone, transfer all authenticator accounts to the new device. Some apps support encrypted export/import; others require you to re-scan QR codes.
Before switching phones: (1) Export your authenticator app data if it supports encrypted backup. (2) Verify you have backup codes for every account. (3) Set up the authenticator on the new phone and verify each account works. (4) Only then wipe the old phone. Skipping this process is one of the most common ways people lock themselves out of accounts.
Defeat Your Own Two-Factor Login, Then Fix It, in Five Steps
Turning on two-factor authentication is the single most valuable security change most people ever make, and this section is not going to argue otherwise. But “2FA” names five quite different things with very different strengths, and the difference between them is not convenience — it decides whether a well-built phishing site can walk straight past it. In the next twenty minutes you will build the attack that defeats codes, find the exact property that makes them defeatable, watch the fix refuse to cooperate, and see why a tired person at 3 a.m. is a control weakness with a name. Every line of output below came from running these files.
Go: open a terminal in a folder you can write to — cd ~/Desktop on macOS or Linux, cd %USERPROFILE%\Desktop on Windows.
Do: save this as relay.py and run python3 relay.py.
"""A phishing site that does not steal your code -- it uses it, live."""
import time
REAL_SITE = {"user": "sarah", "password": "correct-horse-battery"}
VALID_CODE = "418907" # what her authenticator shows right now
def real_site_login(user, password, code):
if user != REAL_SITE["user"] or password != REAL_SITE["password"]:
return None
if code != VALID_CODE:
return None
return "session-token-for-" + user
def phishing_proxy(user, password, code):
"""Sits in the middle. Passes everything straight through."""
print(" [proxy] received password and code from the victim")
token = real_site_login(user, password, code)
print(" [proxy] forwarded them to the real site ->",
"accepted" if token else "rejected")
return token
print("Sarah types her details into a site that looks right.")
stolen = phishing_proxy("sarah", "correct-horse-battery", VALID_CODE)
print()
print("attacker now holds:", stolen)
print("time the code was valid for:", "30 seconds -- and he used it in under one")
print()
print("The code was not guessed, replayed later, or brute-forced. It was")
print("used immediately, for the purpose it was issued for, by the wrong person.")
You should see: the real site accepting, and the attacker holding the session:
Sarah types her details into a site that looks right.
[proxy] received password and code from the victim
[proxy] forwarded them to the real site -> accepted
attacker now holds: session-token-for-sarah
time the code was valid for: 30 seconds -- and he used it in under one
The code was not guessed, replayed later, or brute-forced. It was
used immediately, for the purpose it was issued for, by the wrong person.
Nothing was broken here. The password was correct, the code was correct, the code was used inside its window, and the real site behaved exactly as designed. The attacker simply stood in the middle and passed both along, which is why this is called an adversary-in-the-middle attack and why it is sold as a ready-made service.
Notice what the victim experiences: she types her details, the site says “logging you in”, and she ends up at the real site because the proxy redirects her there. There is no error, no second prompt, nothing to notice afterwards.
If not: if the proxy reports rejected, the code string in the call does not
match VALID_CODE; both are "418907". The attacker's token
printing as None means the same thing.
Go: the same folder.
Do: save this as blindness.py and run python3 blindness.py.
"""What each factor knows about where it is being used."""
FACTORS = [
("password", "knows nothing about the site it is typed into"),
("SMS code", "knows nothing -- it is six digits in a text message"),
("authenticator code", "knows nothing -- computed from a clock and a secret"),
("push 'approve?' prompt","knows the service, but not which site you are looking at"),
("hardware key / passkey","knows the exact site, and refuses to sign for another"),
]
print("%-26s %s" % ("FACTOR", "WHAT IT KNOWS ABOUT WHERE IT IS USED"))
print("-" * 84)
for name, knows in FACTORS:
print("%-26s %s" % (name, knows))
blind = sum(1 for _, k in FACTORS if k.startswith("knows nothing"))
print()
print("factors that cannot tell a real site from a copy:", blind, "of", len(FACTORS))
print()
print("This is the whole difference. A code proves you HAVE something.")
print("It cannot prove WHERE you are, so it cannot notice being relayed.")
You should see: three of five factors unable to tell a real site from a copy:
FACTOR WHAT IT KNOWS ABOUT WHERE IT IS USED
------------------------------------------------------------------------------------
password knows nothing about the site it is typed into
SMS code knows nothing -- it is six digits in a text message
authenticator code knows nothing -- computed from a clock and a secret
push 'approve?' prompt knows the service, but not which site you are looking at
hardware key / passkey knows the exact site, and refuses to sign for another
factors that cannot tell a real site from a copy: 3 of 5
This is the whole difference. A code proves you HAVE something.
It cannot prove WHERE you are, so it cannot notice being relayed.
This is the sentence to carry away: a code proves possession, and possession is not location. Six digits from an authenticator app are computed from a shared secret and the current time. They contain no information about which website you typed them into, so they cannot object to being typed into the wrong one.
That is not a flaw in the algorithm; it is a consequence of the code being something a human reads and retypes. Anything a human can move between screens, an attacker can move too.
If not: this prints a fixed table and cannot really fail. If the count is not
3, one of the three knows nothing strings was reworded — the
counter matches on that prefix.
Go: the same folder.
Do: save this as origin.py and run python3 origin.py. This is the
essential mechanism of a security key or passkey, reduced to its core.
"""The fix: sign the site's name, so the signature is worthless elsewhere."""
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.exceptions import InvalidSignature
key = Ed25519PrivateKey.from_private_bytes(bytes(32)) # fixed seed, so output matches
public = key.public_key()
def sign_login(challenge, origin):
"""The security key signs the challenge AND the site it is talking to."""
return key.sign(challenge + b"|" + origin.encode())
def verify(signature, challenge, origin_the_site_expects):
try:
public.verify(signature, challenge + b"|" + origin_the_site_expects.encode())
return "accepted"
except InvalidSignature:
return "REFUSED"
challenge = b"random-challenge-from-the-server"
print("Sarah is on the real site:")
sig = sign_login(challenge, "https://bank.example")
print(" ", verify(sig, challenge, "https://bank.example"))
print()
print("Sarah is on a copy, and the proxy forwards her signature:")
sig_from_fake = sign_login(challenge, "https://bank-secure-login.example")
print(" ", verify(sig_from_fake, challenge, "https://bank.example"))
print()
print("The browser tells the key which site is asking. The key signs that name.")
print("A signature made for the wrong name does not verify -- so relaying it")
print("achieves nothing, and no human had to notice anything.")
You should see: the same key accepted on the real site and refused on the copy:
Sarah is on the real site:
accepted
Sarah is on a copy, and the proxy forwards her signature:
REFUSED
The browser tells the key which site is asking. The key signs that name.
A signature made for the wrong name does not verify -- so relaying it
achieves nothing, and no human had to notice anything.
The important detail is who supplies the site name. It is not typed by the user and not sent by the page — the browser tells the security key which origin it is actually connected to, and the key signs that. A phishing site is a different origin, so the signature it obtains is a signature for the wrong name, and the real site rejects it.
This is why phishing resistance is a property of the method rather than of the person. The user in this example was completely fooled: she believed the fake site, typed everything, and approved the login. It failed anyway. No other factor on the list has that property.
If not: ModuleNotFoundError: No module named 'cryptography' means the library
is missing — install it with python3 -m pip install cryptography. If both cases
print accepted, the second call is verifying against the fake origin rather than the
one the real site expects; the site always checks its own name.
Go: the same folder.
Do: save this as fatigue.py and run python3 fatigue.py.
"""Push prompts have one weakness, and it is not cryptographic."""
def user_response(prompt_number, hour):
"""A person, at 3am, on the twenty-somethingth identical prompt."""
if hour == 3 and prompt_number >= 22:
return "approve" # to make it stop
return "deny"
approved_at = None
for n in range(1, 41):
if user_response(n, hour=3) == "approve":
approved_at = n
break
print("prompts sent by the attacker :", 40)
print("prompts denied :", approved_at - 1)
print("prompt that was approved :", approved_at)
print()
print("cost to the attacker of sending 40 prompts:", "a loop")
print("number of them that had to succeed :", 1)
print()
print("The prompt asks 'was this you?'. At 3am, after twenty denials, the")
print("question a tired person is actually answering is 'how do I stop this'.")
You should see: one approval out of forty attempts, which is all that was needed:
prompts sent by the attacker : 40
prompts denied : 21
prompt that was approved : 22
cost to the attacker of sending 40 prompts: a loop
number of them that had to succeed : 1
The prompt asks 'was this you?'. At 3am, after twenty denials, the
question a tired person is actually answering is 'how do I stop this'.
Push prompts are a genuine improvement on typed codes — there is nothing to relay, because nothing is retyped. Their weakness is elsewhere: the attacker, who already has the password, can trigger the prompt as often as he likes, and the defender has to be right every time.
If this ever happens to you, the correct reading is unambiguous: repeated unexpected approval prompts mean somebody already knows your password. Deny them, then change that password immediately from a device you trust — the prompts will stop, because the attacker can no longer get as far as triggering them.
If not: the numbers are produced by a fixed rule, so they should match exactly; if
approved_at prints None, the loop range was reduced below 22.
Go: the same folder.
Do: save this as numbermatch.py and run python3 numbermatch.py.
"""Number matching: make the prompt impossible to approve by reflex."""
def plain_prompt(user_taps_approve):
return "granted" if user_taps_approve else "denied"
def matched_prompt(number_on_screen, number_user_types):
if number_on_screen is None:
return "denied -- the user never saw the real login screen"
return "granted" if number_on_screen == number_user_types else "denied"
print("PLAIN PROMPT")
print(" tired user taps approve ->", plain_prompt(True))
print()
print("NUMBER MATCHING")
print(" attacker triggers the login; the number is on HIS screen, not hers")
print(" she is asked to type the number she can see")
print(" she can see :", None)
print(" result ->", matched_prompt(None, 47))
print()
print(" the real Sarah, logging in herself, sees 47 on her own screen")
print(" result ->", matched_prompt(47, 47))
print()
print("There is now no button to press by mistake. Approving requires")
print("information that only exists on the screen of whoever started the login.")
You should see: the tired approval failing and the genuine one succeeding:
PLAIN PROMPT
tired user taps approve -> granted
NUMBER MATCHING
attacker triggers the login; the number is on HIS screen, not hers
she is asked to type the number she can see
she can see : None
result -> denied -- the user never saw the real login screen
the real Sarah, logging in herself, sees 47 on her own screen
result -> granted
There is now no button to press by mistake. Approving requires
information that only exists on the screen of whoever started the login.
Number matching works because it changes the question from one a person can answer by reflex to one that requires information. The number appears on the screen where the login was started. If you did not start it, you cannot see the number, so there is no way to approve it accidentally, however tired or irritated you are.
So the practical ranking, best to worst: a passkey or hardware security key, which cannot be phished at all; then a push prompt with number matching; then an authenticator app; then SMS. Every one of them is enormously better than no second factor. If you take one action from this page, it is to move your email account — the one that can reset all the others — to a passkey or a hardware key.
If not: if the number-matching case prints granted for the attacker, the
None was replaced with a number; the whole point is that the victim's screen shows
nothing because the login was started somewhere else.
Without scrolling up: a service offers you SMS codes, an authenticator app, or a security key, and a friend says they are all the same because they all send a second thing. Where does that reasoning break down, and which would you choose for your email account and why? Answer: it breaks down because it counts factors rather than asking what each one proves. All three prove possession, but only one of them knows where it is being used. Steps 1 and 2 showed that a code contains no information about the site it is typed into, so a proxy can accept it and pass it straight to the real service within its validity window — and step 3 showed that a key which signs the origin refuses to produce a usable signature for a lookalike site, even when the user is entirely convinced. SMS is weaker again, for a reason outside this comparison: the number can be moved to another SIM. For email specifically, choose the security key or passkey, because email is the account through which every other account can be reset — the blast radius of losing it is every service you own.
Now do it without the page: add a fourth factor to blindness.py — a one-time code emailed to
you — and decide which column it belongs in and why. Then do the change that matters: open
the security settings of your main email account and see which second factors it offers. If a
passkey or security key is on the list and you are not using it, adding one takes about two
minutes and is the single largest improvement available to you today.
Summary
Two-factor authentication is your strongest defense against account takeover. It transforms a stolen password from a total compromise into a minor inconvenience -- the attacker has one piece of the puzzle but cannot get in.
- 2FA adds a second verification step that prevents access even if your password is stolen
- TOTP authenticator apps are the recommended method for most people -- they are secure, free, and work offline
- Hardware security keys are the strongest option and are immune to phishing attacks
- SMS codes are better than nothing but should be replaced with TOTP or hardware keys when possible
- Always save backup codes in your password manager or a secure physical location
- Enable 2FA on your email first, then financial accounts, then everything else
- Never approve unexpected 2FA prompts -- they indicate your password is already compromised
Start with your email and work outward. Each account you secure with 2FA dramatically reduces your risk. It takes only a few minutes per account, and the protection it provides is worth far more than the minor inconvenience of entering a code at login.