-
Notifications
You must be signed in to change notification settings - Fork 140
Open
Labels
enhancementNew feature or requestNew feature or request
Description
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.yml1. 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 addresses2. 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 messages4. 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: 60sMetadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request