Skip to content

Twilio 2FA with no dependencies #264

@tori-takashi

Description

@tori-takashi

My way to bypass 2FA with twilio

It's just made by Claude Code but it works well so I'd love to share it.
The advantages of this way is to work just copy and paste, no additional dependency.

Directory structure

$ tree
.
├── ibeam-input
│   ├── conf.yaml
│   └── twilio_2fa_handler.py
├── .env
└── docker-compose.yml

1. conf.yaml

ip2loc: "US"
proxyRemoteSsl: true
proxyRemoteHost: "https://api.ibkr.com"
listenPort: 5000
listenSsl: true
ccp: false
svcEnvironment: "v1"
sslCert: "vertx.jks"
sslPwd: "mywebapi"
authDelay: 3000
portalBaseURL: ""
serverOptions:
  blockedThreadCheckInterval: 1000000
  eventLoopPoolSize: 20
  workerPoolSize: 20
  maxWorkerExecuteTime: 100
  internalBlockingPoolSize: 20
cors:
  origin.allowed: "*"
  allowCredentials: false
webApps:
  - name: "demo"
    index: "index.html"
ips:
  allow:
    - 10.*
    - 192.*
    - 131.216.*
    - 172.* # docker internal
    - 127.0.0.1 # localhost
    - x.x.x.x # IP address of your machine used to call the API
  deny:
    - 212.90.324.10
    - 0.0.0.0/0 # all other addresses

2. twilio_2fa_handler.py

You may need edit # Common patterns for 2FA codes patterns

"""
IBeam Custom 2FA Handler using Twilio SMS API
This handler retrieves 2FA codes from Twilio SMS messages for Interactive Brokers authentication.
"""

import os
import re
import logging
import time
from datetime import datetime, timedelta
from typing import Optional
import requests
from requests.auth import HTTPBasicAuth
from selenium import webdriver
from ibeam.src.two_fa_handlers.two_fa_handler import TwoFaHandler

# Setup logging with console handler only
logger = logging.getLogger(__name__)

if not logger.handlers:
    logger.setLevel(logging.INFO)

    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)

    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )
    console_handler.setFormatter(formatter)

    logger.addHandler(console_handler)


class TwilioTwoFaHandler(TwoFaHandler):
    """
    Custom 2FA handler that retrieves authentication codes from Twilio SMS.

    This handler uses the Twilio REST API to fetch SMS messages and extract
    the 6-digit 2FA code sent by Interactive Brokers.

    Required environment variables:
        - TWILIO_ACCOUNT_SID: Your Twilio Account SID
        - TWILIO_AUTH_TOKEN: Your Twilio Auth Token
        - TWILIO_PHONE_NUMBER: Your Twilio phone number (format: +1234567890)
    """

    def __init__(self, outputs_dir: str = None):
        """Initialize Twilio handler with credentials from environment variables.

        Args:
            outputs_dir: Directory for outputs (required by IBeam, but not used by this handler)
        """
        super().__init__(outputs_dir=outputs_dir)

        self.account_sid = os.getenv('TWILIO_ACCOUNT_SID')
        self.auth_token = os.getenv('TWILIO_AUTH_TOKEN')
        self.phone_number = os.getenv('TWILIO_PHONE_NUMBER')

        # Configurable retry settings
        self.max_attempts = int(os.getenv('TWILIO_MAX_ATTEMPTS', '10'))
        self.wait_seconds = int(os.getenv('TWILIO_WAIT_SECONDS', '5'))
        self.search_window_minutes = int(os.getenv('TWILIO_SEARCH_WINDOW_MINUTES', '5'))

        if not all([self.account_sid, self.auth_token, self.phone_number]):
            raise ValueError(
                "Missing required Twilio environment variables: "
                "TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER"
            )

        # Twilio API configuration
        self.base_url = f"https://api.twilio.com/2010-04-01/Accounts/{self.account_sid}"
        self.auth = HTTPBasicAuth(self.account_sid, self.auth_token)

        logger.info(f"Twilio 2FA Handler initialized (phone: {self.phone_number})")

    def __str__(self) -> str:
        """Return string representation for logging."""
        return f"TwilioTwoFaHandler(phone={self.phone_number}, max_attempts={self.max_attempts})"

    def extract_2fa_code(self, message_body: str) -> Optional[str]:
        """
        Extract 2FA code from SMS message body.

        Interactive Brokers typically sends codes in various formats.
        This method tries multiple patterns to find the 6-digit code.

        Args:
            message_body: The SMS message body

        Returns:
            The extracted 6-digit code as string, or None if not found
        """
        if not message_body:
            return None

        # Common patterns for 2FA codes
        patterns = [
            # English patterns
            r'(?:code|verification code|pin|security code)(?:\s+is)?[:\s]+(\d{6})',
            r'(?:your|the)\s+(?:code|pin)\s+(?:is|:)\s+(\d{6})',
            r'\b(\d{6})\b',  # Any 6 digits surrounded by word boundaries
            r'(\d{3}[-\s]\d{3})',  # "123-456" or "123 456"
        ]

        for pattern in patterns:
            match = re.search(pattern, message_body, re.IGNORECASE)
            if match:
                # Remove any spaces or dashes from the code
                code = match.group(1).replace(' ', '').replace('-', '')
                if len(code) == 6 and code.isdigit():
                    logger.info(f"Extracted 2FA code from message: {code}")
                    return code

        logger.warning(f"Could not extract 2FA code from message: {message_body}")
        return None

    def get_latest_sms(self) -> Optional[str]:
        """
        Retrieve the latest SMS message received within the configured time window.

        Returns:
            The message body, or None if no message found
        """
        try:
            # Calculate the time threshold
            date_sent_after = datetime.now() - timedelta(minutes=self.search_window_minutes)

            # Build request parameters
            params = {
                'To': self.phone_number,
                'PageSize': 20,  # Get last 20 messages
            }

            # Make GET request to Twilio Messages API
            url = f"{self.base_url}/Messages.json"
            response = requests.get(url, auth=self.auth, params=params, timeout=10)

            # Check response status
            if response.status_code != 200:
                logger.error(
                    f"Twilio API error: {response.status_code} - {response.text}"
                )
                return None

            # Parse response
            data = response.json()
            messages = data.get('messages', [])

            if not messages:
                logger.warning(f"No SMS messages found in the last {self.search_window_minutes} minutes")
                return None

            # Filter messages by date and return the most recent one
            for message in messages:
                date_sent_str = message.get('date_sent')
                if date_sent_str:
                    # Parse Twilio date format
                    date_sent = datetime.strptime(
                        date_sent_str, '%a, %d %b %Y %H:%M:%S %z'
                    )
                    if date_sent.replace(tzinfo=None) >= date_sent_after:
                        body = message.get('body', '')
                        from_number = message.get('from', '')
                        logger.info(
                            f"Found SMS from {from_number} "
                            f"sent at {date_sent_str}: {body}"
                        )
                        return body

            logger.warning(f"No recent messages found within {self.search_window_minutes} minutes")
            return None

        except requests.exceptions.RequestException as e:
            logger.error(f"Request error retrieving SMS from Twilio: {str(e)}", exc_info=True)
            return None
        except Exception as e:
            logger.error(f"Error retrieving SMS from Twilio: {str(e)}", exc_info=True)
            return None

    def get_two_fa_code(self, driver: webdriver.Chrome) -> Optional[str]:
        """
        Retrieve the 2FA code from the latest SMS message.

        This is the main method called by IBeam. It will retry multiple times
        if no code is found initially, waiting between attempts.

        Args:
            driver: The Chrome WebDriver instance (not used by this handler, but required by interface)

        Returns:
            The 6-digit 2FA code as string, or None if not found after all attempts
        """
        logger.info(f"Starting 2FA code retrieval via Twilio SMS (attempts: {self.max_attempts})")

        for attempt in range(1, self.max_attempts + 1):
            logger.info(f"Attempt {attempt}/{self.max_attempts} to retrieve 2FA code")

            # Get the latest SMS
            message_body = self.get_latest_sms()

            if message_body:
                # Try to extract the code
                code = self.extract_2fa_code(message_body)
                if code:
                    logger.info(f"Successfully retrieved 2FA code: {code}")
                    return code

            # Wait before retrying (except on the last attempt)
            if attempt < self.max_attempts:
                logger.info(f"No code found, waiting {self.wait_seconds} seconds before retry...")
                time.sleep(self.wait_seconds)

        logger.error("Failed to retrieve 2FA code after all attempts")
        return None


# For standalone testing
def main():
    """Test function to verify the handler works correctly."""
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

    try:
        handler = TwilioTwoFaHandler()
        print(f"Handler initialized: {handler}")

        # Note: driver parameter is not used by Twilio handler, passing None
        code = handler.get_two_fa_code(None)

        if code:
            print(f"\nSuccess! Retrieved 2FA code: {code}")
            return code
        else:
            print("\nFailed to retrieve 2FA code")
            return None

    except Exception as e:
        logger.error(f"Error in main: {str(e)}", exc_info=True)
        return None


if __name__ == '__main__':
    main()

3. .env

# Interactive Brokers Client Portal API Settings
IBEAM_ACCOUNT=
IBEAM_PASSWORD=
IBEAM_TWO_FA_HANDLER=CUSTOM_HANDLER
IBEAM_CUSTOM_TWO_FA_HANDLER=twilio_2fa_handler.TwilioTwoFaHandler
IBEAM_MAX_FAILED_AUTH=3
IBEAM_PAGE_LOAD_TIMEOUT=20

# https://github.com/Voyz/ibeam/issues/198
IBEAM_TWO_FA_EL_ID=ID@@xyz-field-silver-response
IBEAM_TWO_FA_INPUT_EL_ID=ID@@xyz-field-silver-response

IB_HOST=host.docker.internal
IB_PORT=5000

# Twilio 2FA Settings
# Get these from https://console.twilio.com/
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_PHONE_NUMBER=  # Your Twilio phone number in format: +1234567890

# Twilio Handler Optional Settings
TWILIO_MAX_ATTEMPTS=10  # Maximum retry attempts to fetch 2FA code
TWILIO_WAIT_SECONDS=2  # Seconds to wait between retry attempts
TWILIO_SEARCH_WINDOW_MINUTES=1  # How far back to search for SMS messages

4. docker-compose.yml

services:
  ibeam:
    image: voyz/ibeam
    container_name: ibeam
    env_file:
      - .env
    ports:
      - 5000:5000
      - 5001:5001
    volumes:
      - ./ibeam-input:/srv/inputs
    restart: 'no' # Prevents IBEAM_MAX_FAILED_AUTH from being exceeded
    healthcheck:
      test: ["CMD-SHELL", "curl -sk https://localhost:5000/v1/api/iserver/auth/status | grep -q '\"authenticated\":true'"]
      interval: 2s
      timeout: 5s
      retries: 12
      start_period: 60s

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions