wang-ye / robinhood-crypto

Robinhood Crypto
MIT License
45 stars 16 forks source link

LoginException: 400 Client Error: Bad Request for https://api.robinhood.com/oauth2/token/ #7

Open doctorcolossus opened 5 years ago

doctorcolossus commented 5 years ago

As of 2019/5/3 15:25 (UTC +2), I have been getting the following robinhood_crypto_api.robinhood_crypto_api.LoginException:

400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
Traceback (most recent call last):
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 133, in session_request
    resp.raise_for_status()
  File "requests/models.py", line 940, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
Traceback (most recent call last):
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 157, in get_access_token
    data = self.session_request(RobinhoodCrypto.ENDPOINTS['auth'], json_payload=payload, timeout=5, method='post', request_session=auth_session)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 50, in function_reauth
    raise e
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 40, in function_reauth
    res = f(*args, **kwargs)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 137, in session_request
    raise e
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 133, in session_request
    resp.raise_for_status()
  File "requests/models.py", line 940, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
Traceback (most recent call last):
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 157, in get_access_token
    data = self.session_request(RobinhoodCrypto.ENDPOINTS['auth'], json_payload=payload, timeout=5, method='post', request_session=auth_session)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 50, in function_reauth
    raise e
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 40, in function_reauth
    res = f(*args, **kwargs)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 137, in session_request
    raise e
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 133, in session_request
    resp.raise_for_status()
  File "requests/models.py", line 940, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "./robinhood.py", line 39, in <module>
    r = RobinhoodCrypto('**************', '****************')
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 103, in __init__
    _access_token = self.get_access_token(self.username, self.password)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 162, in get_access_token
    raise LoginException()
robinhood_crypto_api.robinhood_crypto_api.LoginException

Had been busy, unfortunately didn't check my script and notice till today.

Any ideas?

doctorcolossus commented 5 years ago

I did double-check my credentials.

doctorcolossus commented 5 years ago

I found a similar issue on Jamonek's repo. aamazie claims that he has resolved this in his fork. I believe this is the relevant commit. I don't understand the code at first glance. Maybe this information will help you. If not, I will try to work on it when I can find some more free time.

wang-ye commented 5 years ago

i will work on it during the weekend. Thanks for reporting this

wang-ye commented 5 years ago

hi, the current master now supports the mfa use case : https://github.com/wang-ye/robinhood-crypto/commit/bc6daecd6127625e7d0b3544d461219e0c54fcab

doctorcolossus commented 5 years ago

Hi wang-ye, I had been busy with school and just got a chance to test this.

It is possible that something else changed since you fixed this, or maybe I am overlooking something about how to use MFA authentication.

However, I'm still getting the same error as before with your latest code:

400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
Traceback (most recent call last):
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 129, in session_request
    resp.raise_for_status()
  File "requests/models.py", line 940, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
Traceback (most recent call last):
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 180, in get_access_token
    data = self.session_request(RobinhoodCrypto.ENDPOINTS['auth'], json_payload=payload, timeout=5, method='post', request_session=auth_session)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 51, in function_reauth
    raise e
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 41, in function_reauth
    res = f(*args, **kwargs)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 133, in session_request
    raise e
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 129, in session_request
    resp.raise_for_status()
  File "requests/models.py", line 940, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
Traceback (most recent call last):
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 180, in get_access_token
    data = self.session_request(RobinhoodCrypto.ENDPOINTS['auth'], json_payload=payload, timeout=5, method='post', request_session=auth_session)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 51, in function_reauth
    raise e
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 41, in function_reauth
    res = f(*args, **kwargs)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 133, in session_request
    raise e
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 129, in session_request
    resp.raise_for_status()
  File "requests/models.py", line 940, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "./robinhood-csv.py", line 28, in <module>
    r = RobinhoodCrypto('**************', '**************')
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 99, in __init__
    _access_token = self.get_access_token(self.username, self.password)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 187, in get_access_token
    raise LoginException()
robinhood_crypto_api.robinhood_crypto_api.LoginException
doctorcolossus commented 5 years ago

I just checked and verified that self.GenerateDeviceToken() is indeed running and returning a device_token to self.device_token. It looks legit - it's a dash-separated hex value with digits {8}-{4}-{4}-{4}-{12}. Nevertheless, I am still getting the LoginException.

doctorcolossus commented 5 years ago

Okay, I just noticed I got a bunch of SMS verification codes from Robinhood - so I think that's the issue. You need to handle the challenge somehow. See lines 166-180 in aamazie's fork of Jamonek's repo.

wang-ye commented 5 years ago

Did you type in the verification codes in the terminal? https://github.com/wang-ye/robinhood-crypto/commit/bc6daecd6127625e7d0b3544d461219e0c54fcab#diff-7f3fc7d4172fcd78eb5c5dc07ac78047R181

doctorcolossus commented 5 years ago

I am never prompted for a verification code.

Using the latest code, I get an error due to self.device_token not being initialized.

>>> from robinhood_crypto_api import RobinhoodCrypto
>>> r = RobinhoodCrypto('**************', '**************')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 98, in __init__
    _access_token = self.get_access_token(self.username, self.password)
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 163, in get_access_token
    if not self.device_token:
AttributeError: 'RobinhoodCrypto' object has no attribute 'device_token'

If I add self.device_token = None between lines 94 & 95, it gets a little further, but then I receive the LoginException before being prompted for any verification code:

>>> from robinhood_crypto_api import RobinhoodCrypto
>>> r = RobinhoodCrypto('**************', '**************')
400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
Traceback (most recent call last):
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 129, in session_request
    resp.raise_for_status()
  File "/usr/lib/python3.7/site-packages/requests/models.py", line 909, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
Traceback (most recent call last):
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 180, in get_access_token
    data = self.session_request(RobinhoodCrypto.ENDPOINTS['auth'], json_payload=payload, timeout=5, method='post', request_session=auth_session)
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 51, in function_reauth
    raise e
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 41, in function_reauth
    res = f(*args, **kwargs)
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 133, in session_request
    raise e
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 129, in session_request
    resp.raise_for_status()
  File "/usr/lib/python3.7/site-packages/requests/models.py", line 909, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
Traceback (most recent call last):
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 180, in get_access_token
    data = self.session_request(RobinhoodCrypto.ENDPOINTS['auth'], json_payload=payload, timeout=5, method='post', request_session=auth_session)
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 51, in function_reauth
    raise e
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 41, in function_reauth
    res = f(*args, **kwargs)
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 133, in session_request
    raise e
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 129, in session_request
    resp.raise_for_status()
  File "/usr/lib/python3.7/site-packages/requests/models.py", line 909, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 99, in __init__
    _access_token = self.get_access_token(self.username, self.password)
  File "robinhood-crypto/robinhood_crypto_api/robinhood_crypto_api.py", line 187, in get_access_token
    raise LoginException()
robinhood_crypto_api.robinhood_crypto_api.LoginException
wang-ye commented 5 years ago

On my local run I got the MFA prompt. Ideally, if given the correct parameters, the api call at line 180 should not throw an exception. Could you print out the payload parameters and try to simulate the same request using postman?

doctorcolossus commented 5 years ago

Hmm, interesting. I installed Postman, but I've never used it before. I used to use the Live HTTP Headers Firefox plugin, so I assume it's something similar to that. I'm pretty busy with schoolwork right now, but I'll try to dig deeper as soon as I can find and I'll let you know if I can find any clues about what could be causing this problem.

doctorcolossus commented 5 years ago

Sorry for the delay! I have been busy and was also procrastinating learning Postman. I decided to just be lazy and use the logging package. Here is the full output with httplib and requests debugging. I did receive the SMS code right after making this request.

DEBUG:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): api.robinhood.com
send: b'CONNECT api.robinhood.com:443 HTTP/1.0\r\n'
send: b'\r\n'
send: b'POST /oauth2/token/ HTTP/1.1\r\nHost: api.robinhood.com\r\naccept-encoding: gzip, deflate\r\naccept-language: en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5\r\ncontent-type: application/json\r\nconnection: keep-alive\r\norigin: https://robinhood.com\r\nuser-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36\r\naccept: */*\r\nContent-Length: 268\r\n\r\n'
send: b'{"password": "**************", "username": "**************", "grant_type": "password", "scope": "internal", "client_id": "****************************************", "expires_in": 86400, "device_token": "********-****-****-****-************", "challenge_type": "sms"}'
reply: 'HTTP/1.1 400 Bad Request\r\n'
header: Date: Wed, 31 Jul 2019 05:08:14 GMT
header: Content-Type: application/json
header: Content-Length: 300
header: Connection: keep-alive
header: Server: openresty
header: Allow: POST, OPTIONS
header: X-Robinhood-API-Version: 0.0.0
header: Content-Security-Policy: default-src 'none'
header: X-Frame-Options: SAMEORIGIN
header: x-content-type-options: nosniff
header: x-xss-protection: 1; mode=block
header: Access-Control-Allow-Origin: https://robinhood.com
header: Vary: Origin
DEBUG:requests.packages.urllib3.connectionpool:https://api.robinhood.com:443 "POST /oauth2/token/ HTTP/1.1" 400 300
DEBUG:robinhood_crypto_api.robinhood_crypto_api:Error in session request calls. Request body b'{"password": "**************", "username": "**************", "grant_type": "password", "scope": "internal", "client_id": "****************************************", "expires_in": 86400, "device_token": "********-****-****-****-************", "challenge_type": "sms"}', headers {'accept-encoding': 'gzip, deflate', 'accept-language': 'en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5', 'content-type': 'application/json', 'connection': 'keep-alive', 'origin': 'https://robinhood.com', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36', 'accept': '*/*', 'Content-Length': '268'}, content b'{"detail":"Request blocked, challenge issued.","challenge":{"id":"********-****-****-****-************","user":"********-****-****-****-************","type":"sms","alternate_type":"email","status":"issued","remaining_retries":3,"remaining_attempts":3,"expires_at":"2019-07-31T01:13:14.631101-04:00"}}'
ERROR:robinhood_crypto_api.robinhood_crypto_api:400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
Traceback (most recent call last):
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 141, in session_request
    resp.raise_for_status()
  File "/usr/lib/python3.7/site-packages/requests/models.py", line 909, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
ERROR:robinhood_crypto_api.robinhood_crypto_api:400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
Traceback (most recent call last):
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 192, in get_access_token
    data = self.session_request(RobinhoodCrypto.ENDPOINTS['auth'], json_payload=payload, timeout=5, method='post', request_session=auth_session)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 63, in function_reauth
    raise e
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 53, in function_reauth
    res = f(*args, **kwargs)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 145, in session_request
    raise e
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 141, in session_request
    resp.raise_for_status()
  File "/usr/lib/python3.7/site-packages/requests/models.py", line 909, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
Traceback (most recent call last):
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 192, in get_access_token
    data = self.session_request(RobinhoodCrypto.ENDPOINTS['auth'], json_payload=payload, timeout=5, method='post', request_session=auth_session)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 63, in function_reauth
    raise e
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 53, in function_reauth
    res = f(*args, **kwargs)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 145, in session_request
    raise e
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 141, in session_request
    resp.raise_for_status()
  File "/usr/lib/python3.7/site-packages/requests/models.py", line 909, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 111, in __init__
    _access_token = self.get_access_token(self.username, self.password)
  File "robinhood_crypto_api/robinhood_crypto_api.py", line 199, in get_access_token
    raise LoginException()
robinhood_crypto_api.robinhood_crypto_api.LoginException
doctorcolossus commented 5 years ago

Below is a comparison between the failed authentication request made by robinhood-crypto with the successful one made by aamazie's Robinhood fork. Both requests resulted in receiving an SMS authentication code. The key differences I have been able to spot are the following:

robinhood-crypto:

POST /oauth2/token/ HTTP/1.1
Host: api.robinhood.com
accept-encoding: gzip, deflate
accept-language: en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5
content-type: application/json
connection: keep-alive
origin: https://robinhood.com
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
accept: */*
Content-Length: 268

{"password": "**************",
"username": "**************",
"grant_type": "password",
"scope": "internal",
"client_id": "****************************************",
"expires_in": 86400,
"device_token":
"********-****-****-****-************", "challenge_type": "sms"}

aamazie/Robinhood:

POST /oauth2/token/ HTTP/1.1
Host: api.robinhood.com
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5
Content-Type: application/x-www-form-urlencoded; charset=utf-8
X-Robinhood-API-Version: 1.0.0
Connection: keep-alive
User-Agent: Robinhood/823 (iPhone; iOS 7.1.2; Scale/2.00)
Content-Length: 221

password=**************
username=**************
grant_type=password
client_id=****************************************
expires_in=86400
scope=internal
device_token=********-****-****-****-************
challenge_type=sms
doctorcolossus commented 5 years ago

A-ha! I think I've figured it out. I just noticed that aamazie/Robinhood's request is also met with an HTTP/1.1 400 Bad Request response! He's using a:

try:
  res = self.session.post(endpoints.login(), data=payload, timeout=15)
  ...
  res2 = self.session.post(sms_challenge_endpoint, data=challenge_res, timeout=15)
  res2.raise_for_status()
  res3 = self.session.post(endpoints.login(), data=payload, timeout=15)
  res3.raise_for_status()
except requests.exceptions.HTTPError:
  raise RH_exception.LoginFailed()

You're doing:

try:
    resp = session.request(method, url, json=json_payload, timeout=timeout)
    resp.raise_for_status()
except Exception as e:
    ...
    raise e

In other words, we should expect a "bad request" response the first time, when the challenge is issued. Then we need to set the X-ROBINHOOD-CHALLENGE-RESPONSE-ID header, respond to the challenge, and finally attempt a second login. Because you are calling resp.raise_for_status() for every request, it's causing the error to be raised and the program to terminate before the user can be prompted to provide the SMS code. You also need to add an endpoint for the challenge, send it a POST request after the initial failed login, and finally make the second login request.

Dalton2264 commented 4 years ago

Hey doctorcolossus/wang-ye, did you ever find a solution to the SMS issue?

I am having the same issue with the LoginException, where I am not prompted to input my MFA prompt. I still get the SMS message from robinhood, but I get the login error at line 187 before I can input the code.

Thanks

doctorcolossus commented 4 years ago

I described what needs to be changed in my last comment - handle or ignore the first one (or two? iiirc) HTTP "400 Bad Request" responses. I am not sure when I'll have time to work on a fix, as I'm traveling now and busy with another project, then starting a new semester at school in a couple of weeks. I'm hoping wang-ye will find time, but if he doesn't and I find some time during next semester, I will have a go at it and report back here.

CodeMasterFelipe commented 4 years ago

I had the same problem with the login exception. I was getting the SMS from Robinhood and everything you guys are describing. But still, I wasn't prompt to input my MFA. What I figure was that the Robinhood Two-Factor Authentication was off in my setting, even though I was getting SMS. When I turned that setting on in Robinhood the login started working and asking for the MFA. It is counter-intuitive because I was getting the SMS from Robinhood. I hope that helps some of you guys.