def verify(username=None, password=None, expiresIn=86400, scope='internal', by_sms=True, store_session=True, auth_type="challenge", code=None, device_token=None, challenge_id=None):
    """This function will effectivly log the user into robinhood by getting an
    authentication token and saving it to the session header. By default, it
    will store the authentication token in a pickle file and load that value
    on subsequent logins.

    :param username: The username for your robinhood account, usually your email.
        Not required if credentials are already cached and valid.
    :type username: Optional[str]
    :param password: The password for your robinhood account. Not required if
        credentials are already cached and valid.
    :type password: Optional[str]
    :param expiresIn: The time until your login session expires. This is in seconds.
    :type expiresIn: Optional[int]
    :param scope: Specifies the scope of the authentication.
    :type scope: Optional[str]
    :param by_sms: Specifies whether to send an email(False) or an sms(True)
    :type by_sms: Optional[boolean]
    :param store_session: Specifies whether to save the log in authorization
        for future log ins.
    :type store_session: Optional[boolean]
    :param auth_type: Specifies whetner authorization is through mfa code or sms challenge
    :type auth_type: Optional[str]
    :param code: Mfa code or sms challenge code
    :type code: Required[str]
    :param device_token: Previously used device token
    :type device_token: Required[str]
    :param challenge_id: Challenge id
    :type challenge_id: Required[str]
    :returns:  A dictionary with log in information. The 'access_token' keyword contains the access token, and the 'detail' keyword \
    contains information on whether the access token was generated or loaded from pickle file.

    """
    home_dir = os.path.expanduser("~")
    data_dir = os.path.join(home_dir, ".tokens")
    if not os.path.exists(data_dir):
        os.makedirs(data_dir)
    creds_file = "robinhood.pickle"
    pickle_path = os.path.join(data_dir, creds_file)
    # Challenge type is used if not logging in with two-factor authentication.
    if by_sms:
        challenge_type = "sms"
    else:
        challenge_type = "email"

    url = urls.login_url()
    payload = {
        'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
        'expires_in': expiresIn,
        'grant_type': 'password',
        'password': password,
        'scope': scope,
        'username': username,
        'challenge_type': challenge_type,
        'device_token': device_token
    }

    if auth_type == "mfa":
        mfa_token = code
        payload['mfa_code'] = mfa_token
        res = helper.request_post(url, payload, jsonify_data=False)
        if res.status_code != 200:
            return {'Error': "Incorrect code"}
        data = res.json()
    elif auth_type == "challenge":
        sms_code = code
        print("challenge:\n", challenge_id, code)
        res = respond_to_challenge(challenge_id, sms_code)
        if 'challenge' in res and res['challenge']['remaining_attempts'] > 0:
            print("retry\n")
            return {'error': "Incorrect code", 'attempts_left': res['challenge']['remaining_attempts']}
        else:
            print('res', res)
        helper.update_session(
            'X-ROBINHOOD-CHALLENGE-RESPONSE-ID', challenge_id)
        print('payload:\n', payload)
        data = helper.request_post(url, payload)
        print('data:\n', data)
    # Update Session data with authorization or raise exception with the information present in data.
    if 'access_token' in data:
        token = '{0} {1}'.format(data['token_type'], data['access_token'])
        helper.update_session('Authorization', token)
        helper.set_login_state(True)
        data['detail'] = "logged in with brand new authentication code."
        print('store', store_session)
        if store_session:
            print('yeboi')
            with open(pickle_path, 'wb') as f:
                pickle.dump({'token_type': data['token_type'],
                                'access_token': data['access_token'],
                                'refresh_token': data['refresh_token'],
                                'device_token': device_token}, f)
    else:
        return {"error": data["detail"]}

    return(data)
Beispiel #2
0
def login(username=None,
          password=None,
          expiresIn=86400,
          scope='internal',
          by_sms=True,
          store_session=True,
          mfa_code=None):
    """This function will effectively log the user into robinhood by getting an
    authentication token and saving it to the session header. By default, it
    will store the authentication token in a pickle file and load that value
    on subsequent logins.

    :param username: The username for your robinhood account, usually your email.
        Not required if credentials are already cached and valid.
    :type username: Optional[str]
    :param password: The password for your robinhood account. Not required if
        credentials are already cached and valid.
    :type password: Optional[str]
    :param expiresIn: The time until your login session expires. This is in seconds.
    :type expiresIn: Optional[int]
    :param scope: Specifies the scope of the authentication.
    :type scope: Optional[str]
    :param by_sms: Specifies whether to send an email(False) or an sms(True)
    :type by_sms: Optional[boolean]
    :param store_session: Specifies whether to save the log in authorization
        for future log ins.
    :type store_session: Optional[boolean]
    :param mfa_code: MFA token if enabled.
    :type mfa_code: Optional[str]
    :returns:  A dictionary with log in information. The 'access_token' keyword contains the access token, and the 'detail' keyword \
    contains information on whether the access token was generated or loaded from pickle file.

    """
    device_token = generate_device_token()
    home_dir = os.path.expanduser("~")
    data_dir = os.path.join(home_dir, ".tokens")
    if not os.path.exists(data_dir):
        os.makedirs(data_dir)
    creds_file = "robinhood.pickle"
    pickle_path = os.path.join(data_dir, creds_file)
    # Challenge type is used if not logging in with two-factor authentication.
    if by_sms:
        challenge_type = "sms"
    else:
        challenge_type = "email"

    url = urls.login_url()
    payload = {
        'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
        'expires_in': expiresIn,
        'grant_type': 'password',
        'password': password,
        'scope': scope,
        'username': username,
        'challenge_type': challenge_type,
        'device_token': device_token
    }

    if mfa_code:
        payload['mfa_code'] = mfa_code

        # If authentication has been stored in pickle file then load it. Stops login server from being pinged so much.
        #if os.path.isfile(pickle_path):
        # If store_session has been set to false then delete the pickle file, otherwise try to load it.
        # Loading pickle file will fail if the access_token has expired.
        if store_session:
            try:
                with open(pickle_path, 'rb') as f:
                    pickle_data = pickle.load(f)
                    access_token = pickle_data['access_token']
                    token_type = pickle_data['token_type']
                    refresh_token = pickle_data['refresh_token']
                    # Set device_token to be the original device token when first logged in.
                    pickle_device_token = pickle_data['device_token']
                    payload['device_token'] = pickle_device_token
                    # Set login status to True in order to try and get account info.
                    helper.set_login_state(True)
                    helper.update_session(
                        'Authorization',
                        '{0} {1}'.format(token_type, access_token))
                    # Try to load account profile to check that authorization token is still valid.
                    res = helper.request_get(urls.portfolio_profile(),
                                             'regular',
                                             payload,
                                             jsonify_data=False)
                    # Raises exception is response code is not 200.
                    res.raise_for_status()
                    return ({
                        'access_token':
                        access_token,
                        'token_type':
                        token_type,
                        'expires_in':
                        expiresIn,
                        'scope':
                        scope,
                        'detail':
                        'logged in using authentication in {0}'.format(
                            creds_file),
                        'backup_code':
                        None,
                        'refresh_token':
                        refresh_token
                    })
            except:
                print(
                    "ERROR: There was an issue loading pickle file. Authentication may be expired - logging in normally.",
                    file=helper.get_output())
                helper.set_login_state(False)
                helper.update_session('Authorization', None)
        else:
            os.remove(pickle_path)

    # Try to log in normally.
    if not username:
        username = input("Robinhood username: "******"Robinhood password: "******"Please type in the MFA code: ")
            payload['mfa_code'] = mfa_token
            res = helper.request_post(url, payload, jsonify_data=False)
            while (res.status_code != 200):
                mfa_token = input(
                    "That MFA code was not correct. Please type in another MFA code: "
                )
                payload['mfa_code'] = mfa_token
                res = helper.request_post(url, payload, jsonify_data=False)
            data = res.json()
        elif 'challenge' in data:
            challenge_id = data['challenge']['id']
            sms_code = input('Enter Robinhood code for validation: ')
            res = respond_to_challenge(challenge_id, sms_code)
            while 'challenge' in res and res['challenge'][
                    'remaining_attempts'] > 0:
                sms_code = input(
                    'That code was not correct. {0} tries remaining. Please type in another code: '
                    .format(res['challenge']['remaining_attempts']))
                res = respond_to_challenge(challenge_id, sms_code)
            helper.update_session('X-ROBINHOOD-CHALLENGE-RESPONSE-ID',
                                  challenge_id)
            data = helper.request_post(url, payload)
        # Update Session data with authorization or raise exception with the information present in data.
        if 'access_token' in data:
            token = '{0} {1}'.format(data['token_type'], data['access_token'])
            helper.update_session('Authorization', token)
            helper.set_login_state(True)
            data['detail'] = "logged in with brand new authentication code."
            if store_session:
                with open(pickle_path, 'wb') as f:
                    pickle.dump(
                        {
                            'token_type': data['token_type'],
                            'access_token': data['access_token'],
                            'refresh_token': data['refresh_token'],
                            'device_token': device_token
                        }, f)
        else:
            raise Exception(data['detail'])
    else:
        raise Exception(
            'Error: Trouble connecting to robinhood API. Check internet connection.'
        )
    return (data)
def lambda_handler(event, context):
    """
    Lambda function to generate and cache full Robinhood login details using
    the 2FA code.
    
    This is a stripped down version of the robin_stocks.authentication module.
    """
    try:
        body = json.loads(event['body'])
        code = body['code']
        challenge_id = body['challenge']
        username = body['username']
        password = body['password']
        sendviasms = body['sms']
    except KeyError:
        return {
            'statusCode':
            400,
            'body':
            json.dumps(
                {
                    'rh_login_successful': False,
                    'message': 'No parameters specified or missing parameters.'
                },
                cls=LambdaMessageEncoder),
            'headers': {
                'Content-Type': 'application/json'
            }
        }

    ddb_client = boto3.resource('dynamodb')
    table = ddb_client.Table(os.environ['CREDENTIALS_TABLE'])

    try:
        device_token = table.get_item(
            Key={'credsPlatform': 'robinhood'})['Item']['deviceToken']
    except Exception:
        _LOGGER.error('Unable to grab Robinhood device token.')
        return {
            'statusCode':
            500,
            'body':
            json.dumps(
                {
                    'rh_login_successful': False,
                    'message': 'Something went wrong server-side.'
                },
                cls=LambdaMessageEncoder),
            'headers': {
                'Content-Type': 'application/json'
            }
        }

    url = urls.login_url()

    if sendviasms:
        challenge_type = "sms"
    else:
        challenge_type = "email"

    #Client ID appears to be a hardcoded magic number from client requests?
    #Something to watch out for. Could be related to User-Agent & app version.
    payload = {
        'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
        'expires_in': 86400,
        'grant_type': 'password',
        'password': password,
        'scope': 'internal',
        'username': username,
        'challenge_type': challenge_type,
        'device_token': device_token
    }

    #Send back the 2FA code to get a challenge ID.
    res = respond_to_challenge(challenge_id, code)

    #For code failures.
    if 'challenge' in res:
        return {
            'statusCode':
            401,
            'body':
            json.dumps(
                {
                    'rh_login_successful': False,
                    'message': 'Failed to login.'
                },
                cls=LambdaMessageEncoder),
            'headers': {
                'Content-Type': 'application/json'
            }
        }

    #Request to receive the JWT.
    helper.update_session('X-ROBINHOOD-CHALLENGE-RESPONSE-ID', challenge_id)
    data = helper.request_post(url, payload)
    one_day_expiry = datetime.now(timezone.utc) + timedelta(seconds=86400)

    if 'access_token' in data:
        try:
            table.put_item(
                Item={
                    'credsPlatform': 'robinhood',
                    'deviceToken': device_token,
                    'tokenType': data['token_type'],
                    'accessToken': data['access_token'],
                    'refreshToken': data['refresh_token'],
                    'expiry': one_day_expiry.isoformat()
                })
        except Exception as e:
            _LOGGER.error('Unable stick Robinhood credentials into DDB.')
            return {
                'statusCode':
                500,
                'body':
                json.dumps(
                    {
                        'rh_login_successful': False,
                        'message': 'Something went wrong server-side.'
                    },
                    cls=LambdaMessageEncoder),
                'headers': {
                    'Content-Type': 'application/json'
                }
            }

    return {
        'statusCode':
        200,
        'body':
        json.dumps(
            {
                'rh_login_successful': True,
                'message': 'Successfully authenticated. Logging in.'
            },
            cls=LambdaMessageEncoder),
        'headers': {
            'Content-Type': 'application/json'
        }
    }