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