Add a Second Factor to a Login Script

CybersecurityBeginner
Practice Now

Introduction

Multi-factor authentication reduces account compromise risk by requiring more than one kind of proof. A password is something the user knows. A second factor can be a code from a separate device, app, file, or hardware token.

In this lab, you will start with a password-only Python login script and then add a simple second factor. To keep the lab beginner-friendly and deterministic, the second factor is an HMAC-based code generated from a protected secret file.

Test Password-Only Login

In this step, you will run the starting login script. It only checks a username and password, which is single-factor authentication because the only proof is something the user knows.

Move into the project directory:

cd /home/labex/project

Inspect the prepared files:

ls -l

View the user database:

cat users.json

Run a successful password-only login:

python3 login.py alice wonderland

The output should be:

login ok

Now try a wrong password:

python3 login.py alice wrong

This command should print login failed. A failed login exits with a nonzero status, which is normal for this test.

Run the checker for the starting state:

python3 mfa-check.py password-only

The expected result is:

password-only: complete

Create the Second-Factor Secret

In this step, you will create a separate secret for the second factor. In a real MFA system, this secret might live in an authenticator app or hardware token. In this lab, it is a local file so the behavior is easy to inspect and verify.

Create a secret value:

printf 'labex-mfa-secret-2026\n' > mfa_secret.txt

Restrict the file so only the owner can read and write it:

chmod 600 mfa_secret.txt

Check the permissions:

ls -l mfa_secret.txt

The permission bits should start with -rw-------:

-rw------- ... mfa_secret.txt

Run the checker for the secret:

python3 mfa-check.py secret

The expected result is:

secret: complete

Generate a Login Code

In this step, you will create a small code generator. It uses HMAC-SHA256 to combine the secret file with the username. HMAC is useful here because the same input and secret produce the same short code, but someone without the secret cannot easily create the code.

Create generate-code.py:

cat > generate-code.py <<'PY'
#!/usr/bin/env python3
import hmac
import hashlib
import sys
from pathlib import Path

if len(sys.argv) != 2:
    print("usage: generate-code.py USERNAME")
    raise SystemExit(2)

username = sys.argv[1]
secret = Path("mfa_secret.txt").read_text().strip().encode()
digest = hmac.new(secret, username.encode(), hashlib.sha256).hexdigest()
print(digest[:6])
PY

Make it executable:

chmod +x generate-code.py

Generate a code for alice:

python3 generate-code.py alice

You should see this deterministic six-character code:

21f18f

Run the checker for the generator:

python3 mfa-check.py generator

The expected result is:

generator: complete

Enforce the Second Factor

In this step, you will replace the login script so it requires both the password and the generated code. This is the key MFA change: the password alone is no longer enough.

Replace login.py with this MFA-aware version:

cat > login.py <<'PY'
#!/usr/bin/env python3
import hmac
import hashlib
import json
import sys
from pathlib import Path

USERS = Path("users.json")
SECRET = Path("mfa_secret.txt")

def expected_code(username):
    secret = SECRET.read_text().strip().encode()
    digest = hmac.new(secret, username.encode(), hashlib.sha256).hexdigest()
    return digest[:6]

def main():
    if len(sys.argv) != 4:
        print("usage: login.py USERNAME PASSWORD MFA_CODE")
        return 2

    username, password, code = sys.argv[1], sys.argv[2], sys.argv[3]
    users = json.loads(USERS.read_text())
    if users.get(username, {}).get("password") != password:
        print("login failed")
        return 1

    if not hmac.compare_digest(code, expected_code(username)):
        print("mfa failed")
        return 1

    print("login ok")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())
PY

Make sure the script is executable:

chmod +x login.py

Generate a fresh code and save it in a shell variable:

CODE="$(python3 generate-code.py alice)"

Use the correct password and code together:

python3 login.py alice wonderland "$CODE"

The output should be:

login ok

Run the checker for MFA enforcement:

python3 mfa-check.py enforced

The expected result is:

enforced: complete

Test MFA Failure and Success

In this step, you will test the final login behavior. A correct password without the second factor should fail, a wrong code should fail, and the correct password plus correct code should succeed.

First, try the old password-only command:

python3 login.py alice wonderland || true

The output should show the new usage message because the MFA code is missing:

usage: login.py USERNAME PASSWORD MFA_CODE

Try a wrong code:

python3 login.py alice wonderland 000000 || true

The output should be:

mfa failed

Generate the correct code and log in:

CODE="$(python3 generate-code.py alice)"
python3 login.py alice wonderland "$CODE"

The output should be:

login ok

Run the full checker:

python3 mfa-check.py all

The final output should show every final MFA component complete:

secret: complete
generator: complete
enforced: complete

Summary

You changed a password-only login script into a simple multi-factor login flow. You created a separate MFA secret, generated an HMAC-based code, required that code in the login script, and tested both failure and success cases.

The main security idea is that a stolen password should not be enough by itself when an independent second proof is required.