Python Requets: How to Access Sites with 2FA Using Python Requests
Automating logins to websites protected by Two-Factor Authentication (2FA) is a common requirement for web scraping, API testing, and building automated workflows. However, 2FA adds a layer of complexity that simple scripts often fail to handle correctly. The login process becomes a multi-step conversation with the server, and your script must maintain state across each step.
This guide explains how to manage persistent sessions with requests.Session, handle manual 2FA code entry, and fully automate code generation using the pyotp library.
Why Sessions Matter for 2FA
The most critical mistake when handling 2FA is using independent requests.post() calls for each step. A 2FA login is not a single request. It is a sequence of requests where the server tracks your progress through cookies, session tokens, and CSRF tokens. If you lose that state between steps, the server will reject your 2FA code because it has no way to associate it with the password you just provided.
The Wrong Way: Independent Requests
import requests
login_url = "https://example.com/api/login"
verify_url = "https://example.com/api/verify-2fa"
# Step 1: submit credentials
response = requests.post(login_url, json={"user": "admin", "pass": "secret123"})
# Step 2: submit 2FA code with a completely separate request
otp_code = input("Enter code: ")
verify_resp = requests.post(verify_url, json={"token": otp_code})
# This will almost certainly fail.
# The server does not know this request belongs to the same login attempt.
print(verify_resp.status_code)
Why this fails: Each requests.post() call starts with a blank slate. The cookies and session tokens from the first request are not carried over to the second. The server sees the 2FA verification as coming from an unknown, unauthenticated client.
The Right Way: Using requests.Session
import requests
# Always use a Session object for multi-step authentication
session = requests.Session()
login_url = "https://example.com/api/login"
verify_url = "https://example.com/api/verify-2fa"
# Step 1: submit credentials
payload = {"user": "admin", "pass": "secret123"}
response = session.post(login_url, json=payload)
if response.status_code == 200:
# Step 2: submit the 2FA code using the SAME session
otp_code = input("Enter the code sent to your device: ")
verify_resp = session.post(verify_url, json={"token": otp_code})
if verify_resp.ok:
print("Login complete. Session is now fully authenticated.")
# Step 3: access protected resources with the authenticated session
protected_data = session.get("https://example.com/api/dashboard")
print(protected_data.json())
When you use requests.Session(), the session object automatically stores and sends cookies (like session_id, csrf_token, and authentication cookies) across all requests. This is what allows the server to recognize your 2FA verification as belonging to the same login attempt that provided the password.
Handling the Full Login Flow
Real-world 2FA login flows often involve additional details like CSRF tokens, custom headers, or intermediate redirects. Here is a more complete example that accounts for these:
import requests
session = requests.Session()
login_url = "https://example.com/api/login"
verify_url = "https://example.com/api/verify-2fa"
# Step 1: visit the login page to obtain any CSRF tokens
login_page = session.get(login_url)
# Step 2: submit credentials
credentials = {"user": "admin", "pass": "secret123"}
login_response = session.post(login_url, json=credentials)
if login_response.status_code != 200:
print(f"Login failed: {login_response.status_code}")
print(login_response.text)
else:
print("Credentials accepted. 2FA code required.")
# Step 3: get the 2FA code (manual input in this example)
otp_code = input("Enter the code sent to your device: ")
# Step 4: submit the 2FA code
verify_response = session.post(verify_url, json={"token": otp_code})
if verify_response.ok:
print("Fully authenticated.")
# The session now carries all authentication cookies
# All subsequent requests are authenticated automatically
profile = session.get("https://example.com/api/profile")
print(profile.json())
else:
print(f"2FA verification failed: {verify_response.status_code}")
Fully Automating 2FA with pyotp
If you are building an automated script and have access to the secret key (the string used when initially setting up an authenticator app like Google Authenticator), you can generate the time-based codes directly in Python without any manual input.
Installing pyotp
pip install pyotp
Generating TOTP Codes
import pyotp
# The secret key from your 2FA setup
# In production, load this from an environment variable or secrets manager
SECRET = "JBSWY3DPEHPK3PXP"
# Create a TOTP (Time-based One-Time Password) generator
totp = pyotp.TOTP(SECRET)
# Generate the code that is valid right now (changes every 30 seconds)
current_code = totp.now()
print(f"Current 2FA code: {current_code}")
# You can also verify a code
print(f"Is '123456' valid? {totp.verify('123456')}")
Output (example, varies by time):
Current 2FA code: 492039
Is '123456' valid? False
Combining Sessions with pyotp for Full Automation
import requests
import pyotp
import os
session = requests.Session()
login_url = "https://example.com/api/login"
verify_url = "https://example.com/api/verify-2fa"
# Load credentials and 2FA secret from environment variables
username = os.environ.get("APP_USERNAME")
password = os.environ.get("APP_PASSWORD")
totp_secret = os.environ.get("APP_TOTP_SECRET")
# Step 1: submit credentials
login_response = session.post(login_url, json={
"user": username,
"pass": password
})
if login_response.status_code == 200:
# Step 2: generate and submit the 2FA code automatically
totp = pyotp.TOTP(totp_secret)
otp_code = totp.now()
verify_response = session.post(verify_url, json={"token": otp_code})
if verify_response.ok:
print("Fully authenticated via automated 2FA.")
# Access protected resources
data = session.get("https://example.com/api/protected-data")
print(data.json())
else:
print(f"2FA verification failed: {verify_response.status_code}")
else:
print(f"Login failed: {login_response.status_code}")
The TOTP secret key grants the same access as your password combined with your authenticator app. Never hardcode it in your scripts. Store it in environment variables, a .env file excluded from version control, or a dedicated secrets manager like AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault.
Handling Timing Issues
TOTP codes are valid for a 30-second window. If your code is generated near the end of that window, it might expire before the server processes it. You can handle this with a simple retry:
import pyotp
import time
import requests
def submit_2fa_with_retry(session, verify_url, totp_secret, max_retries=2):
"""Submit a 2FA code, retrying with a fresh code if the first attempt fails."""
totp = pyotp.TOTP(totp_secret)
for attempt in range(max_retries):
code = totp.now()
response = session.post(verify_url, json={"token": code})
if response.ok:
return response
print(f"Attempt {attempt + 1} failed. Waiting for next code window...")
# Wait for the next 30-second window
time.sleep(30)
return None
You can check how many seconds remain in the current TOTP window using totp.interval - (time.time() % totp.interval). If fewer than 5 seconds remain, consider waiting for the next code before submitting.
Quick Reference
| Requirement | Solution | Key Tool |
|---|---|---|
| Maintain login state across requests | Persistent session with automatic cookie handling | requests.Session() |
| Submit a 2FA code manually | Multi-step POST with user input | session.post() + input() |
| Generate 2FA codes automatically | Programmatic TOTP generation from secret key | pyotp.TOTP() |
| Store secrets securely | Environment variables or secrets manager | os.environ or vault service |
Automating logins must comply with the target website's Terms of Service. Some platforms actively monitor for programmatic access and may ban accounts that bypass 2FA flows too rapidly or in unusual patterns. Always review the site's policies before building automated authentication scripts.
Summary
Accessing 2FA-protected sites with Python Requests requires two key techniques.
First, always use requests.Session() to maintain cookies and authentication state across the multi-step login flow. Without a persistent session, the server cannot associate your 2FA code with the credentials you submitted earlier.
Second, if you need full automation, use the pyotp library to generate time-based one-time passwords from your 2FA secret key, eliminating the need for manual code entry.
Keep your secret keys secure by loading them from environment variables or a secrets manager, and handle timing edge cases with simple retry logic.