Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem: 2-Factor Authentication using IBEAM_TWO_FA_HANDLER=EXTERNAL_REQUEST #185

Closed
xjcarter opened this issue May 13, 2024 · 15 comments
Closed
Labels
bug Something isn't working

Comments

@xjcarter
Copy link

Hi All!
I am attempting to set up my 2FA automated authentication within IBeam
and have hit a roadblock I have been frantically trying to debug before asking for your help.

To be quick - here's the situation.

  1. I have set up a server endpoint http://{cloud_ip:cloud_port}/sms/code that returns the IBRK authentication code.
    NOTE The response is NOT wrapped in JSON - just the string of the authenticaton code.
    Per the wiki, I assume that is what IBeam is expecting. Is that correct?

  2. I have thoroughly tested my 'auth_code_server' and it works as expected:
    a. When IBeam triggers an authentication SMS from IBRK- the message is caught by the server and correctly parsed
    and is ready to provide the desired auth code to IBeam via a GET request to the http://{cloud_ip:cloud_port}/sms/code endpoint.
    b. the needed firewall port for the 'auth_code_server' is open - and tests have accessed it outside being on localhost.
    (I have manually logged into my live IBRK account from which the external server catches and saves the authentication code correctly)
    c. How long does IBeam wait to get that authentication code? And which IBEAM environment variable controls that?

  3. I dumped all my IBeam variables in an 'env.list' file which is the following:

$ cat /root/ibeam_files/env.list
IBEAM_ACCOUNT={my_account_name}
IBEAM_PASSWORD={my_password}
IBEAM_OUTPUTS_DIR=/srv/outputs
IBEAM_REQUEST_RETRIES=5
IBEAM_OAUTH_TIMEOUT=30
IBEAM_REQUEST_TIMEOUT=30
IBEAM_PAGE_LOAD_TIMEOUT=500
IBEAM_MAX_FAILED_AUTH=100
IBEAM_TWO_FA_HANDLER=EXTERNAL_REQUEST
IBEAM_EXTERNAL_REQUEST_URL=http://{cloud_ip:cloud_port}/sms/code
IBEAM_LOG_LEVEL=DEBUG
  1. When I spin up m IBeam container - the log looks like this (see below):
    Specifically:
    1. Why doesn't my IBEAM_EXTERNAL_REQUEST_URL show up in my 'Configuration' dictionary?
    (Oddly- IBEAM_TWO_FA_HANDLER=EXTERNAL_REQUEST does appear in the dictionary)
    2. Should I define each of the environment variables needed for IBEAM_TWO_FA_HANDLER=EXTERNAL_REQUEST
    explicitly? I was assumimg the defaults for IBEAM_TWO_FA_HANDLER=EXTERNAL_REQUEST to be set.

My IBeam launch:

$ docker run --rm --name ibeam --env-file /root/ibeam_files/env.list --network tradenet -p 5000:5000 -v /root/ibeam_files/inputs_directory/:/srv/inputs -v /root/ibeam_files/outputs:/srv/outputs:rw voyz/ibeam:0.5.3

The Log:

2024-05-12 01:22:39,029|I| ############ Starting IBeam version 0.5.3 ############
2024-05-12 01:22:39,031|I| Custom conf.yaml found and will be used by the Gateway
2024-05-12 01:22:39,034|I| Secrets source: env
2024-05-12 01:22:39,035|I| Health server started at port=5001
2024-05-12 01:22:39,035|I| Configuration:
{'INPUTS_DIR': '/srv/inputs/', 'OUTPUTS_DIR': '/srv/outputs', 'GATEWAY_DIR': '/srv/clientportal.gw', 'CHROME_DRIVER_PATH': '/usr/bin/chromedriver', 'GATEWAY_STARTUP': 20, 'GATEWAY_PROCESS_MATCH': 'ibgroup.web.core.clientportal.gw.GatewayStart', 'MAINTENANCE_INTERVAL': 60, 'SPAWN_NEW_PROCESSES': False, 'LOG_LEVEL': 'DEBUG', 'LOG_TO_FILE': True, 'LOG_FORMAT': '%(asctime)s|%(levelname)-.1s| %(message)s', 'REQUEST_RETRIES': 5, 'REQUEST_TIMEOUT': 30, 'RESTART_FAILED_SESSIONS': True, 'RESTART_WAIT': 15, 'REAUTHENTICATE_WAIT': 15, 'HEALTH_SERVER_PORT': 5001, 'SECRETS_SOURCE': 'env', 'GCP_SECRETS_URL': None, 'GATEWAY_BASE_URL': 'https://localhost:5000', 'ROUTE_AUTH': '/sso/Login?forwardTo=22&RL=1&ip2loc=on', 'ROUTE_VALIDATE': '/v1/portal/sso/validate', 'ROUTE_REAUTHENTICATE': '/v1/portal/iserver/reauthenticate?force=true', 'ROUTE_INITIALISE': '/v1/api/iserver/auth/ssodh/init', 'ROUTE_AUTH_STATUS': '/v1/api/iserver/auth/status', 'ROUTE_TICKLE': '/v1/api/tickle', 'ROUTE_LOGOUT': '/v1/api/logout', 'USER_NAME_EL': None, 'PASSWORD_EL': 'NAME@@password', 'SUBMIT_EL': 'CSS_SELECTOR@@.btn.btn-lg.btn-primary', 'ERROR_EL': None, 'SUCCESS_EL_TEXT': 'TAG_NAME@@Client login succeeds', 'OAUTH_TIMEOUT': 30, 'PAGE_LOAD_TIMEOUT': 500, 'ERROR_SCREENSHOTS': False, 'MAX_FAILED_AUTH': 100, 'MIN_PRESUBMIT_BUFFER': 5, 'MAX_PRESUBMIT_BUFFER': 30, 'MAX_IMMEDIATE_ATTEMPTS': 10, 'IBKEY_PROMO_EL_CLASS': 'CLASS_NAME@@ibkey-promo-skip', 'AUTHENTICATION_STRATEGY': 'B', 'MAX_STATUS_CHECK_RETRIES': 120, 'MAX_REAUTHENTICATE_RETRIES': 3, 'UI_SCALING': 1.0, 'TWO_FA_EL_ID': 'ID@@twofactbase', 'TWO_FA_NOTIFICATION_EL': 'CLASS_NAME@@login-step-notification', 'TWO_FA_INPUT_EL_ID': 'ID@@chlginput', 'TWO_FA_HANDLER': 'EXTERNAL_REQUEST', 'STRICT_TWO_FA_CODE': True, 'TWO_FA_SELECT_EL_ID': 'ID@@sf_select', 'TWO_FA_SELECT_TARGET': 'IB Key', 'CUSTOM_TWO_FA_HANDLER': 'custom_two_fa_handler.CustomTwoFaHandler'}
2024-05-12 01:22:39,036|I| Gateway not found, starting new one...
2024-05-12 01:22:39,036|I| Note that the Gateway log below may display "Open https://localhost:[PORT] to login" - ignore this command.
2024-05-12 01:22:39,037|I| Starting Gateway as Linux process with params: ['bash', 'bin/run.sh', 'root/conf.yaml']
running
 runtime path : root:dist/ibgroup.web.core.iblink.router.clientportal.gw.jar:build/lib/runtime/*
 config file  : root/conf.yaml
2024-05-12 01:22:39,046|I| Gateway started with pids: [12]
2024-05-12 01:22:39,047|D| GET https://localhost:5000 (unverified)
2024-05-12 01:22:39,049|I| Cannot ping Gateway. Retrying for another 20 seconds
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by io.netty.util.internal.ReflectionUtil (file:/srv/clientportal.gw/build/lib/runtime/netty-common-4.1.15.Final.jar) to constructor java.nio.DirectByteBuffer(long,int)
WARNING: Please consider reporting this to the maintainers of io.netty.util.internal.ReflectionUtil
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
2024-05-12 01:22:40,049|D| GET https://localhost:5000 (unverified)
2024-05-12 01:22:40,052|I| Cannot ping Gateway. Retrying for another 19 seconds
 -> mount demo on /demo
Java Version: 11.0.20
****************************************************
version: a27ed42161ad96c53e715ca5c5e3e3fa4cff5262 Mon, 24 Apr 2023 15:41:53 -0400
****************************************************
This is the Client Portal Gateway
for any issues, please contact api@ibkr.com
and include a copy of your logs
****************************************************
https://www.interactivebrokers.com/api/doc.html
****************************************************
Open https://localhost:5000 to login
App demo is available after you login under: https://localhost:5000/demo#/
2024-05-12 01:22:41,052|D| GET https://localhost:5000 (unverified)
2024-05-12 01:22:41,699|I| Gateway connection established
2024-05-12 01:22:41,700|D| POST https://localhost:5000/v1/api/tickle (unverified)
2024-05-12 01:22:41,834|I| NO SESSION Status(running=True, session=False, connected=False, authenticated=False, competing=False, collision=False, session_id=None, server_name=None, server_version=None, expires=None)
2024-05-12 01:22:41,835|I| Authentication strategy: "B"
2024-05-12 01:22:41,835|I| No active sessions, logging in...
2024-05-12 01:22:41,836|I| Loading auth webpage at https://localhost:5000/sso/Login?forwardTo=22&RL=1&ip2loc=on
2024-05-12 01:22:50,482|D| Targets: {'PASSWORD': Target(NAME@@password), 'SUBMIT': Target(CSS_SELECTOR@@.btn.btn-lg.btn-primary), 'SUCCESS': Target(TAG_NAME@@Client login succeeds), 'IBKEY_PROMO': Target(CLASS_NAME@@ibkey-promo-skip), 'TWO_FA': Target(ID@@twofactbase), 'TWO_FA_NOTIFICATION': Target(CLASS_NAME@@login-step-notification), 'TWO_FA_INPUT': Target(ID@@chlginput), 'TWO_FA_SELECT': Target(ID@@sf_select), 'USER_NAME': Target(NAME@@username), 'ERROR': Target(CSS_SELECTOR@@.xyz-errormessage)}
2024-05-12 01:22:50,532|I| Gateway auth webpage loaded
2024-05-12 01:22:50,532|I| Login attempt number 1
2024-05-12 01:22:50,738|D| target: Target(NAME@@username)
2024-05-12 01:22:56,036|I| Submitting the form
2024-05-12 01:23:26,744|E| Timeout reached when waiting for authentication. The website seems to not be loaded correctly. Consider increasing IBEAM_PAGE_LOAD_TIMEOUT.
Website URL: https://localhost:5000/sso/Login?forwardTo=22&RL=1&ip2loc=on

Exception:
  File "/srv/ibeam/ibeam_starter.py", line 168, in <module>
    success, shutdown, status = client.start_and_authenticate()
  File "/srv/ibeam/src/gateway_client.py", line 53, in start_and_authenticate
    success, shutdown, status = self.strategy_handler.try_authenticating(request_retries=request_retries)
  File "/srv/ibeam/src/handlers/strategy_handler.py", line 85, in try_authenticating
    return self._authentication_strategy_B(status, request_retries)
  File "/srv/ibeam/src/handlers/strategy_handler.py", line 140, in _authentication_strategy_B
    return self._log_in(status)
  File "/srv/ibeam/src/handlers/strategy_handler.py", line 151, in _log_in
    success, shutdown = self.login_handler.login()
  File "/srv/ibeam/src/handlers/login_handler.py", line 462, in login
    self.handle_timeout_exception(e, targets, driver, website_version, self.route_auth, self.base_url, self.outputs_dir)
  File "/srv/ibeam/src/handlers/login_handler.py", line 443, in login
    self.attempt(targets, wait_and_identify_trigger, driver)
  File "/srv/ibeam/src/handlers/login_handler.py", line 367, in attempt
    trigger, target = self.step_login(targets, wait_and_identify_trigger, driver, self.secrets_handler.account, self.secrets_handler.password, self.secrets_handler.key, self.presubmit_buffer)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/srv/ibeam/src/handlers/login_handler.py", line 188, in step_login
    trigger, target = wait_and_identify_trigger(
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/srv/ibeam/src/handlers/login_handler.py", line 67, in _wait_and_identify_trigger
    trigger = WebDriverWait(driver, timeout).until(any_of(*expected_conditions))
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/venv/lib/python3.11/site-packages/selenium/webdriver/support/wait.py", line 95, in until
    raise TimeoutException(message, screen, stacktrace)

  <class 'selenium.common.exceptions.TimeoutException'> Message:

2024-05-12 01:23:26,745|I| Cleaning up the resources. Display: <pyvirtualdisplay.display.Display object at 0x7f58aa2ed1d0> | Driver: <selenium.webdriver.chrome.webdriver.WebDriver (session="d14bc5c4713c8fb18d8c0e4df48617f2")>
2024-05-12 01:23:26,814|I| Logging in failed
2024-05-12 01:23:26,819|I| Starting maintenance with interval 60 seconds
2024-05-12 01:24:26,819|I| Maintenance

Here's my auth_server_code:

from flask import Flask, request, jsonify
import threading
import time
import logging
import json

## all messages at INFO level and above will be captured
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

app = Flask(__name__)

sms_token = None
lock = threading.Lock()

@app.route('/sms/code', methods=['GET'])
def get_sms_code():
    global sms_token
    try:

        logger.info('request to /sms/code')

        wait_count_max = 120  # seconds
        wait_count = 0
        while wait_count < wait_count_max:
            with lock:
                if sms_token is not None:
                    break
            time.sleep(1)
            wait_count += 1
            if wait_count % 20 == 0:
                logger.info(f'polling for verification code: {wait_count} of {wait_count_max} secs')
        if sms_token is None:
            logger.info(f'SMS polling interval expired.')

        with lock:
            if sms_token:
                rsp = f"Responding with verificationCode: {sms_token['verificationCode']} obtained at {sms_token['dateTime']}"
                logger.info(f'Response:\n {rsp}')
                response = sms_token['verificationCode']
                sms_token = None
                return response
            else:
                logger.info('No SMS token received.')
                return jsonify({'error': 'No SMS token received.'}), 404
    except Exception as e:
        logger.info(e)
        return jsonify({'error': 'Internal Error'}), 500

@app.route('/sms/incoming', methods=['POST'])
def post_sms_incoming():
    global sms_token

    VERFICATION_CODE_TAG = 'Your requested authentication code'
    try:
        logger.info(f'posted data:\n{json.dumps(request.json, indent=4)}')
        body = request.json.get('content', '')
        parts = body.split(':')

        if len(parts) == 2:
            message_tag, code_part = parts[0].strip(), parts[1].strip()
            if message_tag == VERFICATION_CODE_TAG and len(code_part) == 6:
                logger.info(f'Parsed Verification Code: {code_part}')
                with lock:
                    sms_token = {
                        'verificationCode': code_part,
                        'dateTime': time.strftime('%Y-%m-%dT%H:%M:%S')
                    }
            else:
                logger.info(f'Invalid verification code= {body}.')
        else:
            logger.info(f'Non-verification code SMS detected= {body}.')
    except Exception as e:
        logger.info(e)
    finally:
        response = {}
        if sms_token: response = sms_token
        return jsonify(response), 200



if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5051)

Thanks again for all your time, patience and help!

@xjcarter xjcarter added the bug Something isn't working label May 13, 2024
@Voyz
Copy link
Owner

Voyz commented May 14, 2024

Hey @xjcarter! Many thanks for submitting the detailed issue report - and if what GitHub is reporting is true: congrats on your first issue here! 👏

To address a few points here:

NOTE The response is NOT wrapped in JSON - just the string of the authenticaton code. Per the wiki, I assume that is what IBeam is expecting. Is that correct?

Correct, just a 6-digit string should be expected.

How long does IBeam wait to get that authentication code? And which IBEAM environment variable controls that?

It's defined by IBEAM_EXTERNAL_REQUEST_TIMEOUT, 300 seconds by default.

Why doesn't my IBEAM_EXTERNAL_REQUEST_URL show up in my 'Configuration' dictionary?

The full list of env vars supported by that handler is confusingly defined in the handler itself - as opposed to in the var.py file. This is why it doesn't show up. I'll fix that, thanks for bringing it to my attention. The full list is:

_EXTERNAL_REQUEST_METHOD = os.environ.get('IBEAM_EXTERNAL_REQUEST_METHOD', 'GET')
"""Method to use by the external request 2FA handler."""

_EXTERNAL_REQUEST_URL = os.environ.get('IBEAM_EXTERNAL_REQUEST_URL')
"""URL to use by the external request 2FA handler."""

_EXTERNAL_REQUEST_TIMEOUT = int(os.environ.get('IBEAM_EXTERNAL_REQUEST_TIMEOUT', 300))
"""Timeout for the external 2FA request."""

_EXTERNAL_REQUEST_PARAMS = os.environ.get('IBEAM_EXTERNAL_REQUEST_PARAMS')
"""JSON-formatted URL params to use by the external request 2FA handler."""

_EXTERNAL_REQUEST_DATA = os.environ.get('IBEAM_EXTERNAL_REQUEST_DATA')
"""JSON-formatted POST data to use by the external request 2FA handler."""

_EXTERNAL_REQUEST_HEADERS = os.environ.get('IBEAM_EXTERNAL_REQUEST_HEADERS')
"""JSON-formatted headers to use by the external request 2FA handler."""

Should I define each of the environment variables needed for IBEAM_TWO_FA_HANDLER=EXTERNAL_REQUEST
explicitly? I was assumimg the defaults for IBEAM_TWO_FA_HANDLER=EXTERNAL_REQUEST to be set.

Yeah, there are defaults (see the answer above), so you only need to set values that are needed.


Now, from the logs you've shared I conclude that IBeam hasn't even reached the 2FA stage. The authentication step does something unexpected. IBeam suggests increasing IBEAM_PAGE_LOAD_TIMEOUT, but I see that you've already set it to 500 hence there seem to be some issue other than long loading time that affects your instance in this case. This is what you'd need to focus on right now.

None of these triggers are present, causing the timeout:

        trigger, target = wait_and_identify_trigger(
            has_text(targets['SUCCESS']),
            is_visible(targets['TWO_FA']),
            is_visible(targets['TWO_FA_SELECT']),
            is_visible(targets['TWO_FA_NOTIFICATION']),
            is_visible(targets['ERROR']),
            is_clickable(targets['IBKEY_PROMO']),
        )

This is pretty unusual.

The corresponding 2FA targets are:

TWO_FA_EL_ID = os.environ.get('IBEAM_TWO_FA_EL_ID', 'ID@@twofactbase')
"""HTML element check for if Gateway will require 2FA code authentication."""

TWO_FA_NOTIFICATION_EL = os.environ.get('IBEAM_TWO_FA_NOTIFICATION_EL', 'CLASS_NAME@@login-step-notification')
"""HTML element check for if Gateway will require 2FA notification authentication."""

TWO_FA_INPUT_EL_ID = os.environ.get('IBEAM_TWO_FA_INPUT_EL_ID', 'ID@@chlginput')
"""HTML element to input 2FA code into"""
...
TWO_FA_SELECT_EL_ID = os.environ.get('IBEAM_TWO_FA_SELECT_EL_ID', 'ID@@sf_select')
"""HTML element check for if Gateway requires to select the 2FA method."""

My first guess is as follows:

  • Your instance receives a slightly different version of the loading page - this happens sometimes, no idea why.
  • When IBeam submits your credentials, the 2FA field appears on the page, but IBeam fails to detect it - because the HTML class names or IDs are different to what we expect in the 2FA targets.

My suggestion is to:
A. Confirm that the 2FA elements are rendered
B. What HTML class/IDs do they have

Hence:

  1. Can you add IBEAM_ERROR_SCREENSHOTS=True to your env vars? This will save screenshots upon exceptions and should let us see what's the page looking like when we get that error. This should help us understand if 2FA elements are rendered.
  2. Then/alternatively: can you try ssh'ing with a display and see if you're able to access the loading page (https://localhost:5000/sso/Login?forwardTo=22&RL=1&ip2loc=on)? Once you do that and type in your credentials manually, you'd need to search for the 2FA elements defined in the 2FA targets list I shared above, and see if they exist on the page after providing the credentials (see this tutorial on how to do it just in case). My guess is that they are simply called differently, and modifying one of the 2FA targets should fix this.

@xjcarter
Copy link
Author

xjcarter commented May 14, 2024

@Voyz
I'm digging deeper and may have run across some contextual assumptions on my end that may be causing this issue.

  1. Is the WebDriver IBeam is using- via Selenium- webdriver.Chrome? If so, I would assume google-chrome must be properly installed on the machine IBeam is operating on - and not just within the container?
  2. I wanted to get more clarity on the difference between the IBEAM_OAUTH_TIMEOUT and IBEAM_PAGE_LOAD_TIMEOUT environment variables. It appears that when I set IBEAM_OAUTH_TIMEOUT=180 I get the following response:
2024-05-14 22:35:57,351|I| Submitting the form
2024-05-14 22:38:57,676|E| Timeout reached when waiting for authentication. The website seems to not be loaded correctly. Consider increasing IBEAM_PAGE_LOAD_TIMEOUT.
Website URL: https://localhost:5000/sso/Login?forwardTo=22&RL=1&ip2loc=on

Exception:
  File "/srv/ibeam/ibeam_starter.py", line 167, in <module>
    success, shutdown, status = client.start_and_authenticate()
  File "/srv/ibeam/src/gateway_client.py", line 53, in start_and_authenticate
    success, shutdown, status = self.strategy_handler.try_authenticating(request_retries=request_retries)
  File "/srv/ibeam/src/handlers/strategy_handler.py", line 85, in try_authenticating
    return self._authentication_strategy_B(status, request_retries)
  File "/srv/ibeam/src/handlers/strategy_handler.py", line 140, in _authentication_strategy_B
    return self._log_in(status)
  File "/srv/ibeam/src/handlers/strategy_handler.py", line 151, in _log_in
    success, shutdown = self.login_handler.login()
  File "/srv/ibeam/src/handlers/login_handler.py", line 454, in login
    self.handle_timeout_exception(e, targets, driver, website_version, self.route_auth, self.base_url, self.outputs_dir)
  File "/srv/ibeam/src/handlers/login_handler.py", line 435, in login
    self.attempt(targets, wait_and_identify_trigger, driver)
  File "/srv/ibeam/src/handlers/login_handler.py", line 359, in attempt
    trigger, target = self.step_login(targets, wait_and_identify_trigger, driver, self.secrets_handler.account, self.secrets_handler.password, self.secrets_handler.key, self.presubmit_buffer)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/srv/ibeam/src/handlers/login_handler.py", line 180, in step_login
    trigger, target = wait_and_identify_trigger(
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/srv/ibeam/src/handlers/login_handler.py", line 67, in _wait_and_identify_trigger
    trigger = WebDriverWait(driver, timeout).until(any_of(*expected_conditions))
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/venv/lib/python3.11/site-packages/selenium/webdriver/support/wait.py", line 95, in until
    raise TimeoutException(message, screen, stacktrace)

  <class 'selenium.common.exceptions.TimeoutException'> Message:

2024-05-14 22:38:57,795|I| Saving screenshot to /srv/outputs/ibeam__0.5.1__2024-05-14_22-38-57__timeout-exception.png. Make sure to cover your credentials if you share it with others.
2024-05-14 22:38:57,868|I| Cleaning up the resources. Display: <pyvirtualdisplay.display.Display object at 0x7f746c8bb5d0> | Driver: <selenium.webdriver.chrome.webdriver.WebDriver (session="e93bf89f2cf4a2b9a759e9592caa1d87")>
2024-05-14 22:38:57,941|I| Logging in failed

Note that the timestamps between 'Submitting the form' and the 'Timeout' are approximately 180 secs,. I've changed the value of IBEAM_OAUTH_TIMEOUT in different runs and the log time 'gap' is consistent with whatever new value IBEAM_OAUTH_TIMEOUT is assigned. (i,e, if I set the variable to 60 - the Timeout error happens after 1 minute.).

It appears that the IBEAM_PAGE_LOAD_TIMEOUT is not being triggered - it is being triggered by IBEAM_OAUTH_TIMEOUT and the timeout is happening because the 2FA verification field is not visible.

  1. The log shows that the login page appears as follows:
2024-05-14 22:35:43,508|I| Gateway connection established
2024-05-14 22:35:43,509|D| POST https://localhost:5000/v1/api/tickle (unverified)
2024-05-14 22:35:43,643|I| NO SESSION Status(running=True, session=False, connected=False, authenticated=False, competing=False, collision=False, session_id=None, server_name=None, server_version=None, expires=None)
2024-05-14 22:35:43,643|I| Authentication strategy: "B"
2024-05-14 22:35:43,644|I| No active sessions, logging in...
2024-05-14 22:35:43,644|I| Loading auth webpage at https://localhost:5000/sso/Login?forwardTo=22&RL=1&ip2loc=on
2024-05-14 22:35:51,788|D| Targets: {'PASSWORD': Target(NAME@@password), 'SUBMIT': Target(CSS_SELECTOR@@.btn.btn-lg.btn-primary), 'SUCCESS': Target(TAG_NAME@@Client login succeeds), 'IBKEY_PROMO': Target(CLASS_NAME@@ibkey-promo-skip), 'TWO_FA': Target(ID@@twofactbase), 'TWO_FA_NOTIFICATION': Target(CLASS_NAME@@login-step-notification), 'TWO_FA_INPUT': Target(ID@@chlginput), 'TWO_FA_SELECT': Target(ID@@sf_select), 'USER_NAME': Target(NAME@@username), 'ERROR': Target(CSS_SELECTOR@@.xyz-errormessage)}
2024-05-14 22:35:51,840|I| Gateway auth webpage loaded
2024-05-14 22:35:51,841|I| Login attempt number 1
2024-05-14 22:35:52,043|D| target: Target(NAME@@username)

So if understand this correctly - the needed information for 2FA does exist. It must be that is just not showing up on the page.
Does that conclusion make sense?

  1. I've been wrestling with trying to ssh into my cloud machine and do login/authentication manually, but have been having a bugger of a time with Xserver issues and web-browser installations. I know this maybe be wishful thinking - but is there a way I can beat the IBeam process (via some wait env variable) and manually submit by simply going to https://{cloud_ip}:5000/sso/Login?forwardTo=22&RL=1&ip2loc=on while IBeam is its login loop?

@Voyz
Copy link
Owner

Voyz commented May 15, 2024

  1. Is the WebDriver IBeam is using- via Selenium- webdriver.Chrome? If so, I would assume google-chrome must be properly installed on the machine IBeam is operating on - and not just within the container?

If you're running it within a Docker container, there is no need to install the chromedriver. The image already has one.

Yes, the page loads - note log Gateway auth webpage loaded in your first report - hence IBEAM_PAGE_LOAD_TIMEOUT is irrelevant. The IBEAM_OAUTH_TIMEOUT is what defines how long we wait for the triggers to appear, which - in absence of those - causes an exception after that amount of seconds.

  1. So if understand this correctly - the needed information for 2FA does exist. It must be that is just not showing up on the page.
    Does that conclusion make sense?

Yes indeed, that is the case.

The Gateway stays open during all that process, hence you can do what you're suggesting at any point after seeing Gateway connection established. There is no need to stall IBeam itself. You can have multiple attempts to provide the credentials to the same Gateway at the same time. You can even re-login after having successfully logged in in the past. So yeah, you can totally do that if you can forward the display outside of your server.

@xjcarter
Copy link
Author

Thanks Voyz!

I still working on being able to access the login screen manually. I appears that I can access the server @ (https://localhost:5000/sso/Login?forwardTo=22&RL=1&ip2loc=on) but getting a reply of 'Access Denied'. once the gateway has be established. Are you familiar with what mechanism drives this response?

Additionally I added the screenshot of the login screen that is dumped by setting the IBEAM_ERROR_SCREENSHOTS=True.
It looks like the webpage is rendered correctly - so I'm assuming the IBEAM_TWO_FA_INPUT_ID is labeled something other than ID@chlginput.

Am I moving in the right direction?
Thanks for your help.

ibeam__0 5 3__2024-05-15_16-24-19__timeout-exception

@Voyz
Copy link
Owner

Voyz commented May 17, 2024

Yes, Access Denied is a common error that can be easily solved most of the time. See here: https://github.com/Voyz/ibeam/wiki/Troubleshooting#access-denied. It involves modifying the conf.yaml and adding your IP address to it.

And great job providing and analysing the screenshot. Indeed, it seems like the elements are rendered fine, but just need a different ID or class match pattern 👍

@xjcarter
Copy link
Author

@Voyz @lazerlabs

Gentlemen -
I'm slowly but surely moving toward getting to the bottom of this 2FA Authentication issue...
I looped lazerlabs in because it looks to me that he is having the same problem.

Just to recap what Voy previously wrote:

**My first guess is as follows:

Your instance receives a slightly different version of the loading page - this happens sometimes, no idea why.
When IBeam submits your credentials, the 2FA field appears on the page, but IBeam fails to detect it - because the HTML class names or IDs are different to what we expect in the 2FA targets.
My suggestion is to:
A. Confirm that the 2FA elements are rendered
B. What HTML class/IDs do they have**

And per his guidance:
A. I have verified that the needed elements are rendered, and
B. Moved to verify that the needed elements associated (visible or not visible) are associated with the page.

That being said - here is what I have discovered:

  1. Specifically searching for the TWO_FA_INPUT_EL_ID = 'ID@@chlginput' on my webpage - this element does not exist.
    (On a related note, I had no luck finding the other the elements associated with the Targets of other 2FA variables, i.e. :
    'TWO_FA': Target(ID@@twofactbase),
    'TWO_FA_NOTIFICATION': Target(CLASS_NAME@@login-step-notification),
    'TWO_FA_SELECT': Target(ID@@sf_select)
    But maybe we wait to cross that bridge when we come to it...)

  2. I inspected the IBRK authentication webpage that is presented, and found that for the 'Security Code' entry field,
    it is associated with an elementid = 'xyz-field-silver-response'and NOT 'chlginput'.

Chrome_Auth_Screen

As a result I tried to override IBEAM_TWO_FA_INPUT_EL_ID in my environment variable file as follows:

IBEAM_TWO_FA_INPUT_EL_ID='ID@@xyz-field-silver-response'

Upon retrying to re-run the IBeam server - I get the following errors:

2024-05-20 16:08:13,667|I| ############ Starting IBeam version 0.5.3 ############
2024-05-20 16:08:13,668|I| Custom conf.yaml found and will be used by the Gateway
2024-05-20 16:08:13,671|I| Secrets source: env
Traceback (most recent call last):
  File "/srv/ibeam/ibeam_starter.py", line 93, in <module>
    targets = create_targets(cnf)
              ^^^^^^^^^^^^^^^^^^^
  File "/srv/ibeam/src/login/targets.py", line 91, in create_targets
    targets['TWO_FA_INPUT'] = Target(cnf.TWO_FA_INPUT_EL_ID)
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/srv/ibeam/src/login/targets.py", line 41, in __init__
    raise RuntimeError(f'Unknown target type: {type}@@{identifier}')
RuntimeError: Unknown target type: 'ID@@xyz-field-silver-response'

What would you recommend as next steps?
I thought I was formatting the new web element environment variable correctly - but no success?!

Thanks!

@Voyz
Copy link
Owner

Voyz commented May 21, 2024

Specifically searching for the TWO_FA_INPUT_EL_ID = 'ID@@chlginput' on my webpage - this element does not exist.
I inspected the IBRK authentication webpage that is presented, and found that for the 'Security Code' entry field,
it is associated with an elementid = 'xyz-field-silver-response'and NOT 'chlginput'

Great job debugging this. Therefore this confirms my earlier guesses. That indeed should be the root cause of the issue you're facing. You'd need to change the env vars as you deduce.

IBEAM_TWO_FA_INPUT_EL_ID='ID@@xyz-field-silver-response'

Can I suggest that you set this to:

IBEAM_TWO_FA_INPUT_EL_ID=ID@@xyz-field-silver-response

Without the single quotes?


Additional context:

I have a feeling this causes the issue you're encountering now. We're doing a split at @@ and I think your variables may be set as:

self.type = "'ID"
self.identifier = "xyz-field-silver-response'"

Which doesn't match the target-identification pattern due to these single quotes.

This seems to be confirmed when I look at the log you report and notice that the variable logged contains these quotes:

RuntimeError: Unknown target type: 'ID@@xyz-field-silver-response'

While the exception raising code itself doesn't introduce these quotes:

raise RuntimeError(f'Unknown target type: {type}@@{identifier}')

Hence, I assume the single quotes are indeed passed over from the env var and should be removed. Let me know if that works 👍

@xjcarter
Copy link
Author

@Voyz @lazerlabs

Hi Voy -
Although I can replace my TWO_FA_INPUT with the new value showing up on my login screen via setting
IBEAM_TWO_FA_INPUT_EL_ID=ID@@xyz-field-silver-response, I quickly realized that this substitution does
not solve the timeout problem given by:

File "/srv/ibeam/src/handlers/login_handler.py", line 67, in _wait_and_identify_trigger
    trigger = WebDriverWait(driver, timeout).until(any_of(*expected_conditions))
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

We are timing out because TWO_FA, TWO_FA_SELECT, and TWO_FA_NOTIFICATION do not present themselves - so
even though we have a new entry field for the 2FA those are not the elements step_login() is looking for.

In step_login:
...

	  
   trigger, target = wait_and_identify_trigger(
            has_text(targets['SUCCESS']),
            is_visible(targets['TWO_FA']),
            is_visible(targets['TWO_FA_SELECT']),
            is_visible(targets['TWO_FA_NOTIFICATION']),
            is_visible(targets['ERROR']),
            is_clickable(targets['IBKEY_PROMO']),
        )

Given this secondary auth login screen presented here:
ibeam__0 5 3__2024-05-15_16-24-19__timeout-exception

What elements do theTWO_FA, TWO_FA_SELECT, TWO_FA_NOTIFICATIONtargets respresent? I cannot find anything
on this screen outside the presentation of the new TWO_FA_INPUT target...
(Also I searched for the values of the above TWO_FA* targets inside my webpage with no luck).

Can you provide an example of what your auth login page looks like - and point out the TWO_FA* elements on the page?
Thanks!

@Voyz
Copy link
Owner

Voyz commented May 21, 2024

Sorry, thanks for pointing this out and apologies for misleading you here. The variable that IBeam uses to discover the 2FA prompt is IBEAM_TWO_FA_EL_ID not IBEAM_TWO_FA_INPUT_EL_ID. That being said, that second one also needs to be modified to xyz-field-silver-response, as it is where we will type in the 2FA code. Hence:

IBEAM_TWO_FA_EL_ID=ID@@xyz-field-silver-response
IBEAM_TWO_FA_INPUT_EL_ID=ID@@xyz-field-silver-response

I think that should work, once again sorry for not spotting that sooner. Let me know 👍

@xjcarter
Copy link
Author

SUCCESS!
A thousand thank yous!

@xjcarter xjcarter reopened this May 22, 2024
@xjcarter
Copy link
Author

xjcarter commented May 22, 2024 via email

@Voyz
Copy link
Owner

Voyz commented May 22, 2024

Superb news @xjcarter 🥳🥳 Glad to see we've figured it out. Good job! 👏

Sorry, as for your question, it seems to have been cut off, all I see is:

Just out of curiosity - can you give quick description of how that mech

@xjcarter
Copy link
Author

Just out of curiosity -
I was reading through the login_handler.py code and was trying to get a sense of the mechanism of
how the code interacts with the login page. Can your provide a quick summary - and recommend any sources
where I can learn more?
Thanks!

@Voyz
Copy link
Owner

Voyz commented May 23, 2024

Ahh understood. IBeam interacts with the webpage using Selenium. If you're not familiar with it - it is a library used for browser automation, ie. being able to programmatically interact with a browser. In fact, we can even interact with it without displaying anything, just "pretending there is a display". Sort of like solving a Rubics cube blind folded. And that's what IBeam does: it loads the login page without a display, finds the right input fields, types in our credentials and hits the 'Login' button. The complexity comes in due to the fact that there are a bunch of versions of that webpage that IBKR servers - as you've unfortunately noticed in this Issue - and there are various control flows - eg. there are 4 ways to complete 2FA. Hence, we need to recognise all of these and act accordingly. That's what login_handler.py is doing, along with a bunch of functionalities that improve the experience, such as automatic shutdown on critical errors, error handling, screenshots, 2FA acquisition, etc. Hope that helps 👍

@xjcarter
Copy link
Author

xjcarter commented May 23, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants