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.



