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

if challenge_respons['status']; key error 'status' #537

Open
pielshawn opened this issue Jan 18, 2025 · 75 comments
Open

if challenge_respons['status']; key error 'status' #537

pielshawn opened this issue Jan 18, 2025 · 75 comments

Comments

@pielshawn
Copy link

Login is not operating as before. The RH app prompts me to confirm that I'm attempting to log in but the program does not authenticate.

@zabidin901
Copy link

@clarin-ebtio800090 is this meant to be a working solution? _validate_sherrif_id is not defined and the time module is not imported in your solution.

Thank you for digging into the issue however.

@alex-l-zhou
Copy link

@clarin-ebtio800090 is this meant to be a working solution? _validate_sherrif_id is not defined and the time module is not imported in your solution.

Thank you for digging into the issue however.

i tried it and it didn't work

@ravi-bharadwaj
Copy link
Contributor

Looks like only sms codes are used for authentication even though mfa is setup as authentication method. Not sure if its a new feature or bugged-release on RH

@alex-l-zhou
Copy link

Looks like only sms codes are used for authentication even though mfa is setup as authentication method. Not sure if its a new feature or bugged-release on RH

any idea how to handle this?

@HMSS013
Copy link

HMSS013 commented Jan 18, 2025

It seems to be that Robinhood insists on using the Trusted Device authentication method even after selecting and setting up the Authenticator App method and using the QR affiliated code.

@Kr1msonReaper
Copy link

Does anyone know if it's the SMS code in addition to the authentication app? Or is it now just SMS?

@HMSS013
Copy link

HMSS013 commented Jan 18, 2025

Does anyone know if it's the SMS code in addition to the authentication app? Or is it now just SMS?

from what i can tell, it's both.

when Authenticator App is selected in RobinHood it will set up with a QR & Code, but at login RobinHood will attempt to use Trusted Device instead.

and with SMS Authentication set Stocks no longer prompts for input of the SMS code.

@cpasean
Copy link

cpasean commented Jan 18, 2025

Same issue here. Would anyone be able to help me out?
//
print(challenge_response)
=> {'detail': 'Authentication credentials were not provided.'}

@ravi-bharadwaj
Copy link
Contributor

I have been trying to check with robinhood support as they seem to have abandoned multi factor authentication option and using only sms, photo, gov-id, bank as valid form of authentication methods

@Adelantado
Copy link

I am experiencing issues too. Bot will not log in and at the same time App will ask me to verify new device, which I do, but it dose not seem to go thru.

@ravi-bharadwaj
Copy link
Contributor

ravi-bharadwaj commented Jan 19, 2025

Do we any way of using the pickle file from browser to be used instead of pickle file we are generating from app. If there is a way, can you please share the steps.

@nat2k5us
Copy link

nat2k5us commented Jan 19, 2025

workaround:
I replaced the pickle access_token and was able to get it to work - the refresh token is valid for 90 days (typically).

Looks like this only works for 30 mins (sorry)

def replace_pickle(code):
    # Step 1: Create a Backup
    shutil.copy(original_file_path, backup_file_path)
    print(f"Backup created at: {backup_file_path}")

    # Step 2: Load the Original Pickle File
    with open(original_file_path, 'rb') as f:
        data = pickle.load(f)

    # Step 3: Replace the Bearer Token
    new_bearer_token = code
    if isinstance(data, dict) and "access_token" in data:
        data["access_token"] = new_bearer_token
        print("Bearer token replaced.")
    else:
        print("Bearer token not found in the pickle file.")

    # Step 4: Save the Updated Pickle File
    with open(original_file_path, 'wb') as f:
        pickle.dump(data, f)
        print(f"Updated pickle file saved at: {original_file_path}")

@Kr1msonReaper
Copy link

I am not the best at usely solely network requests to login, so I'm going to automate the login via selenium, extract the token, and then just give it to the robinhood module.

@Adelantado
Copy link

So what has changed from my perspective :

I can log in via browser, when I do now I get a Robinhood app notification on my phone to verify it's me and a prompt to allow a new device, which I do and I am able to log in to the account via browser. Until now this did not exist and verification was done by entering / passing the 2FA code generated via totp = pyotp.TOTP(log_2FA).now()

Now the Robinhood app notification on my phone happens the same when trying to log in via the script, however, after verifying new device in the app, I get an exception error and script just terminates it self ( have no procedure in place to handle that exception at this time ) . I still generate the 2FA code, the .token folder gets created but no pickle file is written, and the phone app validation fails and program just craps out on the exception created when trying to log in: login = r.login(log_User, log_Pass, mfa_code=totp)

Seems Authentication.py will need to be modified again, whish I was smart enough to figure that one. Sorry Folks and thanks.

@bhyman67
Copy link

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!

bhyman67/Mods-to-robin-stocks-Authentication@02e5491

You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

@cpasean
Copy link

cpasean commented Jan 19, 2025

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!

bhyman67/Mods-to-robin-stocks-Authentication@02e5491

You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

This works perfectly! You truly saved my day, Mr. Hyman. Many thanks and much respect!

@HMSS013
Copy link

HMSS013 commented Jan 19, 2025

brilliant stuff @bhyman67, where's the tip button on this thing?

now the age old question:

does anyone know how long these pickles last?

...and how can we make them last longer.

@Kr1msonReaper
Copy link

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!

bhyman67/Mods-to-robin-stocks-Authentication@02e5491

You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

I appreciate you! Wasn't looking forward to trying to figure out how to fix this. Now I just need to find a service that can grab the sms code for me.

@jmfernandes
Copy link
Owner

@bhyman67 If you have time to create a PR to fix this issue I would greatly appreciate it! I just merged another PR, so you should update your branch.

@bhyman67
Copy link

bhyman67 commented Jan 19, 2025

@bhyman67 If you have time to create a PR to fix this issue I would greatly appreciate it! I just merged another PR, so you should update your branch.

Sounds good, @jmfernandes! I'll try to get to that sometime soon.

@Adelantado
Copy link

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!

bhyman67/Mods-to-robin-stocks-Authentication@02e5491

You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

Thanks so much, I got the Bot to log in.
On a personal note: Now I just wish the new phone verification step could be avoid it / automated as it really screws the concept of having a 24/7/365 BOT running w no human interaction.

@p-o-f
Copy link

p-o-f commented Jan 20, 2025

Is the new phone verification step intentional? That seems to defeat the point of having 2FA if they're going to just ask for a code with SMS anyways...

@cottuzy
Copy link

cottuzy commented Jan 20, 2025

What is the login flow now with these changes if we were to use external 2FA app?

  1. enter user/pass
  2. enter 2FA otp
  3. verify on phone
  4. we get access token on robin?

@realhitta
Copy link

is it possible to login by script without needing to manually approve the login request in the Robinhood app?

@doormat-1010
Copy link

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!

bhyman67/Mods-to-robin-stocks-Authentication@02e5491

You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

I was able to successfully generate a fresh pickle file using this method - thank you for putting this solution together. However, I deleted the pickle file and reran to refresh the pickle, and app on phone is no longer generating the verification. Two minute window expires and no pickle file is generated. Anyone else experiencing this? I tried reverting to original authentication.py to see if it would work after having approved via app verification, but no success. I'm wondering if the revised authentication.py needs additional edits for handling cases where Robinhood account has already logged approval via the new verification process?

@bhyman67
Copy link

bhyman67 commented Jan 21, 2025

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!
bhyman67/Mods-to-robin-stocks-Authentication@02e5491
You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

I was able to successfully generate a fresh pickle file using this method - thank you for putting this solution together. However, I deleted the pickle file and reran to refresh the pickle, and app on phone is no longer generating the verification. Two minute window expires and no pickle file is generated. Anyone else experiencing this? I tried reverting to original authentication.py to see if it would work after having approved via app verification, but no success. I'm wondering if the revised authentication.py needs additional edits for handling cases where Robinhood account has already logged approval via the new verification process?

Yeah I ran into the same problem... It worked initially, but then that same problem happened to me. Which is why I paused on going any further on the pull request.

I think you're right that this needs further edits and testing... This doesn't seem to be a full working solution yet...

#538 (comment)

@Kr1msonReaper
Copy link

For anyone who wants full automation, you can use something like selenium to manually plug in your username, password and possibly Auth app code and then plug in the sms code by linking robinhood to a Google voice number, having it forward the message to your Google email and then by using the Gmail app to read the code programmatically and plug it into the page.

Convoluted, but that's the free option. Otherwise, you could use a Twilio number.

@ssmanji89
Copy link

Yep, indeed. Authentication.py needs to be modified to handle the new app notification method. I made the following modifications to the auth module and got it to work!

bhyman67/Mods-to-robin-stocks-Authentication@02e5491

You should have 2 minutes to approve the login request in the Robinhood app from the moment that you call the login routine.

I was able to successfully generate a fresh pickle file using this method - thank you for putting this solution together. However, I deleted the pickle file and reran to refresh the pickle, and app on phone is no longer generating the verification. Two minute window expires and no pickle file is generated. Anyone else experiencing this? I tried reverting to original authentication.py to see if it would work after having approved via app verification, but no success. I'm wondering if the revised authentication.py needs additional edits for handling cases where Robinhood account has already logged approval via the new verification process?

Try a different IP?

@Adelantado
Copy link

Adelantado commented Jan 22, 2025

@ Kr1msonReaper; > For anyone who wants full automation, you can use something like selenium to manually plug in your username, password and possibly Auth app code and then plug in the sms code by linking robinhood to a Google voice number, having it forward the message to your Google email and then by using the Gmail app to read the code programmatically and plug it into the page.

Convoluted, but that's the free option. Otherwise, you could use a Twilio number.

Are you able to have your script log in without having to manually verify new device manually in the app ?
I am having trouble understanding how this may work as I do not receive any SMS code on my cell; I do get a notification to verify new device, then I log in in the app and finally validate prompt; there is no SMS code generated and no place to input anywhere.
Anyway, if you got this to work, would you care to share the code ? Thanks.

@Kr1msonReaper
Copy link

@ Kr1msonReaper; > For anyone who wants full automation, you can use something like selenium to manually plug in your username, password and possibly Auth app code and then plug in the sms code by linking robinhood to a Google voice number, having it forward the message to your Google email and then by using the Gmail app to read the code programmatically and plug it into the page.

Convoluted, but that's the free option. Otherwise, you could use a Twilio number.

Are you able to have your script log in without having to manually verify new device manually in the app ? I am having trouble understanding how this may work as I do not receive any SMS code on my cell; I do get a notification to verify new device, then I log in in the app and finally validate prompt; there is no SMS code generated and no place to input anywhere. Anyway, if you got this to work, would you care to share the code ? Thanks.

I believe the SMS option is only available if you don't have the app. I haven't implemented the automation for myself because a single approval seems to be good for a while. Might get to it when I have the time and motivation.

@bhyman67
Copy link

bhyman67 commented Jan 27, 2025

Thanks bhyman67 this really help us newbies to learn the code and hopefully contribute in the future! This is a great library and project!

Sure thing!! Sure wish I could try bringing this thing to the finish line... Still been periodically checking how my 2fa process goes and if it ever goes back to using the device approval method (like it's supposed to do according to my settings) rather than the SMS method...

@mike-labadessa
Copy link

Sorry if I missed this. But I keep getting the timeout error. Do we have to turnoff the Authenticator App authentication and use sms in robinhood? It seems we can only choose one Robinhood authentication option, but authentication.py needs by_sms also. Maybe by_sms and mfa_code variables need to be merged to a “auth_type” variable. Because why does challenge_type need to exist in the payload if the app user uses mfa?

I think with the rh app update, when they added the sms option, it made it a requirement instead of being an option that was intended. could be wrong but shouldn’t the code check to see if mfa_code is being used before passing challenge_type with the payload?

@gbtota
Copy link

gbtota commented Jan 28, 2025

Yeah i'm not getting the app notification either. What is everyone who gets the app noti their 2 factor authentication set on? I have mine set to "Device authentication" ?

Are you getting an SMS notification, instead? That's the case with me.

My two-factor auth setting is set to device approvals. But Robinhood is using the SMS 2fa method instead...

I just recently modified the Authentication module again to account for this...

bhyman67/Mods-to-robin-stocks-Authentication@6f29cc5

I think this should still work if the device approval method is used in the 2fa process... But I just can't confirm that since I haven't been able to get Robinhood to use the device approval 2fa method for me. So please double check this and use at own risk...

I'm still not going any further on that pull request... This is getting messy and Robinhood's new authentication methods are seeming to be just too finicky. This is a tough one...

This fix works but you must delete the old pickle file first. otherwise its getting an expired challenge response.

@bhyman67
Copy link

bhyman67 commented Jan 28, 2025

Sorry if I missed this. But I keep getting the timeout error. Do we have to turnoff the Authenticator App authentication and use sms in robinhood? It seems we can only choose one Robinhood authentication option, but authentication.py needs by_sms also. Maybe by_sms and mfa_code variables need to be merged to a “auth_type” variable. Because why does challenge_type need to exist in the payload if the app user uses mfa?

I think with the rh app update, when they added the sms option, it made it a requirement instead of being an option that was intended. could be wrong but shouldn’t the code check to see if mfa_code is being used before passing challenge_type with the payload?

Hi Mike. I'm not quite sure... Robinhood's 2fa process is still a little funky.

Feel free to share some code samples demonstrating your ideas and I'd be happy to take a look!

@OnishiKenshin
Copy link

OnishiKenshin commented Jan 28, 2025

I was able to automate the SMS verification. I don't know how to use github, so apologies for how scuffed this post will be, but I was able to solve it in under an hour with the help of ChatGPT. My solution also feels pretty situational with how my automated trading works, but maybe by writing how I did it here, I can help others find a personal fix for theirs as well.

First, my trading algorithm runs off of my Macbook, so by syncing iMessage to also receive texts, I was now getting the verification codes for Robinhood on my Macbook's iMessage as well as my phone.

Then, I used ChatGPT to make me a python script that would scrape the string of the entire most recent text message from the number Robinhood uses to send verification codes, and return the last 7 characters minus the period, so it would just be the six digit verification code. The code looks like this:

import sqlite3
import os

# Path to the chat database file
db_path = os.path.expanduser('~/Library/Messages/chat.db')

# Function to get the most recent message from a specific phone number
def get_recent_verification_code(phone_number):
    # Connect to the SQLite database
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    # Query to get the most recent message from the specific phone number
    query = """
    SELECT message.text, message.date
    FROM message
    JOIN handle ON message.handle_id = handle.ROWID
    WHERE handle.id = ?
    ORDER BY message.date DESC
    LIMIT 1
    """
    
    cursor.execute(query, (phone_number,))
    result = cursor.fetchone()

    if result:
        message_text = result[0]
        # Extract the last 7 characters and remove the last one (the period)
        verification_code = message_text[-7:-1]
        return verification_code
    else:
        return None

if __name__ == "__main__":
    # Example usage
    phone_number = '00000'  # Replace with the phone number Robinhood uses to send you codes
    verification_code = get_recent_verification_code(phone_number)

    if verification_code:
        print(verification_code)
    else:
        print(f"No verification code found for {phone_number}")

Finally, I saved that script (titled getLastText) to the same folder that authentication.py exists in. I edited the authentication.py file to import the function that scrapes the verification code, and changed the authentication.py code from this:

# Need a conditional here for if the type is sms
        if res["context"]["sheriff_challenge"]["type"] == "sms":
            print("SMS code sent to your phone")
            challenge_url = f"https://api.robinhood.com/challenge/{challenge_id}/respond/"
            # prompt user for sms code
            sms_code = input("Please enter the SMS code: ")
            challenge_response = request_post(url=challenge_url, payload={"response":sms_code})
            request_post(inquiries_url, payload={"sequence":0,"user_input":{"status":"continue"}},json=True)
            return

To this:

        from getLastText import get_recent_verification_code #pasted at the start where the imports are

        # Need a conditional here for if the type is sms
        if res["context"]["sheriff_challenge"]["type"] == "sms":
            print("SMS code sent to your phone")
            challenge_url = f"https://api.robinhood.com/challenge/{challenge_id}/respond/"
            # prompt user for sms code
            time.sleep(10)
            phone_number = '00000'  # Replace with the robinhood phone number
            sms_code = get_recent_verification_code(phone_number)
            challenge_response = request_post(url=challenge_url, payload={"response":sms_code})
            request_post(inquiries_url, payload={"sequence":0,"user_input":{"status":"continue"}},json=True)
            return

I added in the time.sleep(10) because I delete the pickle file daily with automation and generate a new one each day before market open, so my authentication.py isn't required to be super fast. Furthermore, for SMS it's unknown to me how fast the text gets received, therefore the slight delay exists.

I should also note that to whoever thought that using twilio would be a solution, I tried entering a twilio phone number into Robinhood to replace my actual phone number, and it wouldn't let me, saying that it was a security risk. So as far as I know, this is the best way I've discovered so far to automate the login process. Change your authentication method to SMS instead of the authenticator app, and find a solution similar to what I did.

If you are editing my code and doing what I did, you will need to edit the phone_number variable in my scripts to whatever number the Robinhood texts come from. I set it to 00000 in these scripts because I was unsure if it would be a security risk for me or not (for any reason).

@Tigeredgar
Copy link

Having a hard time finding the pickle file.
Where is the pickle file located?

@aesterhuizen
Copy link

aesterhuizen commented Jan 28, 2025 via email

@ericwowk1
Copy link

are there any fixes yet for my robinhood app sending verification code instead of device approval? i tried that fork but i still get

packages\robin_stocks\robinhood\authentication.py", line 188, in login
raise Exception(data['detail'])
KeyError: 'detail'

@Gates8911
Copy link

Gates8911 commented Jan 30, 2025

are there any fixes yet for my robinhood app sending verification code instead of device approval? i tried that fork but i still get

packages\robin_stocks\robinhood\authentication.py", line 188, in login raise Exception(data['detail']) KeyError: 'detail'

"""Contains all functions for the purpose of logging in and out to Robinhood."""
import getpass
import os
import pickle
import secrets
import time

from robin_stocks.robinhood.helper import *
from robin_stocks.robinhood.urls import *

def generate_device_token():
    """Generates a cryptographically secure device token."""
    rands = [secrets.randbelow(256) for _ in range(16)]
    hexa = [str(hex(i + 256)).lstrip("0x")[1:] for i in range(256)]
    token = ""
    for i, r in enumerate(rands):
        token += hexa[r]
        if i in [3, 5, 7, 9]:
            token += "-"
    return token

def respond_to_challenge(challenge_id, sms_code):
    """This function will post to the challenge url.
    :param challenge_id: The challenge id.
    :type challenge_id: str
    :param sms_code: The sms code.
    :type sms_code: str
    :returns:  The response from requests.
    """
    url = challenge_url(challenge_id)
    payload = {
        'response': sms_code
    }
    return(request_post(url, payload))

def login(username=None, password=None, expiresIn=86400, scope='internal', by_sms=True, store_session=True, mfa_code=None, pickle_path="", pickle_name=""):
    """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]
    :param pickle_path: Allows users to specify the path of the pickle file.
        Accepts both relative and absolute paths.
    :param pickle_name: Allows users to name Pickle token file in order to switch
        between different accounts without having to re-login every time.
    :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 pickle_path:
        if not os.path.isabs(pickle_path):
            # normalize relative paths
            pickle_path = os.path.normpath(os.path.join(os.getcwd(), pickle_path))
        data_dir = pickle_path
    if not os.path.exists(data_dir):
        os.makedirs(data_dir)
    creds_file = "robinhood" + pickle_name + ".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 = login_url()
    login_payload = {
        'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
        'expires_in': expiresIn,
        'grant_type': 'password',
        'password': password,
        'scope': scope,
        'username': username,
        'challenge_type': challenge_type,
        'device_token': device_token,
        'try_passkeys': False,
        'token_request_path':'/login',
        'create_read_only_secondary_token':True,
    }
    if mfa_code:
        login_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 acess_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']
                    login_payload['device_token'] = pickle_device_token
                    # Set login status to True in order to try and get account info.
                    set_login_state(True)
                    update_session(
                        'Authorization', '{0} {1}'.format(token_type, access_token))
                    # Try to load account profile to check that authorization token is still valid.
                    res = request_get(
                        positions_url(), 'pagination', {'nonzero': 'true'}, jsonify_data=False)
                    # Raises exception if 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=get_output())
                set_login_state(False)
                update_session('Authorization', None)
        else:
            os.remove(pickle_path)
    # Try to log in normally.
    if not username:
        username = input("Robinhood username: ")
        login_payload['username'] = username
    if not password:
        password = getpass.getpass("Robinhood password: ")
        login_payload['password'] = password
    data = request_post(url, login_payload)
    # Handle case where mfa or challenge is required.
    if data:
        if 'mfa_required' in data:
            mfa_token = input("Please type in the MFA code: ")
            login_payload['mfa_code'] = mfa_token
            res = request_post(url, login_payload, jsonify_data=False)
            while (res.status_code != 200):
                mfa_token = input(
                    "That MFA code was not correct. Please type in another MFA code: ")
                login_payload['mfa_code'] = mfa_token
                res = request_post(url, login_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)
            update_session(
                'X-ROBINHOOD-CHALLENGE-RESPONSE-ID', challenge_id)
            data = request_post(url, login_payload)
        elif 'verification_workflow' in data:
            print("Verification workflow required. Please check your Robinhood app for instructions.")
            workflow_id = data['verification_workflow']['id']
            _validate_sherrif_id(device_token=device_token, workflow_id=workflow_id, mfa_code=mfa_code) 
            data = request_post(url, login_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'])
            update_session('Authorization', token)
            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': login_payload['device_token']}, f)
        else:
            if 'detail' in data:
                raise Exception(data['detail'])
            raise Exception(f"Received an error response {data}")
    else:
        raise Exception('Error: Trouble connecting to robinhood API. Check internet connection.')
    return(data)

def _validate_sherrif_id(device_token:str, workflow_id:str,mfa_code:str):
    url = "https://api.robinhood.com/pathfinder/user_machine/"
    machine_payload = {
        'device_id': device_token,
        'flow': 'suv',
        'input': {'workflow_id': workflow_id}
    }
    data = request_post(url=url, payload=machine_payload,json=True)
    machine_id = _get_sherrif_challenge(data)
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"
    response = request_get(inquiries_url)
    challenge_id = response["context"]["sheriff_challenge"]["id"] # used to be type_context
    challenge_url = f"https://api.robinhood.com/challenge/{challenge_id}/respond/" 
    challenge_payload = {'response': mfa_code}
    challenge_response = request_post(url=challenge_url, payload=challenge_payload)
    start_time = time.time()
    while time.time() - start_time < 60: # 1 minute
        time.sleep(5)
        email_text_code = input("Prompt for text or email code (if prompt sent via robinhood app set this to 0000 after verifying):")
        challenge_payload['response'] = email_text_code
        challenge_response = request_post(url=challenge_url, payload=challenge_payload)
        inquiries_payload = {"sequence":0,"user_input":{"status":"continue"}}
        inquiries_response = request_post(url=inquiries_url, payload=inquiries_payload,json=True)
        if inquiries_response["type_context"]["result"] == "workflow_status_approved":
            print("login successful")
            return
        else:
            raise Exception("workflow status not approved")
    raise Exception("Login confirmation timed out. Please try again.")


def _get_sherrif_challenge(data):
    if "id" in data:
        return data["id"]
    raise Exception("Id not returned in user-machine call")

@login_required
def logout():
    """Removes authorization from the session header.
    :returns: None
    """
    set_login_state(False)
    update_session('Authorization', None)

@mike-labadessa
Copy link

mike-labadessa commented Feb 1, 2025

are there any fixes yet for my robinhood app sending verification code instead of device approval? i tried that fork but i still get
packages\robin_stocks\robinhood\authentication.py", line 188, in login raise Exception(data['detail']) KeyError: 'detail'

"""Contains all functions for the purpose of logging in and out to Robinhood."""
import getpass
import os
import pickle
import secrets
import time

from robin_stocks.robinhood.helper import *
from robin_stocks.robinhood.urls import *

def generate_device_token():
    """Generates a cryptographically secure device token."""
    rands = [secrets.randbelow(256) for _ in range(16)]
    hexa = [str(hex(i + 256)).lstrip("0x")[1:] for i in range(256)]
    token = ""
    for i, r in enumerate(rands):
        token += hexa[r]
        if i in [3, 5, 7, 9]:
            token += "-"
    return token

def respond_to_challenge(challenge_id, sms_code):
    """This function will post to the challenge url.
    :param challenge_id: The challenge id.
    :type challenge_id: str
    :param sms_code: The sms code.
    :type sms_code: str
    :returns:  The response from requests.
    """
    url = challenge_url(challenge_id)
    payload = {
        'response': sms_code
    }
    return(request_post(url, payload))

def login(username=None, password=None, expiresIn=689285, scope='internal', by_sms=True, store_session=True, mfa_code=None, pickle_path="", pickle_name=""):
    """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]
    :param pickle_path: Allows users to specify the path of the pickle file.
        Accepts both relative and absolute paths.
    :param pickle_name: Allows users to name Pickle token file in order to switch
        between different accounts without having to re-login every time.
    :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 pickle_path:
        if not os.path.isabs(pickle_path):
            # normalize relative paths
            pickle_path = os.path.normpath(os.path.join(os.getcwd(), pickle_path))
        data_dir = pickle_path
    if not os.path.exists(data_dir):
        os.makedirs(data_dir)
    creds_file = "robinhood" + pickle_name + ".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 = login_url()
    login_payload = {
        'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
        'expires_in': expiresIn,
        'grant_type': 'password',
        'password': password,
        'scope': scope,
        'username': username,
        'challenge_type': challenge_type,
        'device_token': device_token,
        'try_passkeys': False,
        'token_request_path':'/login',
        'create_read_only_secondary_token':True,
    }
    if mfa_code:
        login_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 acess_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']
                    login_payload['device_token'] = pickle_device_token
                    # Set login status to True in order to try and get account info.
                    set_login_state(True)
                    update_session(
                        'Authorization', '{0} {1}'.format(token_type, access_token))
                    # Try to load account profile to check that authorization token is still valid.
                    res = request_get(
                        positions_url(), 'pagination', {'nonzero': 'true'}, jsonify_data=False)
                    # Raises exception if 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=get_output())
                set_login_state(False)
                update_session('Authorization', None)
        else:
            os.remove(pickle_path)
    # Try to log in normally.
    if not username:
        username = input("Robinhood username: ")
        login_payload['username'] = username
    if not password:
        password = getpass.getpass("Robinhood password: ")
        login_payload['password'] = password
    data = request_post(url, login_payload)
    # Handle case where mfa or challenge is required.
    if data:
        if 'mfa_required' in data:
            mfa_token = input("Please type in the MFA code: ")
            login_payload['mfa_code'] = mfa_token
            res = request_post(url, login_payload, jsonify_data=False)
            while (res.status_code != 200):
                mfa_token = input(
                    "That MFA code was not correct. Please type in another MFA code: ")
                login_payload['mfa_code'] = mfa_token
                res = request_post(url, login_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)
            update_session(
                'X-ROBINHOOD-CHALLENGE-RESPONSE-ID', challenge_id)
            data = request_post(url, login_payload)
        elif 'verification_workflow' in data:
            print("Verification workflow required. Please check your Robinhood app for instructions.")
            workflow_id = data['verification_workflow']['id']
            _validate_sherrif_id(device_token=device_token, workflow_id=workflow_id, mfa_code=mfa_code) 
            data = request_post(url, login_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'])
            update_session('Authorization', token)
            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': login_payload['device_token']}, f)
        else:
            if 'detail' in data:
                raise Exception(data['detail'])
            raise Exception(f"Received an error response {data}")
    else:
        raise Exception('Error: Trouble connecting to robinhood API. Check internet connection.')
    return(data)

def _validate_sherrif_id(device_token:str, workflow_id:str,mfa_code:str):
    url = "https://api.robinhood.com/pathfinder/user_machine/"
    machine_payload = {
        'device_id': device_token,
        'flow': 'suv',
        'input': {'workflow_id': workflow_id}
    }
    data = request_post(url=url, payload=machine_payload,json=True)
    machine_id = _get_sherrif_challenge(data)
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"
    response = request_get(inquiries_url)
    challenge_id = response["context"]["sheriff_challenge"]["id"] # used to be type_context
    challenge_url = f"https://api.robinhood.com/challenge/{challenge_id}/respond/" 
    challenge_payload = {'response': mfa_code}
    challenge_response = request_post(url=challenge_url, payload=challenge_payload)
    start_time = time.time()
    while time.time() - start_time < 60: # 1 minute
        time.sleep(5)
        email_text_code = input("Prompt for text or email code (if prompt sent via robinhood app set this to 0000 after verifying):")
        challenge_payload['response'] = email_text_code
        challenge_response = request_post(url=challenge_url, payload=challenge_payload)
        inquiries_payload = {"sequence":0,"user_input":{"status":"continue"}}
        inquiries_response = request_post(url=inquiries_url, payload=inquiries_payload,json=True)
        if inquiries_response["type_context"]["result"] == "workflow_status_approved":
            print("login successful")
            return
        else:
            raise Exception("workflow status not approved")
    raise Exception("Login confirmation timed out. Please try again.")


def _get_sherrif_challenge(data):
    if "id" in data:
        return data["id"]
    raise Exception("Id not returned in user-machine call")

@login_required
def logout():
    """Removes authorization from the session header.
    :returns: None
    """
    set_login_state(False)
    update_session('Authorization', None)

this works. i havent tried with the authenticator app set as verification, but it works for the SMS verification method. Care to explain what you changed? THANK YOU, this is great. wish i could help

@Gates8911
Copy link

Gates8911 commented Feb 1, 2025

this works. i havent tried with the authenticator app set as verification, but it works for the SMS verification method. Care to explain what you changed? THANK YOU, this is great. wish i could help

####REPLY####
Changed mostly the validate sheriff function, robinhood tweaked a few things on the backend, used a browser sign in on the website to log the requests and returns for a couple different accounts, if memory serves they also changed one of their URL's slightly. Regardless the main issue was they switched back to non 3rd party verification for the api, which means a user verification has to be handled now with each login. Some accounts it sends code to email, others it sends text, and finally it can be a simple device approval (regardless of security settings in account, which is annoying). Worked on this for 12 hours straight, its very hard to create a script accounting for all of this because each scenario will have different backend keys being returned and if it is an in-app device approval, that scenario requires no input but the script must still give the user time to verify. So I used the input command to accomplish this, if an account is doing simple device approvals then the input code doesnt matter and basically serves as a delay function. Other than that I also changed the device token generator function to a better more secure method using the "secrets" package instead of "random". Its also more efficient in the form of less redundancies in the code itself. Side note: you can also extend the session, (86400 seconds is not the max the api will allow, the max is now 689285) which is just seconds shy of 8 days instead of 1 day, (but you can also write code to refresh the session for you using the rs.update_session function triggered by time or iterations if youd rather do this for automation. Again, just to reiterate, it does not seem to matter what the account settings are set for, I am assuming that is an issue on their end as far as choosing a security method within the app. It no longer seems to be working with the api (except for 2fa since it does still communicate properly with the 3rd party app, which is why I left mine on). So my account for example has 2fa on, and script handles it with pyotp method and yet I get the in-app device approval now. On one of the other accounts I handle I could not get the device approval to pop up initially, at which point I tried everything, clearing data, deleting tokens, deleting approved devices, turning off my vpn, changing security settings, got nothing, which is why I found it necessary to analyze their website calls and this script was the only one that worked, since I posted I have revised it a little bit and added the max session time as the default if yall want me to post it. Glad I could help, I would still consider myself a noob to coding as well. But clearly I am getting better with time. Took me like a year to develop my full bot and knew very little about computers prior.

@Gates8911
Copy link

Gates8911 commented Feb 2, 2025

@mike-labadessa
Did some more experimenting to see if I could make the authentication script any better or more efficient while also keeping it functional for all security settings and sign in methods, and being proactive as far as keeping some of the script that may not be necessary now but could be later in the event another update happens, it seems robinhood is currently experimenting with their auth methods so I am guessing there are more changes ahead, however I did remove a lot of the code that I viewed as unlikely to be used again as is of which is currently not functioning in the authentication script. This is my final revision and it can now check for which method the account is actually using to validate (email, sms, or in-app verification) and will prompt user accordingly, default sign in is 3 days (should be able to be changed to 7 if you wish to alter the login argument "expiresIn" integer in script or when called). If you have any questions not covered here feel free to message me, hope this helps someone, if for whatever reason it doesnt work for you refer to my previous script if you still need help.

"""Contains all functions for the purpose of logging in and out to Robinhood."""
import getpass
import os
import pickle
import secrets
import time
from robin_stocks.robinhood.helper import *
from robin_stocks.robinhood.urls import *


def generate_device_token():
    """Generates a cryptographically secure device token."""
    rands = [secrets.randbelow(256) for _ in range(16)]
    hexa = [str(hex(i + 256)).lstrip("0x")[1:] for i in range(256)]
    token = ""
    for i, r in enumerate(rands):
        token += hexa[r]
        if i in [3, 5, 7, 9]:
            token += "-"
    return token


def _get_sherrif_id(data):
    if "id" in data:
        return data["id"]
    raise Exception("Id not returned in user-machine call")


def respond_to_challenge(challenge_id, sms_code):
    """This function will post to the challenge url.
    :param challenge_id: The challenge id.
    :type challenge_id: str
    :param sms_code: The sms code.
    :type sms_code: str
    :returns:  The response from requests.
    """
    url = challenge_url(challenge_id)
    payload = {
        'response': sms_code
    }
    return(request_post(url, payload))


def _validate_sherrif_id(device_token: str, workflow_id: str):
    """Handles the Robinhood verification workflow when prompted via the app/text/email."""
    pathfinder_url = "https://api.robinhood.com/pathfinder/user_machine/"
    machine_payload = {'device_id': device_token, 'flow': 'suv', 'input': {'workflow_id': workflow_id}}
    machine_data = request_post(url=pathfinder_url, payload=machine_payload, json=True)
    machine_id = _get_sherrif_id(machine_data)
    
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"
    inquiries_payload = {"sequence": 0, "user_input": {"status": "continue"}}
    inquiries_response = request_get(url=inquiries_url, payload=inquiries_payload)
    
    prompt_id = inquiries_response['context']["sheriff_challenge"]["id"]
    prompt_url = f"https://api.robinhood.com/push/{prompt_id}/get_prompts_status/"
    prompt_response = request_get(prompt_url)
    if prompt_response["challenge_status"] == "issued":
        try:
            # Loop for checking verification status.
            user_code = None
            start_time = time.time()
            while time.time() - start_time < 120:  # 2 minutes timeout
                if 'sheriff_challenge' in inquiries_response['context'] and inquiries_response['context']['sheriff_challenge']['type'] == 'prompt':
                    print(f"(timeout = 15 seconds) check robinhood app for device approval...")
                    time.sleep(15)
                    
                elif 'sheriff_challenge' in inquiries_response['context'] and inquiries_response['context']['sheriff_challenge']['type'] == 'sms':
                    user_code = input("sms verification code: ")
                    
                elif 'sheriff_challenge' in inquiries_response['context'] and inquiries_response['context']['sheriff_challenge']['type'] == 'email':
                    user_code = input("email verification code: ")

                # If manual input is needed for verification.
                if user_code:
                    challenge_payload = {'response': user_code} if user_code != None else {'response': "0000"}
                    challenge_url = f"https://api.robinhood.com/challenge/{machine_id}/respond/"
                    challenge_response = request_post(url=challenge_url, payload=challenge_payload)
                inquiries_response = request_post(url=inquiries_url, payload=inquiries_payload,json=True)
                if "type_context" in inquiries_response and inquiries_response["type_context"]["result"] == "workflow_status_approved":
                    return
                else:
                    print("Waiting for login verification via sms, email, or in-app device approval ")
                    continue

            raise Exception("Login confirmation timed out. Please try again.")
        except Exception as e:
            print(f"exception line 88, Error in validation: {e}")
    else:
        inquiries_response = request_post(url=inquiries_url, payload=inquiries_payload,json=True)
        if inquiries_response["type_context"]["result"] == "workflow_status_approved":
            print("login successful")
        else:
            print(f"line 95 Error in validation")


def login(username=None, password=None, expiresIn=86400, scope='internal', by_sms=True, store_session=True, mfa_code=None, pickle_path="", pickle_name=""):
    """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]
    :param pickle_path: Allows users to specify the path of the pickle file.
        Accepts both relative and absolute paths.
    :param pickle_name: Allows users to name Pickle token file in order to switch
        between different accounts without having to re-login every time.
    :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 pickle_path:
        if not os.path.isabs(pickle_path):
            # normalize relative paths
            pickle_path = os.path.normpath(os.path.join(os.getcwd(), pickle_path))
        data_dir = pickle_path
    if not os.path.exists(data_dir):
        os.makedirs(data_dir)
    creds_file = "robinhood" + pickle_name + ".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 = login_url()
    login_payload = {
        'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
        'expires_in': expiresIn,
        'grant_type': 'password',
        'password': password,
        'scope': scope,
        'username': username,
        'challenge_type': challenge_type,
        'device_token': device_token,
        'try_passkeys': False,
        'token_request_path':'/login',
        'create_read_only_secondary_token':True,
    }
    if mfa_code:
        login_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 acess_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']
                    login_payload['device_token'] = pickle_device_token
                    # Set login status to True in order to try and get account info.
                    set_login_state(True)
                    update_session(
                        'Authorization', '{0} {1}'.format(token_type, access_token))
                    # Try to load account profile to check that authorization token is still valid.
                    res = request_get(
                        positions_url(), 'pagination', {'nonzero': 'true'}, jsonify_data=False)
                    # Raises exception if 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=get_output())
                set_login_state(False)
                update_session('Authorization', None)
        else:
            os.remove(pickle_path)
    # Try to log in normally.
    if not username:
        username = input("Robinhood username: ")
        login_payload['username'] = username
    if not password:
        password = getpass.getpass("Robinhood password: ")
        login_payload['password'] = password
    data = request_post(url, login_payload)
    # Handle challenge.
    if data:
        try:
            if 'verification_workflow' in data:
                workflow_id = data['verification_workflow']['id']
                _validate_sherrif_id(device_token=device_token, workflow_id=workflow_id) 
                data = request_post(url, login_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'])
                update_session('Authorization', token)
                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': login_payload['device_token']}, f)
                        print("login successful!")
                        return
        except Exception as e:
            print(f"Error execption line 223: {e}")
    else:
        raise Exception('Error: Trouble connecting to robinhood API. Check internet connection.')
    return(data)


@login_required
def logout():
    """Removes authorization from the session header.
    :returns: None
    """
    set_login_state(False)
    update_session('Authorization', None)

@schnup
Copy link

schnup commented Feb 3, 2025

I used the new authorization.py file. It has been working for two weeks until now I am getting this new error
=> Error in request_post: Received 502 Error execption line 223: argument of type 'NoneType' is not iterable

I also couldn't log in my account from the RBH webpage. It kept loading in the login page.

@alex-l-zhou
Copy link

@schnup check vpn

@nickreynolds84
Copy link

I seem to be having a very similar issue but the solutions above do not work for me. After the update in late December, I have been able to authenticate. However after the 17th I have been unable to authenticate programmatically using mfa. I am getting the attached error. Anyone have any thoughts?
For reference I am running python 3.10 and robin-stocks 3.3.1.

Image

@mike-labadessa
Copy link

I seem to be having a very similar issue but the solutions above do not work for me. After the update in late December, I have been able to authenticate. However after the 17th I have been unable to authenticate programmatically using mfa. I am getting the attached error. Anyone have any thoughts? For reference I am running python 3.10 and robin-stocks 3.3.1.

Image

@nickreynolds84
I haven’t had the time to try @Gates8911 2nd solution, or test all of the options. I’ve been trying to get to it but have been busy.

I got Gates8911 1st solution to work by switching from mfa to text verification.

@henryzhangpku
Copy link
Contributor

This new auth method has been working for a few days and stopped working now ...

@mike-labadessa
Copy link

This new auth method has been working for a few days and stopped working now ...

I just tried a second a go and it worked. Maybe try restarting your kernel. I tried a second time and it crashed on the sheriff_id. But restarting the kernel fixed it

@Gates8911
Copy link

@nickreynolds84
Not sure what version of the authentication your using but i do know its not the last script I shared considering my final auth script ends on line 235

@nickreynolds84
Copy link

This new auth method has been working for a few days and stopped working now ...

I just tried a second a go and it worked. Maybe try restarting your kernel. I tried a second time and it crashed on the sheriff_id. But restarting the kernel fixed it

Thanks @mike-labadessa. I'll try this first

@nickreynolds84
Copy link

nickreynolds84 commented Feb 4, 2025

@nickreynolds84 Not sure what version of the authentication your using but i do know its not the last script I shared considering my final auth script ends on line 235

@Gates8911 I copied your last solution to try next, just haven't gotten to it yet. I'm hoping to continue to authenticate using mfa because I am not always available to answer the text verification like others have seemed to get to work

@Gates8911
Copy link

@mike-labadessa @nickreynolds84 @henryzhangpku @alex-l-zhou @schnup
Just found out that the extended session default time only works on certain accounts or certain circumstances, I just decided to get on my test account to experiment with my AI bot using my RH test account and I had trouble logging in, I went through the code over and over, verified all calls used and full login flow and it wouldnt let me login, so i tried the only thing it could be at that point, the default expiresIn time. Changed it back to 86400 seconds and it began working again, but you can still have your script trigger a refresh session command before the session expires to increase session times, my script still runs for weeks at a time and all i did is have it re-login after an error is caught, which typically i have some sort of minor routine error once or twice a day and this restarts the session time before the token expires as well. Or you can use the rs.update_session() function to be triggered every 23 hours. Ps. I already modified both of my above scripts to use 86400 default session time, sorry if this caused issues for anyone, if so it should be fixed now.

@mike-labadessa
Copy link

@mike-labadessa @nickreynolds84 @henryzhangpku @alex-l-zhou @schnup Just found out that the extended session default time only works on certain accounts or certain circumstances, I just decided to get on my test account to experiment with my AI bot using my RH test account and I had trouble logging in, I went through the code over and over, verified all calls used and full login flow and it wouldnt let me login, so i tried the only thing it could be at that point, the default expiresIn time. Changed it back to 86400 seconds and it began working again, but you can still have your script trigger a refresh session command before the session expires to increase session times, my script still runs for weeks at a time and all i did is have it re-login after an error is caught, which typically i have some sort of minor routine error once or twice a day and this restarts the session time before the token expires as well. Or you can use the rs.update_session() function to be triggered every 23 hours. Ps. I already modified both of my above scripts to use 86400 default session time, sorry if this caused issues for anyone, if so it should be fixed now.

Triggering the rs.update_session() every 23 hours makes sense because 86400 seconds equal one day. Maybe Robinhood devs hardcoded 86400 on their end somewhere. Thanks @Gates8911

@Gates8911
Copy link

If anyone needs another authentication to try let me know, I made one more, wasnt fully convinced I had all possibilities covered. Didnt automatically post it bc i didnt want to spam this chat with more code. Just email me if you need another one to try. Mgates8900@gmail.com

@Adelantado
Copy link

If anyone needs another authentication to try let me know, I made one more, wasnt fully convinced I had all possibilities covered. Didnt automatically post it bc i didnt want to spam this chat with more code. Just email me if you need another one to try. Mgates8900@gmail.com

Thanks so much for your time and contribution to the cause ;), same goes to all in here.
Fix is working for me as I am able to make the script log in having to authenticate device in the app, however, in my case and I am sure there are many out there in the same boat and, knowing that what I am about to say is on Robinhood's end and has nothing to do with the original API, this or any solution is less than ideal as it makes all the time ( almost 4 years ), money and effort poured into having BOTs running 24/7 on AWS without human interaction useless.
Bots were set up to check status on each other and if found to be down, launch each other; now I had to send myself a notification to make me aware Bot is down and include a pause at login until I am ready to connect to AWS, un-pause and authorize device on app which makes, as I said, all the work done useless. The reason of the pause is cause if you try to loop and keep trying to log in without authenticating in the app, Robin will flag your IP and you will start to get 429 errors which for may can be a game over.
Hope this info is useful to someone, and I also hope Robin will make changes to allow scripts to login automatically once again.
Thanks again to Yall!

@mobeston
Copy link

mobeston commented Feb 6, 2025

Just curious why no one here uses alpaca instead

@Kr1msonReaper
Copy link

Kr1msonReaper commented Feb 6, 2025 via email

@doctorcolossus
Copy link

How specifically are they overpriced? Aren't their spreads similar to Robinhood's?

Although actually building this unofficial Robinhood API has taken a lot of work, it doesn't seems to me like there is any difference in "programming skills" required to use it vs. using Alpaca's official one.

@Gates8911
Copy link

Gates8911 commented Feb 7, 2025

@doctorcolossus @Kr1msonReaper @nickreynolds84 @mobeston @Adelantado @mike-labadessa @henryzhangpku @alex-l-zhou @ravi-bharadwaj @pielshawn @zabidin901 @HMSS013 @jmfernandes @cpasean @nat2k5us @schnup
Since I see theres still a few issues people are having ill post my latest one that has had zero issues now across 3 different accounts, one using device approvals, another using email, and another using sms, (this code can be used with 3rd party 2fa but since robinhoods api doesnt support 2fa with the last update it is not necessary to include 2fa but will still work, I kept the option in bc the case may be that robinhood found security concerns with their 2fa and may fix it and change back to it, in which case it would be nice to still have some of the 2fa code, but this script really just needs username and pass even though 2fa is still an option). I also realize this is quite a bit longer than previous versions but it has to check for all 3 verification methods and added retries to the logic with various events and more output logs for different scenarios to better diagnose a failure. If this doesnt work for you then I would check login method thoroughly (I say this because I literally cant improve this script any further, if someone else can more power to ya, if this works for everyone I think it should be merged for now)

import getpass
import os
import pickle
import secrets
import time
from robin_stocks.robinhood.helper import *
from robin_stocks.robinhood.urls import *

def generate_device_token():
    """Generates a cryptographically secure device token."""
    rands = [secrets.randbelow(256) for _ in range(16)]
    hexa = [str(hex(i + 256)).lstrip("0x")[1:] for i in range(256)]
    token = ""
    for i, r in enumerate(rands):
        token += hexa[r]
        if i in [3, 5, 7, 9]:
            token += "-"
    return token


def _get_sherrif_id(data):
    """Extracts the sheriff verification ID from the response."""
    if "id" in data:
        return data["id"]
    raise Exception("Error: No verification ID returned in user-machine response")


def _validate_sherrif_id(device_token: str, workflow_id: str):
    """Handles Robinhood's verification workflow, including email, SMS, and app-based approvals."""
    print("Starting verification process...")
    pathfinder_url = "https://api.robinhood.com/pathfinder/user_machine/"
    machine_payload = {'device_id': device_token, 'flow': 'suv', 'input': {'workflow_id': workflow_id}}
    machine_data = request_post(url=pathfinder_url, payload=machine_payload, json=True)

    machine_id = _get_sherrif_id(machine_data)
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"

    start_time = time.time()
    
    while time.time() - start_time < 120:  # 2-minute timeout
        time.sleep(5)
        inquiries_response = request_get(inquiries_url)

        if not inquiries_response:  # Handle case where response is None
            print("Error: No response from Robinhood API. Retrying...")
            continue

        if "context" in inquiries_response and "sheriff_challenge" in inquiries_response["context"]:
            challenge = inquiries_response["context"]["sheriff_challenge"]
            challenge_type = challenge["type"]
            challenge_status = challenge["status"]
            challenge_id = challenge["id"]
            if challenge_type == "prompt":
                print("Check robinhood app for device approvals method...")
                prompt_url = f"https://api.robinhood.com/push/{challenge_id}/get_prompts_status/"
                while True:
                    time.sleep(5)
                    prompt_challenge_status = request_get(url=prompt_url)
                    if prompt_challenge_status["challenge_status"] == "validated":
                        break
                break

            if challenge_status == "validated":
                print("Verification successful!")
                break  # Stop polling once verification is complete

            if challenge_type in ["sms", "email"] and challenge_status == "issued":
                user_code = input(f"Enter the {challenge_type} verification code sent to your device: ")
                challenge_url = f"https://api.robinhood.com/challenge/{challenge_id}/respond/"
                challenge_payload = {"response": user_code}
                challenge_response = request_post(url=challenge_url, payload=challenge_payload)

                if challenge_response.get("status") == "validated":
                    break

    # **Now poll the workflow status to confirm final approval**
    inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{machine_id}/user_view/"
    
    retry_attempts = 5  # Allow up to 5 retries in case of 500 errors
    while time.time() - start_time < 120:  # 2-minute timeout 
        try:
            inquiries_payload = {"sequence": 0, "user_input": {"status": "continue"}}
            inquiries_response = request_post(url=inquiries_url, payload=inquiries_payload,json=True)
            if "type_context" in inquiries_response and inquiries_response["type_context"]["result"] == "workflow_status_approved":
                print("Verification successful!")
                return
            else:
                time.sleep(5)  # **Increase delay between requests to prevent rate limits**
        except requests.exceptions.RequestException as e:
            time.sleep(5)
            print(f"API request failed: {e}")
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")
            print("Retrying workflow status check...")
            continue

        if not inquiries_response:  # Handle None response
            time.sleep(5)
            print("Error: No response from Robinhood API. Retrying...")
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")
            continue

        workflow_status = inquiries_response.get("verification_workflow", {}).get("workflow_status")

        if workflow_status == "workflow_status_approved":
            print("Workflow status approved! Proceeding with login...")
            return
        elif workflow_status == "workflow_status_internal_pending":
            print("Still waiting for Robinhood to finalize login approval...")
        else:
            retry_attempts -= 1
            if retry_attempts == 0:
                raise TimeoutError("Max retries reached. Assuming login approved and proceeding.")

    raise TimeoutError("Timeout reached. Assuming login is approved and proceeding.")



def login(username=None, password=None, expiresIn=86400, scope='internal', store_session=True, mfa_code=None, pickle_path="", pickle_name=""):
    """Handles the login process to Robinhood, including multi-factor authentication, session persistence, and verification handling."""
    print("Starting login process...")
    device_token = generate_device_token()
    home_dir = os.path.expanduser("~")
    data_dir = os.path.join(home_dir, ".tokens")

    if pickle_path:
        if not os.path.isabs(pickle_path):
            pickle_path = os.path.normpath(os.path.join(os.getcwd(), pickle_path))
        data_dir = pickle_path

    if not os.path.exists(data_dir):
        os.makedirs(data_dir)

    creds_file = "robinhood" + pickle_name + ".pickle"
    pickle_path = os.path.join(data_dir, creds_file)

    url = login_url()
    login_payload = {
        'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
        'expires_in': expiresIn,
        'grant_type': 'password',
        'password': password,
        'scope': scope,
        'username': username,
        'device_token': device_token,
        'try_passkeys': False,
        'token_request_path': '/login',
        'create_read_only_secondary_token': True,
    }

    if mfa_code:
        login_payload['mfa_code'] = mfa_code

    # **Load cached authentication session if available**
    if os.path.isfile(pickle_path) and 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']
                pickle_device_token = pickle_data['device_token']
                login_payload['device_token'] = pickle_device_token
                set_login_state(True)
                update_session('Authorization', f'{token_type} {access_token}')
                print("Logged in using cached credentials.")
                return {'access_token': access_token, 'token_type': token_type}
        except Exception as e:
            print(f"Error loading cached credentials. Attempting normal login. Error: {e}")
            set_login_state(False)
            update_session('Authorization', None)

    # **Attempt to login normally**
    if not username:
        username = input("Robinhood username: ")
        login_payload['username'] = username
    if not password:
        password = getpass.getpass("Robinhood password: ")
        login_payload['password'] = password

    data = request_post(url, login_payload)

    if data:
        try:
            if 'verification_workflow' in data:
                print("Verification required, handling challenge...")
                workflow_id = data['verification_workflow']['id']
                _validate_sherrif_id(device_token, workflow_id)

                # Reattempt login after verification
                data = request_post(url, login_payload)

            if 'access_token' in data:
                token = f"{data['token_type']} {data['access_token']}"
                update_session('Authorization', token)
                set_login_state(True)

                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': login_payload['device_token']
                        }, f)
                return data
        except Exception as e:
            print(f"Error during login verification: {e}")

    print("Login failed. Check credentials and try again.")
    return None


@login_required
def logout():
    """Logs out from Robinhood by clearing session data."""
    set_login_state(False)
    update_session('Authorization', None)
    print("Logged out successfully.")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests