-
Notifications
You must be signed in to change notification settings - Fork 490
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
fix #524: login with robinhood mfa #526
Conversation
can some one tell me whats wrong with my code that i keep getting KeyError: 'status', from robin_stocks.robinhood.helper import * def generate_device_token():
def respond_to_challenge(challenge_id, sms_code):
def login(username=None, password=None, expiresIn=86400, scope='internal', by_sms=True, store_session=True, mfa_code=None, pickle_path="", pickle_name=""):
def _validate_sherrif_id(device_token:str, workflow_id:str,mfa_code:str): def _get_sherrif_challenge(token_id:str, data:dict):
@login_required
|
@pulkitcollier I submitted a parallel request for this fix. I think @pulkitcollier's solution looks better than mine. @jmfernandes |
@pulkitcollier @noahfields - Thanks gents for this, you both are legends! For this commit - I replaced this code with my authentication.py file. I have a test script that spits out the MFA code, and tells me to prompt it. I input the MFA code and get the below error, any thoughts? Error: Error during Robinhood login: 'dict' object has no attribute 'status_code' This is my test script: import robin_stocks.robinhood as robin # Ensure you're importing from the correct submodule def login_robinhood():
Call the login function to testlogin_robinhood() |
anyone know how to make the code work for device approvals. i only get device approvals now from my iphone robinhood app. and then the code i run waits for the device approval to be approved and then checks to see if i can log in successfully. but it never sees that i approved the device. import random Generate a unique device token hexa = [str(hex(i + 256)).lstrip("0x")[1:] for i in range(256)] Save and load session data def load_session(pickle_name="robinhood_session.pickle"): def handle_verification_workflow(device_token, workflow_id): inquiries_url = f"https://api.robinhood.com/pathfinder/inquiries/{response.json()['id']}/user_view/" for attempt in range(max_attempts):
print("Device approval timed out. Please ensure you approved the login request.") Login to Robinhooddef robinhood_login(username=None, password=None, expires_in=86400, scope="internal"): url = "https://api.robinhood.com/oauth2/token/" session = load_session() response = requests.post(url, data=payload) if response.status_code == 200: if "verification_workflow" in data: print("Failed to log in:", data) Verification workflow triggered. Workflow ID: 00e60f42-de4c-4282-ad7d-e9c01a0206e1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❗ Noticed some potential issues with this approach:
1. Forcing a Numeric Code, Even If You Only Got a Link
You always prompt the user for a “Sheriff Challenge code.” But in many “verification_workflow” flows, Robinhood only sends a link or an app push notification to approve the new device (no numeric code). The user may have nothing to type. If that’s the case, the user is stuck, or they might attempt to type something meaningless and get repeated errors.
A better approach may be to inform the user that if they only see a link, they must click it in the Robinhood email/app. If the user truly has a numeric code, they type it here. Otherwise, the code will keep failing.
2. The While Loop in _validate_sherrif_id()
Reprompts for MFA in the Wrong Place: Inside _validate_sherrif_id()
data = request_post(url=url, payload=payload, json=True)
while (data.status_code != 200):
mfa_code = input("That MFA code was not correct. Please type in another MFA code: ")
data = request_post(url, payload, jsonify_data=False)
Potential issues here:
- This is the user_machine call, not the actual “respond to challenge” call. The payload used is:
payload = {
'device_id': device_token,
'flow': 'suv',
'input': {'workflow_id': workflow_id}
}
- That does not include the user’s numeric code in the payload. So re-typing the code doesn’t change anything about how this call is made—it’s always the same body.
If data.status_code != 200 is due to a network error, or some other reason, you’re stuck prompting for an MFA code that doesn’t even get used in that payload.
- In other words, this loop conflates “retry the numeric code” with “retry the user_machine call.” They’re different API endpoints. In reality, the code-based challenge is handled later with:
challenge_payload = {'response': mfa_code}
challenge_response = request_post(url=challenge_url, payload=challenge_payload, json=True)
That is where a wrong code should trigger a re-prompt. But in your snippet, you do it on the user_machine call.
3. No Separate Handling for Link-Based Approvals
If Robinhood only sent an email reading “Click here to approve this device,” the user has no numeric code. They must manually click the link. Your code:
mfa_code = input("Please type in the MFA code: ")
_validate_sherrif_id(...)
4. Potential for Infinite Loops or Confusion
Because _validate_sherrif_id() uses:
while (data.status_code != 200):
...
data = request_post(url, payload, jsonify_data=False)
there’s a risk of repeated failing if the code is truly wrong or if it’s a link-based challenge. You’ll keep prompting for an MFA code but never calling the actual challenge endpoint to re-check the code. The loop might never exit if the root cause isn’t a bad code but a link-based approval.
✔️ Improvised Patches:
A simplified fix to this without any additional complexities may be a minor alteration in the verification_workflow
:
elif 'verification_workflow' in data:
workflow_id = data['verification_workflow']['id']
if not mfa_code:
print("If you received a numeric 'Sheriff Challenge' code via SMS or email, enter it below. If you only got a link, you must approve that link instead.")
mfa_code = input("Sheriff Challenge code (if any): ")
_validate_sherrif_id(device_token=device_token, workflow_id=workflow_id, mfa_code=mfa_code)
data = request_post(url, payload)
- Separates the sheriff challenge logic into
_validate_sherrif_id()
- Warns the user about the difference between a code-based vs. link-based challenge.
- Avoids the confusing infinite re-prompt on the “user_machine”.
Potential enhancments with this approach:
- If the user tries to proceed without a code in a link-based scenario, we'll raise an exception. That’s not a bug per se, but we might want to refine the user message or do a more graceful exit—however, that’s a design choice, and the code is still workable as is.
@noahfields approach also works well being that they prompt for a Sheriff Code in _validate_sherrif_id
: #532 🏆
if mfa_code == None:
mfa_code = input("Please type in the MFA code: ")
in _validate_sherrif_id()
, you allow users who receive a numeric code via SMS or email to enter it. Previously, the library would try to validate with mfa_code=None
and fail immediately.
- Avoids Hard 401
If the user has a code-based challenge, they can now provide that code in real-time rather than being stuck. - Maintains Old Flows
The rest of the code for 'mfa_required' and 'challenge' blocks remains unchanged, so normal SMS/email 2FA or the older challenge flows still work as before.
❗ Improvement: Add a short clarity message. This at least clarifies that link-based verification requires a manual step. But the rest is still the user’s responsibility to click that link.
Additional lore around the origin of this issue:
- During the week of December 8th:
- Robinhood has introduced, or made changes to, a “Sheriff Challenge” or “verification_workflow” mechanism for unrecognized devices. This does not set "mfa_required" in the JSON response, so the normal SMS/Email “challenge” block is never triggered.
- When
mfa_code
is not provided,_validate_sherrif_id()
fails immediately because it sendsNone
to the endpoint. The new code-based prompt fixes this edge case for anyone receiving a numeric code.
PR is redundant with #532 being merged in. |
Prompt for mfa code after first request to resolve Issue #524