Skip to content

Two Factor Authentication

voyz edited this page May 10, 2023 · 5 revisions

While the Gateway doesn't support any interface for two-factor authentication (2FA), IBeam provides a method for attempting to complete the 2FA in various forms.

It is done by stopping the authentication process while the 2FA request is detected and launching a callback which will acquire the 2FA code and return it back to authentication process. Upon receiving a correct code, IBeam will input it into the 2FA form and continue with the authentication.

Currently, there are two built-in handlers for 2FA, however IBeam also allows you to provide your own custom 2FA handler using Inputs Directory.

You can select which handler you want to use by setting the IBEAM_TWO_FA_HANDLER environment variable to:

  • GOOGLE_MSG - for Google Messages Handler
  • EXTERNAL_REQUEST - for External Request Handler
  • CUSTOM_HANDLER - for Custom 2FA Handler

Built-in 2FA Handlers

IBeam comes with two built-in 2FA handlers:

  • Google Messages Handler - acquiring the code from an SMS at Google Messages
  • External Request Handler - acquiring the code through an external HTTP request

Google Messages Handler

  1. Set IBEAM_TWO_FA_HANDLER to GOOGLE_MSG.

  2. Set IBEAM_TWO_FA_SELECT_TARGET to One Time Passcode

  3. Open Google Messages app on your phone and tap settings (⋮) menu at the top right.

  4. Tap "Messages for web" and then "QR code scanner"

  5. Run IBeam and you will be presented with a URL to a QR code. You’ll need to observe the IBeam logs and authenticate using the QR. IBeam will present this in the following format:

    Web messages is not authenticated. Open this URL to pair web messages with your android phone:
    http://api.qrserver.com/v1/create-qr-code/?color=000000&bgcolor=FFFFFF&qzone=1&margin=0&size=400x400&ecc=L&data=https%3A//support.google.com/messages/%3Fp%3Dweb_computer%23%3Fc%3DCj0AqX3tUQSryndVWbP6rfpNx26g0EvyR0fHTrw/MsDjcU0ORObLOBE22gJE1lxMBuHzJdhRG21voGwPwZaXEiD6Kd8NMqpjXmGHVOOrhGWc5rM7AosbrbF5jGJCyYizeBog%2B8zUHGyh%2BHOW%2BBsAf8CGrY3tC2xacO9b0O2x58LfVLA%3D
    
  6. Copy this URL, open it in a browser and authenticate using Google Messages by scanning the resulting QR code with your phone.

How it works?

Google Messages can be used to receive the 2FA SMS code.

When IBeam first runs with this handler, it will generate a unique URL that you need to access to complete the Google Messages registration.

From then on, upon 2FA request IBeam will access that Google Messages account and read the most recent SMS, content of which will be parsed for the 2FA code.

The integration functions by looking for an "unread" (bolded) message on your phone with a verification code from Interactive Brokers. If you accidentally read/open this message using your phone before IBeam finds it, you will have to wait for the timeout and second attempt.

External Request Handler

  1. Set IBEAM_TWO_FA_HANDLER to EXTERNAL_REQUEST.
  2. Expects any 2FA method.

External Request Handler will request the code from an external server using HTTP. It is expected that you have a form of acquiring the 2FA code independently of IBeam.

You can provide the following environment variables for the HTTP request:

Variable name Default value Description
IBEAM_EXTERNAL_REQUEST_METHOD GET Method to use by the external request 2FA handler.
IBEAM_EXTERNAL_REQUEST_URL None URL to use by the external request 2FA handler.
IBEAM_EXTERNAL_REQUEST_TIMEOUT 300 Timeout for the external 2FA request.
IBEAM_EXTERNAL_REQUEST_PARAMS None JSON-formatted URL params to use by the external request 2FA handler.
IBEAM_EXTERNAL_REQUEST_DATA None JSON-formatted POST data to use by the external request 2FA handler.
IBEAM_EXTERNAL_REQUEST_HEADERS None JSON-formatted headers to use by the external request 2FA handler.

For example:

  • IBEAM_EXTERNAL_REQUEST_METHOD: POST
  • IBEAM_EXTERNAL_REQUEST_URL: https://my-example-server.com/ibkr-two-fa
  • IBEAM_EXTERNAL_REQUEST_TIMEOUT: 120
  • IBEAM_EXTERNAL_REQUEST_PARAMS: {"example_url_param": "example_param_value"}
  • IBEAM_EXTERNAL_REQUEST_DATA: {"example_post_data": "example_data_value"}
  • IBEAM_EXTERNAL_REQUEST_HEADERS: {"Content-Type": "application/json"}

Custom 2FA Handler

  1. Set IBEAM_TWO_FA_HANDLER to CUSTOM_HANDLER to use it.
  2. Expects any 2FA method.

IBeam supports using your own custom 2FA handler class provided through the Inputs Directory that will be used to acquire the 2FA code. This class must inherit from ibeam.src.two_fa_handlers.two_fa_handler.TwoFaHandler and implement the method get_two_fa_code which will be called to acquire the code.

Once you create the custom 2FA handler class, place it in the Inputs Directory and specify its fully qualified name as IBEAM_CUSTOM_TWO_FA_HANDLER environment variable following the pattern: MODULE_NAME.CLASS_NAME. For example, for a class CustomTwoFaHandler located in a file called 'custom_two_fa_handler.py' you'd set the environment variable as custom_two_fa_handler.CustomTwoFaHandler.

IBeam will not make any assumptions about this class or the get_two_fa_code method, other than that it will return the 2FA code. Make sure that it doesn't block the execution of IBeam indefinitely upon failure.

Additionally, to provide more runtime logging information you can implement the __str__ method which IBeam will use to log your custom 2FA handler before execution.

Risks

The 2FA process is not using an officially supported automatic interface, therefore 2FA support is provided on basis of our best attempts to automatically perform the 2FA code acquisition and input. We provide no guarantee that the process will always complete or prevent account lock-outs due to unsuccessful attempts - although IBeam will attempt to minimise the probability of this happening.

Restrictions

Due to implied volatility of the 2FA authentication process, IBeam has a number of built-in security measures that should minimise issues caused by invalid authentication:

Strict 2FA Code

By default IBeam will parse the 2FA code returned by the handlers and accept only codes that are made of 6 digits (although both int and str are valid codes). You can disable this check by setting IBEAM_STRICT_TWO_FA_CODE to False.

Maximum failed attempts

As of writing this, IBKR performs a 24-hour lock-out on an account upon 10 consecutive unsuccessful login attempts. Any successful login is meant to reset this counter.

To prevent this lock-out from happening, IBeam will count the amount of consecutive failed attempts and shut down if authentication fails too many times. The environment variable IBEAM_MAX_FAILED_AUTH defines the maximum number of failed authentication attempts that IBeam will allow. To disable this functionality, set this environment variable to a very large number.

Automating 2FA

While IBeam currently doesn't have any method of automating the 2FA out of the box, a number of suggestions were made by our users. This section lists these potential solutions:

Twilio

Twilio - method 1

By @jembl

https://github.com/Voyz/ibeam/issues/8#issuecomment-763353923

  1. I got a www.twilio.com account and got a number, changed my IB phone number to twilio phone number.
  2. I add a webhook for SMS, when an SMS is received, it will call my webserver i.e. www.sample.com/smshook
  3. use this code to parse the SMS on the server side and upload it to a storage of my choice
public String receive(String message) {
        StringBuilder sb = new StringBuilder();
        ResourceSet<Message> messages = Message.reader().limit(10).read();

        for(Message record : messages) {
            if (!smsStorageManager.getSmsWithSid(record.getSid()).isPresent()) {
                String messageSid = record.getSid();
                String messageBody = record.getBody();
                try {
                    log.info("received message with SID " + record.getSid());
                    log.info("received message with BODY " + record.getBody());
                    String[] splits = messageBody.split(":");
                    if (messageBody.startsWith("Your requested authentication code: ")) {
                        String code = splits[1].trim();
                        twoFactorCodeBlobStorageManager.insertIntoCodeBlob(code);
                    }
                    sb.append(messageSid + "\n");
                } catch (Exception e) {
                    log.error("Failed to process the message " + messageBody, e);
                } finally {
                    Message.deleter(messageSid).delete();
                }
            }
        }

        return sb.toString();
    }
  • On the container side i read the code from storage after login inside gateway_client.py:

(Note that carrying out the following logic can now be executed using a custom TwoFaHandler, without having to modify and build the IBeam image as suggested)

sf_select_present = EC.presence_of_element_located((By.ID, _SF_SELECT_EL_ID))
WebDriverWait(driver, 15).until(sf_select_present)
_LOGGER.info("sf_select has appeared")
sf_select_el = Select(driver.find_element_by_id(_SF_SELECT_EL_ID))
sf_select_el.select_by_value('4.2')
_LOGGER.info("sf_select one time passcode")

two_factor_input_present = EC.presence_of_element_located((By.ID, _TWO_FAC_EL_ID))
WebDriverWait(driver, 15).until(two_factor_input_present)
time.sleep(45)
two_factor_el = driver.find_element_by_id(_TWO_FAC_EL_ID)
_LOGGER.info(two_factor_el.text)
two_factor_el.send_keys(get_latest_authentication_code())

_LOGGER.info('Submitting the form for two fac')
submit_form_el = driver.find_element_by_id(_SUBMIT_EL_ID)
submit_form_el.click()

success_present = EC.text_to_be_present_in_element((By.TAG_NAME, 'pre'), _SUCCESS_EL_TEXT)
WebDriverWait(driver, _OAUTH_TIMEOUT).until(success_present)
_LOGGER.info(driver.page_source.encode("utf-8"))

_LOGGER.debug('Client login succeeds')

and the code to get SMS from storage:

def get_latest_authentication_code():
    connection_string = "YAYAYAYAYYA"
    blob_client = BlobClient.from_connection_string(
        connection_string, container_name="sms", blob_name="code.text")
    # [START download_a_blob]
    _LOGGER.info("downloading the code from blob")
    download_stream = blob_client.download_blob()
    code = download_stream.content_as_text()
    _LOGGER.info("done downloading the code from blob ")
    _LOGGER.info(str(code))
    return code

Twilio - method 2

By @kashyappatel7

https://github.com/Voyz/ibeam/issues/8#issuecomment-1542568655

I wanted to show some Node.js code for implementing the EXTERNAL_REQUEST without needing a external database/datastore:

Basically, you setup a Twilio account and get a phone number and then setup a SMS webhook to https://your-site.com/sms/incoming. Then paste in the following code to your Node.js express web server:

let smsToken;

router.get('/sms/code', async (req, res) => {    
    try {
        if(req.query.token != 'ANY_STRING_VALUE_FOR_SECURITY') {
            console.log('Invalid token specified in query param.');
            res.send({});
            return;
        } else {

            console.log('request to /sms/code');

            smsToken = undefined;
            
            let waitCountMax = 120; // seconds
            let waitCount = 0;
            while(waitCount < waitCountMax) {
                waitCount++;
                if(!smsToken) {
                    await new Promise(done => setTimeout(done, 1000));
                } else {
                    break;
                }
            }

            if(smsToken) {
                console.log(`Responding with verificationCode: ${smsToken.verificationCode} obtained at ${smsToken.dateTime}`);
                res.send(smsToken.verificationCode);
                smsToken = undefined;
            } else {
                console.log('No SMS token received.');
                res.send({});
            }
        }        
    } catch(e) {
        console.error(e);
        res.send({});
    }    
});

router.post('/sms/incoming', async (req, res) => {    
    try {
        // "Your requested authentication code: 289026"
        let parts = req.body.Body.split(':');

        if(parts.length == 2) {
            let codePart = parts[1].trim();
            if(parts[0] == 'Your requested authentication code' && codePart.length == 6) {
                console.log(`Parsed Verification Code: ${codePart}`);
                
                
                smsToken = { 
                    verificationCode: codePart, 
                    dateTime: (new Date()).toISOString()
                };
                
            } else {
                console.log('Invalid verification code.');
                console.log(JSON.stringify(req.body));
            }
        } else {
            console.log('Non-verification code SMS detected.');
            console.log(JSON.stringify(req.body));
        }
    } catch(e) {
        console.error(e);
    } finally {
        res.writeHead(200, { 'Content-Type': 'text/xml' });
        res.end('<Response />');
    }    
});

Next, start your docker container using something like:

docker run --env IBEAM_ACCOUNT=YOUR_PAPER_OR_LIVE_ACCOUNT_ID --env IBEAM_PASSWORD=YOUR_PWD --env IBEAM_TWO_FA_HANDLER=EXTERNAL_REQUEST --env IBEAM_EXTERNAL_REQUEST_URL=https://your-site.com/sms/code --env IBEAM_EXTERNAL_REQUEST_PARAMS={\"token\":\"ANY_STRING_VALUE_FOR_SECURITY\"} --env IBEAM_LOG_LEVEL=DEBUG --env IBEAM_PAGE_LOAD_TIMEOUT=500 -p 5000:5000 voyz/ibeam

Disabling 2FA

IBKR told one of our users that disabling 2FA is not supported at this time:

"We would like to inform you that for security reasons it is not possible to disable the 2 factor authentication as without 2 factor authentication your account is prone to malicious attacks and activities. Hence you will not be able to permanently disable the 2 factor authentication for your secondary user as well."


Next

Consider contributing to IBeam. Have a look at the Roadmap to get started.