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:
- Submit username and password
- 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")
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
| Method | Best For | Requirements |
|---|---|---|
| Manual Input | Occasional scripts, debugging | Phone access |
| PyOTP (Automated) | Servers, cron jobs, bots | 2FA 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