Skip to main content

How to Handle 2FA (Two-Factor Authentication) with Python Requests

Automating logins on websites with Two-Factor Authentication requires special handling. While 2FA is excellent for security, it breaks standard username/password automation. Python offers two main approaches: manual code entry and full automation using TOTP.

Understanding the Challenge​

2FA typically works in two steps:

  1. Submit username and password
  2. Submit a time-based code from an authenticator app or SMS

The key is maintaining the session between these steps using requests.Session().

Manual Intervention (Interactive Script)​

For occasional scripts, pause and ask the user to enter the code:

import requests

session = requests.Session()
login_url = "https://example.com/api/login"
verify_url = "https://example.com/api/2fa"

# Step 1: Send credentials
payload = {"username": "myuser", "password": "mypassword"}
response = session.post(login_url, json=payload)

if response.status_code == 200:
print("Credentials accepted. Awaiting 2FA code...")

# Step 2: Get code from user
code = input("Enter the code sent to your phone: ")

# Step 3: Submit 2FA code
verify_payload = {"code": code}
verify_res = session.post(verify_url, json=verify_payload)

if verify_res.status_code == 200:
print("Login successful!")
# Session is now authenticated
protected = session.get("https://example.com/api/protected")
else:
print("Invalid code.")
else:
print("Login failed.")

Why Use Session?​

import requests

# ❌ Wrong: Separate requests lose cookies
requests.post(login_url, json=credentials)
requests.post(verify_url, json={"code": "123456"}) # Different session!

# ✅ Correct: Session maintains cookies between requests
session = requests.Session()
session.post(login_url, json=credentials)
session.post(verify_url, json={"code": "123456"}) # Same session!

Full Automation with TOTP​

For autonomous bots and scheduled tasks, generate the 2FA code programmatically using the secret key.

Installation​

pip install pyotp

Getting the Secret Key​

When setting up 2FA, most services show a QR code and a "Manual Entry Key" or "Secret Key", a string like JBSWY3DPEHPK3PXP. Save this securely.

Implementation​

import requests
import pyotp
import os

# Load secret from environment variable (never hardcode!)
SECRET_KEY = os.environ.get("TOTP_SECRET")

def get_2fa_code():
"""Generate current TOTP code."""
totp = pyotp.TOTP(SECRET_KEY)
return totp.now()

session = requests.Session()

# Step 1: Login with credentials
response = session.post("https://example.com/api/login", json={
"username": "bot_user",
"password": "bot_password"
})

if response.status_code == 200:
# Step 2: Generate and submit 2FA code
current_code = get_2fa_code()
print(f"Generated 2FA Code: {current_code}")

verify_response = session.post(
"https://example.com/api/2fa",
json={"code": current_code}
)

if verify_response.status_code == 200:
print("Bot successfully logged in!")
else:
print("2FA verification failed")
warning

Never hardcode the TOTP secret in your source code. Use environment variables, secrets managers, or encrypted configuration files.

Handling Timing Issues​

TOTP codes are time-based and typically valid for 30 seconds. Handle edge cases:

import pyotp
import time

def get_2fa_code_with_retry(secret_key, max_retries=2):
"""Generate code with retry for timing edge cases."""
totp = pyotp.TOTP(secret_key)

for attempt in range(max_retries):
code = totp.now()

# Check remaining validity time
remaining = 30 - (int(time.time()) % 30)

if remaining < 5:
# Code about to expire, wait for new one
print(f"Code expiring soon, waiting {remaining + 1} seconds...")
time.sleep(remaining + 1)
continue

return code

return totp.now()

Complete Example with Error Handling​

import requests
import pyotp
import os
from typing import Optional

class TwoFactorAuth:
def __init__(self, base_url: str, secret_key: str):
self.base_url = base_url
self.secret_key = secret_key
self.session = requests.Session()

def login(self, username: str, password: str) -> bool:
"""Complete login flow with 2FA."""
# Step 1: Submit credentials
login_response = self.session.post(
f"{self.base_url}/api/login",
json={"username": username, "password": password}
)

if login_response.status_code != 200:
print(f"Login failed: {login_response.text}")
return False

# Step 2: Submit 2FA code
totp = pyotp.TOTP(self.secret_key)
code = totp.now()

verify_response = self.session.post(
f"{self.base_url}/api/2fa",
json={"code": code}
)

if verify_response.status_code != 200:
print(f"2FA failed: {verify_response.text}")
return False

print("Successfully authenticated!")
return True

def get(self, endpoint: str) -> Optional[requests.Response]:
"""Make authenticated GET request."""
return self.session.get(f"{self.base_url}{endpoint}")

# Usage
auth = TwoFactorAuth(
base_url="https://example.com",
secret_key=os.environ["TOTP_SECRET"]
)

if auth.login("username", "password"):
response = auth.get("/api/protected-data")
print(response.json())

Summary​

MethodBest ForRequirements
Manual InputOccasional scripts, debuggingPhone access
PyOTP (Automated)Servers, cron jobs, bots2FA Secret Key

Key Points:

  • Always use requests.Session() to maintain cookies between login steps
  • Store TOTP secrets securely using environment variables
  • Handle timing edge cases for codes near expiration
  • The secret key is shown once during 2FA setup; save it then