Should I test that?

Should I test two-factor authentication?

Verdict

Yes

Test two-factor authentication before it ships: every route that creates a session must keep a user with 2FA on signed out until a valid code arrives, a used code must fail, and failed codes must hit an attempt limit; do not test the TOTP algorithm inside the library.

Why

Two-factor authentication is Test mandatory, with automated tests in CI. The typical case is a web application that adds authenticator-app codes (TOTP) and recovery codes to its own password sign-in. Blast radius is safety-or-legal, because a bypass hands a stranger an account and its personal data, and Reversibility is impossible, because data an attacker has read stays read. Detectability is never: a sign-in that skipped the code looks like any other sign-in. Change frequency is rarely, a few changes a year, and Test cost is moderate, because a test generates the current code from a known secret, so rule R2 gives Test mandatory.

When the decision changes
WhenDecisionWhy
The team adds a sign-in path every month, such as passkeys, Google sign-in or a mobile APITest mandatory: one test reads the route table and fails for any route that creates a session without the second-factor checkChange frequency rises to regularly, and Blast radius stays safety-or-legal, so rule R2 still applies
An identity provider such as Keycloak, Okta or Entra ID asks for the code, and your app only reads its tokenTest mandatory: one test that your app refuses a token whose `amr` or `acr` claim shows no second factorTest cost falls to trivial, because the test signs a token with a test key, and Blast radius stays safety-or-legal
2FA guards a staff tool with no customer data, such as a shift rotaTest minimally: one test that a password without a code gets no sessionBlast radius falls to internal, Detectability to eventually and Reversibility to with-effort, because staff notice changed shifts and a backup restores them
2FA runs in a prototype on your own machine, with seeded accounts and no real usersDo not test 2FA yet; write the session tests before the first real user signs upBlast radius falls to none, because nobody but you can sign in
SMS codes stop arriving in one country after a carrier change that no provider sandbox showsTest SMS delivery differently: alert when the share of sent codes that users enter drops in any countryBlast radius falls to users and Reversibility to with-effort, because people are locked out, not exposed; Detectability moves to eventually, because users in one country give up quietly, and with Test cost heavy, rule R9 applies

What breaks if you don't test

Expect the failure in the routes around the code check. A password reset that ends by signing the user in, a Google sign-in callback, and a mobile API that trades a password for a token each create a session without the second step, and the OWASP MFA cheat sheet names the API and the mobile app as the paths teams miss. Nobody notices until an attacker uses the route. The second failure is a code that works twice: RFC 6238 forbids it, but pyotp's verify() keeps no record of used codes and leaves replay rejection to your database.

What you lose if you over-test

Checking TOTP output against the RFC test vectors repeats tests that pyotp and otplib already run. A browser test that reads a real SMS on a real phone takes days to build and fails whenever the carrier is slow. A time-window test that waits on the real clock spends a minute per case; generate old codes with at() instead.

How to test

Write integration tests through HTTP with a test user whose TOTP secret the test knows, and run them in CI. The minimum set:

  1. Every route that can create a session leaves a user with 2FA on signed out until a valid code arrives.
  2. A correct code signs in once and fails the second time.
  3. A code from 90 seconds ago fails; RFC 6238 recommends at most one 30-second step of delay.
  4. After the attempt limit, even a correct code fails. NIST SP 800-63B allows at most 100 consecutive failures per authenticator.
  5. Each recovery code works once, and turning 2FA off asks for a fresh code.

When the answer changes

  • An identity provider takes over the code check, and your app only reads its token.
  • The accounts behind 2FA hold no customer data.
  • SMS codes fail to arrive in some countries.

Real incident + Code example

The reset link that skipped the code

On a B2B SaaS product I worked on, the password reset flow ended by signing the user in. The 2FA step lived only in the login view, and every test went through that view, so no test showed that anyone with access to a user's mailbox got in without a code. An outside researcher reported it four months after release. We moved the check into the one function that creates a session and added this test, with a fixture that walks each sign-in path:

import pyotp
import pytest

SIGN_IN_PATHS = ["password", "password_reset", "magic_link", "google"]

@pytest.mark.parametrize("path", SIGN_IN_PATHS)
def test_no_session_before_second_factor(client, user_with_2fa, sign_in, path):
    sign_in(client, user_with_2fa, via=path)
    assert "_auth_user_id" not in client.session  # Django's signed-in key

def test_code_is_accepted_once(client, user_with_2fa, sign_in):
    code = pyotp.TOTP(user_with_2fa.totp_secret).now()
    sign_in(client, user_with_2fa, via="password")
    client.post("/2fa/", {"code": code})
    assert "_auth_user_id" in client.session
    client.logout()
    sign_in(client, user_with_2fa, via="password")
    client.post("/2fa/", {"code": code})
    assert "_auth_user_id" not in client.session

FAQ

How do I test two-factor authentication in automated tests?

Give the test user a known TOTP secret, generate the current code with the same library the server uses, and submit the code through HTTP. Assert that the user has no session before the code and has one after it.

How do end-to-end tests sign in when 2FA is on?

End-to-end tests sign in with 2FA on by generating the code from the seeded user's secret, for example with otplib in Playwright. Never add a setting that skips the second step in test environments, because such a setting can reach production.

Should I test SMS verification codes?

Yes, test your own SMS code logic (expiry, single use, the attempt limit) with a fake SMS sender that records each code. Do not send real messages to real phones in tests.