Skip to content

Python Requests: How to Access Sites with 2FA Using Python Requests

Learn how to automate 2FA logins in Python using requests.Session for session persistence and pyotp for automatic TOTP code generation.

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"

response = requests.post(login_url, json={"user": "admin", "pass": "secret123"})

otp_code = input("Enter code: ")
verify_resp = requests.post(verify_url, json={"token": otp_code})

print(verify_resp.status_code)

The Right Way: Using requests.Session

import requests

session = requests.Session()

login_url = "https://example.com/api/login"
verify_url = "https://example.com/api/verify-2fa"

payload = {"user": "admin", "pass": "secret123"}
response = session.post(login_url, json=payload)

if response.status_code == 200:
    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.")

        protected_data = session.get("https://example.com/api/dashboard")
        print(protected_data.json())

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"

login_page = session.get(login_url)

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.")

    otp_code = input("Enter the code sent to your device: ")

    verify_response = session.post(verify_url, json={"token": otp_code})

    if verify_response.ok:
        print("Fully authenticated.")

        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

SECRET = "JBSWY3DPEHPK3PXP"

totp = pyotp.TOTP(SECRET)

current_code = totp.now()
print(f"Current 2FA code: {current_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"

username = os.environ.get("APP_USERNAME")
password = os.environ.get("APP_PASSWORD")
totp_secret = os.environ.get("APP_TOTP_SECRET")

login_response = session.post(login_url, json={
    "user": username,
    "pass": password
})

if login_response.status_code == 200:
    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.")

        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}")

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...")
        time.sleep(30)

    return None

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

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.

Navigation