Embercode
✕
Safety and PrivacyLesson 2 of 6

40 min read

Two-factor login and passkeys

Why this matters

A password can be stolen. It can be guessed, reused from a breached site, or typed into a fake login page by mistake. The lesson "Passwords and password managers" in this module covers how to make a password hard to guess and how to keep it unique. Even a good password can still be stolen, so many sites ask for a second proof when you log in. This is called two-factor login, or 2FA. The first factor is the password. The second factor is something else that only you can provide.

Two-factor login is one of the most useful protections you can turn on. A stolen password alone is no longer enough to open the account. The attacker also needs your phone, your security key, or your passkey.

This lesson covers three kinds of second proof: authenticator app codes, security keys and passkeys. For each one, you will see what it stops, what it does not stop, and what to do when you lose the device. The lesson does not cover how phishing pages look. That is in "Recognising phishing and scams".

The idea: three kinds of proof

Security people describe proofs in three groups:

  • Something you know, such as a password or a PIN.
  • Something you have, such as a phone, a code generator or a small hardware key.
  • Something you are, such as a fingerprint or a face scan.

Two-factor login means using two different groups. A password plus a code from your phone uses "know" and "have". A password plus a fingerprint on a passkey uses "know" and "are", or "have" and "are" when the fingerprint unlocks a device you own.

Using two proofs from the same group does not help much. Two passwords are both "know", so a thief who knows one of them still has the same kind of proof. Two codes from the same app are both "have", so a thief who has your phone still has them both.

The key word is independent. A thief has to lose to the second proof separately from the first one. A good second factor is hard to copy and hard to trick a person into giving away.

Authenticator apps and one-time codes

An authenticator app is a program on your phone that shows a six-digit code. The code changes every 30 seconds. When you log in, you type the password and then the current code. The site checks the code and lets you in only if it is correct.

The method behind these codes is called TOTP, which stands for time-based one-time password. It is defined in a public standard called RFC 6238. "RFC" is short for Request for Comments, the name for a standard document published by the internet engineering community. The standard is open, so anyone can read it and check the method.

How a code is made

When you turn on the feature, the site makes a random secret. The secret is a short string of letters and digits. The site shows it to you as a QR code, which is a square of black and white dots. Your app scans the QR code and saves the secret. The site also saves the same secret on its side.

Both sides then do the same calculation. The calculation uses three things:

  1. The secret. This is shared once, at setup.
  2. The current time, divided into 30-second blocks. Every block has a number called a counter. The counter is the number of whole 30-second blocks since 1970 in UTC, which is the standard world time with no daylight saving changes.
  3. A hash function called HMAC-SHA1. A hash function is a calculation that turns an input into a fixed-length output, and it is hard to run backwards. HMAC is a way of using a hash with a secret key.

The result is a long number. The method takes the last part of it and reduces it to six digits. Both sides get the same six digits if they have the same secret and the same clock.

Here is a short Python program that does the calculation. It follows RFC 6238 and uses the test secret that the standard publishes.

import base64
import hashlib
import hmac
import struct
import time
 
def totp(secret_b32, unix_time, digits=6, step=30):
    key = base64.b32decode(secret_b32)
    counter = int(unix_time // step)
    msg = struct.pack(">Q", counter)
    digest = hmac.new(key, msg, hashlib.sha1).digest()
    offset = digest[-1] & 0x0F
    code = struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF
    return str(code % (10 ** digits)).zfill(digits)
 
# RFC 6238 test secret "12345678901234567890" in base32
secret = base64.b32encode(b"12345678901234567890").decode()
print("secret (base32):", secret)
for t in (59, 1111111109):
    print("time", t, "8 digits:", totp(secret, t, digits=8), "6 digits:", totp(secret, t, digits=6))
 
# Same secret, times 30 seconds apart in a short window
for t in (1000, 1015, 1029, 1030, 1045):
    print("time", t, "counter", t // 30, "code", totp(secret, t))

Save it as totp.py and run it with python3 totp.py. The output from one run was:

secret (base32): GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
time 59 8 digits: 94287082 6 digits: 287082
time 1111111109 8 digits: 07081804 6 digits: 081804
time 1000 counter 33 code 841346
time 1015 counter 33 code 841346
time 1029 counter 34 code 749439
time 1030 counter 34 code 749439
time 1045 counter 34 code 749439

The lines for time 59 and time 1111111109 match the test vectors in RFC 6238. At time 59 the eight-digit code is 94287082, and at time 1111111109 it is 07081804. If your output differs on those lines, check the code before you trust it.

The other lines show the 30-second blocks. Times 1000 and 1015 are both in block 33, so they give the same code. Times 1029, 1030 and 1045 are in block 34, so they give the same code, which is different from block 33.

What happens when you log in

Here is a worked example with a real timeline, using the same numbers as above.

At time 1000, your app shows 841346. You type it at time 1015, because you were slow. The site computes the code for the current block, which is still block 33, so it gets 841346 and accepts it.

Now suppose you type it at time 1029. The site is still in block 33, so it accepts the code. A second later, at time 1030, the block changes to 34. The code for block 34 is 749439. The old code 841346 is no longer the current one, so a fresh login will need 749439.

Many sites accept the code from the block just before or just after the current one. This allows for a phone clock that is a few seconds off, or a person who is slow to type. The window is small, but it exists, and it matters for the attacks described below.

What an authenticator code protects against

An authenticator code stops an attacker who has only your password. They cannot make the code, because the secret never leaves your phone and the site. A stolen password from a breached website is useless on its own, because the attacker does not have the code.

It also stops many automatic attacks. Credential stuffing, the attack described in the password lesson, depends on the same pair working on many sites. The second factor makes that pair fail on the sites that use it.

What an authenticator code does not protect against

A code can be stolen by a phishing page. A fake page asks for your password and then asks for the current code. You type both. The fake page sends them to the real site straight away, because the code is valid for about 30 seconds. The attacker logs in as you. This is called a real-time phishing relay. It does not need to break the code, because you give it to them.

A code also does not help if the attacker controls your phone or can read your messages. If the code is sent by text message, a different risk applies, described in the next section.

Text message codes

Some sites send the code by SMS, which is a text message. This is better than having no second factor. It is weaker than an authenticator app.

Text messages can be read by someone who controls the phone number. A SIM swap is an attack where someone convinces a phone company to move your number to a SIM card they hold. A SIM card is the small chip that identifies your phone to the mobile network. Once the number is theirs, they receive your codes. Text messages can also be intercepted in some other ways. For these reasons, use an authenticator app or a passkey instead of text codes when the site offers the choice.

Security keys

A security key is a small hardware device, usually the size of a house key. It plugs into a USB port or touches a phone with NFC, which is a short-range wireless connection. You press a button on it to confirm that you are present.

A security key does not show a code. Instead, it signs a message. The site sends a challenge, which is a random message. The key signs the challenge with a private key that stays inside the device and cannot be read out. The site checks the signature with the public key it saved when you registered the key. A public key can be shared freely. It can check a signature but cannot make one. The private key is the part that must stay secret.

The standard that defines this is called FIDO2, with a part called WebAuthn that the browser uses. You do not need to know the details to use a key. You need to know two facts.

First, the key checks the address of the page. When you log in, the browser tells the key the address that is asking, which is the origin of the page. The key signs the challenge together with that address. A fake page at a different address gets a signature that the real site will reject. The key does not sign for a fake page, even if you press the button.

Second, the key does not reveal anything you can type. The fake page cannot ask you for the code, because there is no code. This is why security keys are described as phishing-resistant.

Worked example: a fake bank page and a security key

Suppose you get an email that says your bank needs you to log in again. The link goes to bank-secure-login.example.net, not your bank's real address. The page looks the same. You enter your password. The page then asks you to touch your security key.

You touch the key. The browser sends the challenge and the fake page's address to the key. The key signs the challenge with the fake page's address in the signed data. The fake page passes this signature to the real bank. The real bank checks the signature against the saved public key, and the address in the signature is wrong. The login fails. The attacker has your password, but nothing they can do with it passes the bank's check.

With an authenticator code, the same fake page would have collected the code and passed it on within 30 seconds. The security key is stronger here because the key ties the proof to the address.

Security key downsides

A security key costs money, and you need to buy one that works with the sites you use. It can be lost or damaged. Most sites that accept keys also ask you to register a backup, which is a second key or another method. Keep the backup somewhere you can reach. If you lose your only key and have no backup, you must use the site's account recovery process, which varies by site and can be slow.

Passkeys

A passkey is a login key stored on your device or in your password manager, and it works the way a security key does. It uses the same FIDO2 standard. The difference is where the key lives and how you unlock it.

With a passkey, you do not type a password at all on sites that support it. You confirm with your fingerprint, your face or your device PIN. The device then signs the challenge. The site stores only the public key.

A passkey is bound to the site's address, just like the security key. The browser checks the address before it offers a passkey for a site. On a fake page with a different address, the browser does not offer the passkey for the real site. This is why passkeys stop phishing in the same way that security keys do.

Synced and device-bound passkeys

Passkeys come in two forms:

  • A synced passkey is copied between your devices through a cloud account. The copy is encrypted. Your phone and your laptop can both use it. If you lose one device, the other still works.
  • A device-bound passkey stays on one device, usually a security key or a particular phone. It cannot be copied. It is harder to lose by accident, but if you lose the device, you lose the key.

Both forms are phishing-resistant, because both are tied to the site's address. The choice between them is about convenience and recovery, not about how well they stop attacks.

Worked example: logging in on a new laptop

Suppose you have a synced passkey for your code hosting account. You open the login page on a new laptop. The page offers to sign you in with a passkey. Your laptop asks for your fingerprint or device PIN. After you confirm, the laptop uses the synced passkey, which it received through your cloud account, to sign the challenge. You are logged in without a password.

Now suppose the new laptop is not signed in to your cloud account. The browser cannot find the synced passkey on this device. It will offer a different way to log in, such as a QR code that you scan with your phone, which holds the passkey. The phone signs the challenge and the laptop completes the login. This flow is designed so that the passkey never has to be copied into a new device by hand.

Comparing the three

The table shows what each method stops. The last column is the main weakness of each one.

MethodStops a stolen passwordStops a fake login pageMain weakness
Authenticator app codeYesNo, a code can be relayed in real timePhone lost or copied, or a fake page gets the code in time
Security keyYesYesLost key, and a backup must be set up
PasskeyYesYesLost device with no other copy, or an account that does not offer passkeys

Text message codes sit below the authenticator app. They stop a stolen password but can be defeated by a SIM swap, and they do not stop a fake page that collects the code.

The right choice depends on the site. Use a passkey or security key wherever the site offers one. Use an authenticator app where no passkey option exists. Use text codes only as a last resort, and check whether the site also offers a stronger method.

Setting up two-factor login safely

Set up the second factor in this order, because it protects you from getting locked out:

  1. Turn on two-factor login for your email first. Email is the account that resets other passwords, so it matters most.
  2. Save the recovery codes that the site shows when you turn the feature on. Recovery codes are single-use codes for when you cannot use your second factor. Print them or write them down, and store them somewhere safe that you control. Do not store them in the same unlocked file as your passwords, unless your manager keeps them encrypted.
  3. Add a second method if the site allows it. A security key and an authenticator app together means losing one does not lock you out.
  4. Test the login once with the new method before you close the page. Log out and log in again.
  5. Repeat for your bank, your code hosting account and the other important accounts.

If you use a password manager, check that it can store your authenticator codes and passkeys. Some managers do this, and it is convenient. The downside is that a stolen unlocked manager then gives an attacker both the password and the code. For the most important accounts, keep the second factor on a separate device.

Common mistakes

Setting up the second factor and then never testing it. A login that fails on the day you need it is a common surprise. Log out and log in once after you set it up.

Keeping recovery codes only on the phone that makes the codes. If that phone is lost, the codes are lost with it. Keep a printed or written copy somewhere else.

Treating a code as safe to type anywhere. A code typed into a fake page is handed to the attacker. Only type a code after you have checked the address of the page.

Thinking a passkey is a password you can share. A passkey is tied to your device and to the site. It cannot be read out and typed into another machine by hand. If a site asks you to type a passkey, something is wrong.

Real error messages from a secret that is copied wrongly

Developers who build code-based login use a library or a few lines of code that decode the secret. If the secret is copied wrongly, the decoder stops with a message. Here are two messages from Python, raised by the program you have already seen when it gets a bad secret:

binascii.Error: Non-base32 digit found

This happens when the secret has a character outside the base32 alphabet. Base32 uses the letters A to Z and the digits 2 to 7. A secret that contains a 1 or a 0 breaks this rule. A common cause is a copy that mixes up a 1 with the letter l.

binascii.Error: Incorrect padding

This happens when the secret is the wrong length. Base32 works in blocks of eight characters, and Python checks that the length fits. One missing character from a copied secret gives this message.

In both cases, do not guess the missing or wrong character. Copy the secret again from the site, or turn on the feature again and make a new secret.

Practice

  1. The output above shows that times 1000 and 1015 give the same code, 841346. Why does the code stay the same for those two times?

Show the answer

Both times fall in the same 30-second block. The counter is the time divided by 30, rounded down. For 1000 the counter is 33, and for 1015 it is also 33. The same counter and the same secret give the same code.

  1. Run the program in this lesson. What is the eight-digit code at time 59, and what does it show?

Show the answer

The code is 94287082. It matches the test vector published in RFC 6238, so it shows that the calculation is correct.

  1. Your phone's clock is four minutes behind the real time. What will happen when you use the authenticator app?

Show the answer

Four minutes is about eight 30-second blocks. The codes on your phone will be for times the site does not expect to be current, so the site will most likely reject them. Turn on automatic time setting on the phone so that the clock stays correct.

  1. A fake page asks for your password and then for the current authenticator code. Why does a security key stop this attack, and an authenticator code does not?

Show the answer

An authenticator code is a number you can type anywhere, so the fake page can collect it and pass it to the real site within 30 seconds. A security key signs a challenge together with the address of the page asking for it. The fake page has a different address, so the signature fails at the real site. The fake page never gets a usable proof.

  1. You copied a secret as GEZDGNBV1Y3TQOJQ, and the decoder says Non-base32 digit found. What is wrong?

Show the answer

The secret contains a 1. Base32 uses the letters A to Z and the digits 2 to 7, so the 1 is not allowed. The copy probably mixed up a 1 with the letter l, or a 0 with the letter O. Copy the secret again from the site, or make a new one.

Summary

  • Two-factor login adds a second proof, from a different group than the password, so a stolen password alone is not enough.
  • An authenticator app shows a six-digit code that changes every 30 seconds, made from a shared secret and the current time.
  • Authenticator codes can be relayed by a fake page in real time, so they do not stop phishing.
  • A security key or passkey signs a challenge together with the page's address, so a fake page cannot use it.
  • Save recovery codes somewhere safe, add a second method where possible, and test the login before you rely on it.
Check yourself: Logins (8 questions) →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Passwords and password managers