robinhood-unofficial / pyrh

Python Framework to make trades with the unofficial Robinhood API
https://pyrh.readthedocs.io/en/latest/
MIT License
1.79k stars 603 forks source link

login issue, 400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/ #176

Closed hz2018tv closed 5 years ago

hz2018tv commented 5 years ago

It happened last night (04/25/2019) when I restarted my client app and has been like this since. Anyone else having the same issue? Thanks

aamazie commented 5 years ago

I'm not having any issues with that.

hz2018tv commented 5 years ago

Just reinstall the latest repo, same problem. not sure if it is account-related? do we need any new user settings, like new user agreements? Thanks

aamazie commented 5 years ago

I use my own repo and it's working fine. My login function is identical to the parent repo, though, so it shouldn't make any difference. Do you mind pasting the code you're running to test it?

hz2018tv commented 5 years ago

I use the example test app from repo and have the same problem.


from Robinhood import Robinhood

Setup

my_trader = Robinhood()

login

my_trader.login(username="", password="")


then it failed with "400" bad request with my real account login and a fake one. So, I think it is not related to account, since the fake user/pass failed the same way.

I turned on debugging, see the logs below


DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.robinhood.com send: 'POST /oauth2/token/ HTTP/1.1\r\nHost: api.robinhood.com\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\nAccept-Encoding: gzip, deflate\r\nX-Robinhood-API-Version: 1.0.0\r\nAccept: /\r\nUser-Agent: Robinhood/823 (iPhone; iOS 7.1.2; Scale/2.00)\r\nConnection: keep-alive\r\nContent-Type: application/x-www-form-urlencoded; charset=utf-8\r\nContent-Length: 100\r\n\r\nusername=user1&password=pass2&client_id=c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS&grant_type=password' reply: 'HTTP/1.1 400 Bad Request\r\n' header: Date: Fri, 26 Apr 2019 19:17:32 GMT header: Content-Type: application/json header: Content-Length: 56 header: Connection: keep-alive header: Server: nginx 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 DEBUG:urllib3.connectionpool:https://api.robinhood.com:443 "POST /oauth2/token/ HTTP/1.1" 400 56 <Response [400]>


It has been working for a long time, just stopped since last night. Did you guys use two-factor login? or just password?

aamazie commented 5 years ago

I'll do more tests, but Robinhood just introduced a new version of Robinhood Gold. Sometimes when they introduce something new, you have to login to their app. Try doing that, see if there are any notifications for you, and try to login with code again.

hz2018tv commented 5 years ago

already did this morning before posting. upgraded to the latest mobile app, logout, relogin, no notifications, (that is why I asked whether there are new user settings, new user agreements,etc.). same error with repo test app, my own app, my own login, fake login. ...

lansurge commented 5 years ago

I'm having the same error. I've tried turning on and off the Web Beta setting, 2 factor auth. Logout/login after each change and still getting the same error when trying to auth via API. Tried in my app and postman (both were working few days ago), but having no luck now.

diwang137 commented 5 years ago

Same error too.

aamazie commented 5 years ago

It's still working for me so I can't help much. Might want to go to https://robinhood.com/login and inspect the auth code. There's an invisibleRecaptchaSiteKey in the JavaScript. Maybe that's new and you're getting blocked out from that? Just a wild hunch.

AbirFaisal commented 5 years ago

I am not using your API I have my own written in Java but I use others API's as references. However like you guys I also started getting login errors.

The server responds with the following from the endpoint "https://api.robinhood.com/oauth2/token/":

{"detail":"This version of Robinhood is no longer supported. Please update your app or use Robinhood for Web to log in to your account."}

So we need to figure out how to get the api to think we are on a newer version.

Also make sure you guys are using HTTP/2 otherwise you will get "400 Bad Request" like someone mentioned earlier. Not sure what the Python equivalent is but if you switch to Java use the new HttpRequest which supports and by default uses HTTP/2.

indigo62018 commented 5 years ago

Same issue here.

{"detail":"This version of Robinhood is no longer supported. Please update your app or use Robinhood for Web to log in to your account."}

And, it depends on account. In my case, this happens only on my main account.

derekshreds commented 5 years ago

I already have it working. I think pretty much everyone is using my code from reddit for the oauth2 system when they snuck that change in last year.

The same login system works, but we have to add some details.

The old dictionary post was this: {"username", username }, {"password", password }, {"grant_type", "password" }, {"client_id", "c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS" },

Add these variables now: {"expires_in", "86400" }, {"scope", "internal" }, {"device_token", !!!device_token!!! }, {"challenge_type", "sms" }

So for device token, you'll have 2 options. You can mine it from robinhood.com/login

ga('create', 'UA-46330882-9', 'robinhood.com', {
    appName: 'Robinhood Web App',
    appVersion: "0.6.291",
    clientId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"        <- this will be the one to mine
});

or alternatively, I wrote code mimicking exactly how they generate it. It's in c#, but you can port it to whatever pretty easily.

public static string GenerateDeviceToken()
{
    List<int> rands = new List<int>();
    var rng = new Random();
    for (int i = 0; i < 16; i++)
    {
        var r = rng.NextDouble();
        double rand = 4294967296.0 * r;
        rands.Add(((int)((uint)rand >> ((3 & i) << 3))) & 255);
    }

    List<string> hex = new List<string>();
    for (int i = 0; i < 256; ++i)
    {
        hex.Add(Convert.ToString(i + 256, 16).Substring(1));
    }

    string id = "";
    for (int i = 0; i < 16; i++)
    {
        id += hex[rands[i]];

        if (i == 3 || i == 5 || i == 7 || i == 9)
        {
            id += "-";
        }
    }

    return id;
}

Make sure you save the device_id you generate unless you wanna get SMS challenged each time you log in. Only regenerate it if your auth_token you have saved stops being valid.

Now, to answer the SMS response/challenge you'll have to face now, which is basically just a forced 2 factor authentication you're gonna get this response:

        public class Challenge
        {
            public string detail;
            public ChallengeInfo challenge;

            public class ChallengeInfo
            {
                public string id;
                public string user;
                public string type;
                public string alternate_type;
                public string status;
                public int remaining_retries;
                public int remaining_attempts;
                public string expires_at;
            }
        }

If "status" == "issued", you're gonna have to save the "id" from the challenge object.

Then you'll send a response to https://api.robinhood.com/challenge/{id}/respond/, where {id} is the id you saved.

The response format you'll send them is just { "response" : number_you_received_on_sms }. It's just a 6 digit number.

They'll respond with:

        public class ChallengeResponse
        {
            public string id;
            public string user;
            public string alternate_type;
            public string status;
            public int remaining_retries;
            public int remaining_attempts;
            public string expires_at;
        }

If "status" == "validated", you're good to go. Make sure you saved the device_token you validated along with the auth_token so you can "hopefully" just keep refreshing it indefinitely. If you go a day without logging in and let the auth_token period lapse or refuse to save these values to use to log in on consecutive app launches, you WILL be forced to redo this two-auth system each time.

Just a minor annoyance, but we can all go back to trading now.

Enjoy!

aamazie commented 5 years ago

@derekshreds I know I speak on everyone's behalf when I say thank you for this. How did you figure this out / where did you find this?

derekshreds commented 5 years ago

@aamazie I spent the morning reverse engineering the web app to figure it out. If you look at the source for robinhood.com/login they link to an App-xxxxxxxxx.js file. It's their web app, but also includes an API client, I just had to waste time undoing the obfuscation to figure out the login changes.

The device_token id generation is basically nonsense as far as I'm concerned, so I'm pretty sure this update was specifically aimed at stopping public API use.

aamazie commented 5 years ago

For some reason I'm still able to log in, but this code should work for just scraping the device_id in Python:

from urllib.request import Request, urlopen
from bs4 import BeautifulSoup as soup
import re

req = Request("https://robinhood.com/login", headers={'User-Agent': 'Mozilla Chrome Safari'})
webpage = urlopen(req).read()
urlopen(req).close()

page_soup = soup(webpage, "lxml")
container = str(page_soup.findAll("script"))

device_token = re.search('clientId: "(.+?)"', container).group(1)
aamazie commented 5 years ago

@hz2018tv , @derekshreds addressed that in the last paragraph.

As I've been saying, I can't test this since my log in function still works, but so far I've coded the beginning of what might work. I need to find a way to see if the device_token is still valid, and then how to interact with the SMS and what it sends back. Could somebody let me know what a response comes back as (I'm sure it's not in C#)? Or someone could take what I'm posting below and build the rest off of it.

self.device_token = ""

def GetDeviceToken():

    req = Request("https://robinhood.com/login", headers={'User-Agent': 'Mozilla Chrome Safari'})
    webpage = urlopen(req).read()
    urlopen(req).close()

    page_soup = soup(webpage, "lxml")
    container = str(page_soup.findAll("script"))

    self.device_token = re.search('clientId: "(.+?)"', container).group(1)

def login(self,
          username,
          password,
          mfa_code=None):
    """Save and test login info for Robinhood accounts
    Args:
        username (str): username
        password (str): password
    Returns:
        (bool): received valid auth token
    """
    self.username = username
    self.password = password

    if self.device_token == "":
        GetDeviceToken()

    #(and/or) if self.device_token is invalid: (need to find a way to check for this)
    #    GetDeviceToken()

    payload = {
        'password': self.password,
        'username': self.username,
        'grant_type': 'password',
        'client_id': self.client_id,
        'expires_in': '86400',
        'scope': 'internal',
        'device_token': self.device_token,
        'challenge_type': 'sms'
    }

    if mfa_code:
        payload['mfa_code'] = mfa_code
    try:
        res = self.session.post(endpoints.login(), data=payload, timeout=15)
        res.raise_for_status()
        data = res.json()
    except requests.exceptions.HTTPError:
        raise RH_exception.LoginFailed()

    if 'mfa_required' in data.keys():           # pragma: no cover
        raise RH_exception.TwoFactorRequired()  # requires a second call to enable 2FA

    if 'access_token' in data.keys() and 'refresh_token' in data.keys():
        self.auth_token = data['access_token']
        self.refresh_token = data['refresh_token']
        self.headers['Authorization'] = 'Bearer ' + self.auth_token
        return True

    return False

Also, I personally feel that mining for the device token is the best way to go about this particular instance. I've been against scraping in the API in the past because it's slow but I figure that it doesn't have to scrape very often if it's saved. Plus, they could change how they calculate/qualify the device token (in the JS it's called client_Id).

hz2018tv commented 5 years ago

I assume,

  1. the device_token is just a string, it does not have to be the one you mine from the login page, you can completely regenerate a brand new one
  2. requests does not work anymore as it does not handle 2.0, so this whole part will need a little overhaul.
aamazie commented 5 years ago

@hz2018tv I didn't see that second question. That's a question for @derekshreds or someone else.

aamazie commented 5 years ago

Your first assumption is right. And Derek coded that in C#.

Your second assumption I think is right.. I'm using urllib.request. It may only be good for Python 3.

And to your 3rd point in your 3-point question, it doesn't need an extra auth_code. It just needs "status" == "validated".

aamazie commented 5 years ago

I know C# so I'll give that custom generation a go.

hz2018tv commented 5 years ago

And to your 3rd point in your 3-point question, it doesn't need an extra auth_code. It just needs "status" == "validated".

All the steps are done to get the "access_token/auth_token", but I am not sure how/where to get it after the success challenge when status is changed from "issued" to "validated". you need this access_token/auth_token for renewal. just the status=="validated" will not be enough, I guess.

aamazie commented 5 years ago

Ok, this should work for the custom device_token:

import random

def GenerateDeviceToken():
    rands = []
    for i in range(0,16):
        r = random.random()
        rand = 4294967296.0 * r
        rands.append((int(rand) >> ((3 & i) << 3)) & 255)

    hexa = []
    for i in range(0,256):
        hexa.append(str(hex(i+256)).lstrip("0x").rstrip("L")[1:])

    id = ""
    for i in range(0,16):
        id += hexa[rands[i]]

        if (i == 3) or (i == 5) or (i == 7) or (i == 9):
            id += "-"

    # self.device_token = id
    print(id) #can get rid of this in API code

GenerateDeviceToken()

@derekshreds I have no idea how you reverse engineered that. You're sick.

hz2018tv commented 5 years ago

@derekshreds , please help me understand the flow. step 1) post to "/oauth2/token" with device_id and challenge_type added. this will trigger sms/email verification and this op will be blocked till validated. step 2) wait till you get the 6-digit verification code, then post it to /challenge/id/respond/, then get the status changed from "issued" to "validated", step 3) now where/when/how to get access_token/auth_token? if you post to oauth2/token site again, it will challenge again I assume. Or step1 is waiting when blocked while doing step2 in a different thread in an async way? Thanks.

diwang137 commented 5 years ago

@aamazie Could you be more specific on how to login with only status=="validated" ? I tried following the steps above but still cannot login... basically same issue with @hz2018tv

AbirFaisal commented 5 years ago

Thanks guys I am able to login now. Still stuck at {"status":"validated"} but thats ok it's something.

Here is the Java equivalent of @derekshreds GenerateDeviceToken() method:

//Thanks
//https://github.com/Jamonek/Robinhood/issues/176#issuecomment-487310801
String generateDeviceToken() {

    List<Integer> rands = new ArrayList<>();
    var rng = new Random();
    for (int i = 0; i < 16; i++) {
        var r = rng.nextDouble();
        double rand = 4294967296.0 * r;
        var a = ((int) rand >> ((3 & i) << 3)) & 255;
        rands.add(a);
    }

    List<String> hex = new ArrayList<>();
    for (int i = 0; i < 256; ++i) {
        var a = Integer.toHexString((i + 256)).substring(1);
        hex.add(a);
    }

    String s = null;
    for (int i = 0; i < 16; i++) {
        s += hex.get(rands.get(i));

        if (i == 3 || i == 5 || i == 7 || i == 9) {
            s += "-";
        }
    }
    return id;
}
derekshreds commented 5 years ago

I'm on it. Gimme a few hours to go through their source again. I was able to log in with my changes until this morning.

derekshreds commented 5 years ago

Alright, so after you get the "status" == "validated", save the challenge_id.

We're gonna repost to login the same way as before, but we're going to add a header:

{ "X-ROBINHOOD-CHALLENGE-RESPONSE-ID" : your_challenge_id }

After this, it'll give you the access_token as usual, and you'll be good to go.

Cheers!

hz2018tv commented 5 years ago

Right on! Now it makes perfect sense. Thanks @derekshreds

hz2018tv commented 5 years ago

how to renew the access_token?

{scope: r, grant_type: "refresh_token", client_id: const_client_id, refresh_token: saved_refresh_token} post the above, no complaints, but after that got "Unauthorized client". @derekshreds

derekshreds commented 5 years ago

Make sure you are also posting username / password

hz2018tv commented 5 years ago

added "username" and "password", same error. Is "scope" still 'internal'? after a succes posting to /oauth2/token, response contains a long "access_token" and a shorter "refresh_token", I used the shorter one for renewal. is that right?

aamazie commented 5 years ago

Anybody have this working? I'd like to see some code or a pull request. My login stopped working this morning. Going to give this a go myself now.

hz2018tv commented 5 years ago

@aamazie I use curl with -H -d to test the login process, now stuck at token renewal. have not jumped into python yet, meanwhile lets see what else changes Robinhood is rolling out.

derekshreds commented 5 years ago

You guys know you can renew it also just by reposting the same stuff you did to get it, right? That's what I always do. You don't have to repost the challenge_id header after the first time though.

hz2018tv commented 5 years ago

@derekshreds good to know. thx. will try when home. was trying to follow the renew() from the js, a little tough for me coz I dunno much about js.

aamazie commented 5 years ago

Ok, so there has to be some leniency going on with their login code. I haven't gotten the old login to break on my local machine, but this morning I was running a test script on a virtual server and it was throwing the login errors after market open. Now it seems to be working again without any changes. I updated my API repo anyway but only up until the SMS challenge.. not sure where to code further with that yet. Not very familiar with cURL. Going to be looking into how to code the Python more unless someone gives me some code first. Either way, you can check my repo for some skeleton code as it stands. As it is, this iteration works for the time being but I'm sure it'll break once the market opens again tomorrow.

Chenzoh12 commented 5 years ago

Has anybody solved this login issue for Python? If so, could you please share some sample code. I have had no luck so far.

aamazie commented 5 years ago

I've been very busy. If someone can help build on the Python I've already written following Derek's code, that would be very appreciated. If not, I'll try to get to it in the next few days.

viaConBodhi commented 5 years ago

FYI...there is some code via https://github.com/westonplatter/fast_arrow/issues/85 that has been found to work. Not production pretty but functional. Also catches/parses email (from gmail) rather than manual SMS. When I get some time I'll try to pretty-fy it.

viaConBodhi commented 5 years ago

So I'm getting an email with a several minute delay sent but I'm getting a 400 return so I can't grab the challenge. I think it may be related to the HTTP/2 mentioned above. Any thoughts on what I'm doing wrong?

` import os import requests import random from fast_arrow.util import get_last_path from fast_arrow.resources.user import User from fast_arrow.resources.account import Account from fast_arrow.exceptions import AuthenticationError from fast_arrow.exceptions import NotImplementedError

CLIENT_ID = "c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS"

HTTP_ATTEMPTS_MAX = 2

class Client(object):

def __init__(self, **kwargs):
    self.options = kwargs
    self.account_id     = None
    self.account_url    = None
    self.access_token   = None
    self.refresh_token  = None
    self.mfa_code       = None
    self.scope          = None
    self.authenticated  = False
    self.device_token   = None
    self.certs = os.path.join(os.path.dirname(__file__), 'ssl_certs/certs.pem')

def authenticate(self):
    '''
    Authenticate using data in `options`
    '''

    if "username" in self.options and "password" in self.options:

        self.login_oauth2(self.options["username"], self.options["password"], self.options.get('mfa_code'))
    elif "access_token" in self.options and "refresh_token" in self.options:
        self.access_token = self.options["access_token"]
        self.refresh_token = self.options["refresh_token"]
        self.__set_account_info()
    else:
        self.authenticated = False
    return self.authenticated

def get(self, url=None, params=None, retry=True):
    '''
    Execute HTTP GET
    '''
    headers = self._gen_headers(self.access_token, url)
    attempts = 1
    while attempts <= HTTP_ATTEMPTS_MAX:
        try:
            res = requests.get(url, headers=headers, params=params, timeout=15, verify=self.certs)
            res.raise_for_status()
            return res.json()
        except requests.exceptions.RequestException as e:
            attempts += 1
            if res.status_code in [400]:
                raise e
            elif retry and res.status_code in [403]:
                self.relogin_oauth2()

def post(self, url=None, payload=None, retry=True):
    '''
    Execute HTTP POST
    '''
    headers = self._gen_headers(self.access_token, url)
    attempts = 1
    while attempts <= HTTP_ATTEMPTS_MAX:
        try:
            print(requests.post(url, headers=headers, data=payload, timeout=15, verify=self.certs))
            res = requests.post(url, headers=headers, data=payload, timeout=15, verify=self.certs)
            res.raise_for_status()
            if res.headers['Content-Length'] == '0':
                return None
            else:
                return res.json()
        except requests.exceptions.RequestException as e:
            attempts += 1
            if res.status_code in [400]:
                raise e
            elif retry and res.status_code in [403]:
                self.relogin_oauth2()

def _gen_headers(self, bearer, url):
    '''
    Generate headders, adding in Oauth2 bearer token if present
    '''
    headers = {
        "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",
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",

    }
    if bearer:
        headers["Authorization"] = "Bearer {0}".format(bearer)
    if url == "https://api.robinhood.com/options/orders/":
        headers["Content-Type"] = "application/json; charset=utf-8"
    return headers

def GenerateDeviceToken(self):
        rands = []
        for i in range(0,16):
            r = random.random()
            rand = 4294967296.0 * r
            rands.append((int(rand) >> ((3 & i) << 3)) & 255)

        hexa = []
        for i in range(0,256):
            hexa.append(str(hex(i+256)).lstrip("0x").rstrip("L")[1:])

        id = ""
        for i in range(0,16):
            id += hexa[rands[i]]

            if (i == 3) or (i == 5) or (i == 7) or (i == 9):
                id += "-"

        return id

def login_oauth2(self, username, password, mfa_code=None):
    '''
    Login using username and password
    '''

    if self.device_token == None:
        self.device_token = self.GenerateDeviceToken()

    data = {
        "grant_type": "password",
        "scope": "internal",
        "client_id": CLIENT_ID,
        "expires_in": 86400,
        "password": password,
        "username": username,
        "challenge_type": 'email',
        "device_token": self.device_token
    }

    if mfa_code is not None:
        data['mfa_code'] = mfa_code
    url = "https://api.robinhood.com/oauth2/token/"
    res = self.post(url, payload=data, retry=False)

    if res is None:
        if mfa_code is None:
            msg = "Client.login_oauth2(). Could not authenticate. Check username and password."
            raise AuthenticationError(msg)
        else:
            msg = "Client.login_oauth2(). Could not authenticate. Check username and password, and enter a valid MFA code."
            raise AuthenticationError(msg)
    elif res.get('mfa_required') is True:
        msg = "Client.login_oauth2(). Could not authenticate. MFA is required."
        raise AuthenticationError(msg)

    self.access_token   = res["access_token"]
    self.refresh_token  = res["refresh_token"]
    self.mfa_code       = res["mfa_code"]
    self.scope          = res["scope"]
    self.__set_account_info()
    return self.authenticated

def __set_account_info(self):
    account_urls = Account.all_urls(self)
    if len(account_urls) > 1:
        msg = "fast_arrow 'currently' does not handle multiple account authentication."
        raise NotImplementedError(msg)
    elif len(account_urls) == 0:
        msg = "fast_arrow expected at least 1 account."
        raise AuthenticationError(msg)
    else:
        self.account_url = account_urls[0]
        self.account_id = get_last_path(self.account_url)
        self.authenticated = True

def relogin_oauth2(self):
    '''
    (Re)login using the Oauth2 refresh token
    '''
    url = "https://api.robinhood.com/oauth2/token/"
    data = {
        "grant_type": "refresh_token",
        "refresh_token": self.refresh_token,
        "scope": "internal",
        "client_id": CLIENT_ID,
        "expires_in": 86400,
    }
    res = self.post(url, payload=data, retry=False)
    self.access_token   = res["access_token"]
    self.refresh_token  = res["refresh_token"]
    self.mfa_code       = res["mfa_code"]
    self.scope          = res["scope"]

def logout_oauth2(self):
    '''
    Logout for given Oauth2 bearer token
    '''
    url = "https://api.robinhood.com/oauth2/revoke_token/"
    data = {
        "client_id": CLIENT_ID,
        "token": self.refresh_token,
    }
    res = self.post(url, payload=data)
    if res == None:
        self.account_id     = None
        self.account_url    = None
        self.access_token   = None
        self.refresh_token  = None
        self.mfa_code       = None
        self.scope          = None
        self.authenticated  = False
        return True
    else:
        raise AuthenticationError("fast_arrow could not log out.")

`

firstlast912 commented 5 years ago

Alright, so after you get the "status" == "validated", save the challenge_id.

We're gonna repost to login the same way as before, but we're going to add a header:

{ "X-ROBINHOOD-CHALLENGE-RESPONSE-ID" : your_challenge_id }

After this, it'll give you the access_token as usual, and you'll be good to go.

Cheers!

Could you please post the

xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Hi,

Thanks a lot for posting this

1) should we use the same logic you have described to generate the client/device id or any random hexadecimal string in this format will work - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ?

2) I am able to get any random id in the above format validated, I also get an email saying that I logged in from US but I am not getting the access token in the next step, which is POSTing the payload along with the challenge id as the header.

tataiermail commented 5 years ago

Login from Python API not working for me today. Tried after 2 weeks or so... Below is the login code:

import robin_stocks as r
login = r.login('xx@xx.com','xxxxxxx')
my_stocks = r.build_holdings()
#print(my_stocks)

Below is the error:

400 Client Error: Bad Request for url: https://api.robinhood.com/oauth2/token/
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-e8d9c6d0d00c> in <module>()
----> 1 login = r.login('xxxxxxx','xxxxxxxxxxx')
      2 my_stocks = r.build_holdings()
      3 #print(my_stocks)

C:\ProgramData\Anaconda3\lib\site-packages\robin_stocks\authentication.py in login(username, password, expiresIn, scope)
     27     }
     28     data = helper.request_post(url,payload)
---> 29     token = 'Bearer {}'.format(data['access_token'])
     30     helper.update_session('Authorization',token)
     31     return(data)

TypeError: 'NoneType' object is not subscriptable
aamazie commented 5 years ago

Sorry guys, I'm back. Busy week.

Using the plan that @derekshreds laid out, I've written code up until I send {'response':'the_sms_code_they_sent'}, but I only have the SMS code on my phone and obviously it changes everytime. Once we get that, we'll be back up and running.

Any thoughts on how to do this the simplest? It doesn't return with their json response and I'm not sure how to get that off my phone.

I assume this is why @wccramer and his forum went for the email route, but it seems the issue with that is the lag in receiving the email.

aamazie commented 5 years ago

After posting the above, I'm realizing that unless @derekshreds or someone else has a magical way to get the SMS response code simply, that going the SMS route is going to be a serious pain. I'm going to try to do this the email way and code for having to wait for the email.

aamazie commented 5 years ago

This is going to mean that we're going to have to give our emails and email passwords and for those of us GMail users, we're going to have to lower security on the accounts we use for this.

The other option is to do this for our phones and write code for if you have an iPhone, Android, etc.....

Even if we were to pull off the Email way, there would still be a lag in automated trading software using this API when having to log back in.

It's lookin like the golden days of the unofficial Robinhood API are over.

I'm pasting the code I used below for the SMS. Convert it to test the Email way by changing the payload challenge_type to "email". If you use this with the Endpoints.py and Excpeptions.py files in the same directory, you'll see what I was working with. The commented out code in the Login function was where I was testing.

import logging
import warnings

from enum import Enum

#External dependencies
from six.moves.urllib.parse import unquote  # pylint: disable=E0401
from six.moves.urllib.request import getproxies  # pylint: disable=E0401
from six.moves import input

import getpass
import requests
import six
import dateutil
import time
import random

#Application-specific imports
import exceptions as RH_exception
import endpoints

class Robinhood:

    def __init__(self):
            self.session = requests.session()
            self.session.proxies = getproxies()
            self.headers = {
                "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)"
            }
            self.session.headers = self.headers
            self.auth_method = self.login_prompt
            self.device_token = ""
            self.challenge_id = ""

    def GenerateDeviceToken(self):
        rands = []
        for i in range(0,16):
            r = random.random()
            rand = 4294967296.0 * r
            rands.append((int(rand) >> ((3 & i) << 3)) & 255)

        hexa = []
        for i in range(0,256):
            hexa.append(str(hex(i+256)).lstrip("0x").rstrip("L")[1:])

        id = ""
        for i in range(0,16):
            id += hexa[rands[i]]

            if (i == 3) or (i == 5) or (i == 7) or (i == 9):
                id += "-"

        self.device_token = id

    def login_prompt(self):  # pragma: no cover
        """Prompts user for username and password and calls login() """

        username = input("Username: ")
        password = getpass.getpass()

        return self.login(username=username, password=password)

    def login(self,
              username,
              password,
              mfa_code=None):
        """Save and test login info for Robinhood accounts
        Args:
            username (str): username
            password (str): password
        Returns:
            (bool): received valid auth token
        """
        self.username = username
        self.password = password

        if self.device_token == "":
            self.GenerateDeviceToken()

        #or if self.device_token is invalid. need to find a way to check for this

        payload = {
            'password': self.password,
            'username': self.username,
            'grant_type': 'password',
            'client_id': "c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS",
            'expires_in': '86400',
            'scope': 'internal',
            'device_token': self.device_token,
            'challenge_type': 'sms'
        }

        if mfa_code:
            payload['mfa_code'] = mfa_code
        try:
            res = self.session.post(endpoints.login(), data=payload, timeout=15)
            print(res.json())
            response_data = res.json()
            self.challenge_id = response_data["challenge"]["id"]
            print(self.challenge_id)
            sms_challenge_endpoint = "https://api.robinhood.com/challenge/{}/respond/".format(self.challenge_id)
            challenge_res = {"response":"wtf"}
            res2 = self.session.post(sms_challenge_endpoint, data=challenge_res, timeout=15)
            print(res2.json())
            # res.raise_for_status()
            # data = res.json()
            # print(data)
        except requests.exceptions.HTTPError:
            raise RH_exception.LoginFailed()

        if 'mfa_required' in data.keys():           # pragma: no cover
            raise RH_exception.TwoFactorRequired()  # requires a second call to enable 2FA

        if 'access_token' in data.keys() and 'refresh_token' in data.keys():
            self.auth_token = data['access_token']
            self.refresh_token = data['refresh_token']
            self.headers['Authorization'] = 'Bearer ' + self.auth_token
            return True

        return False

    def investment_profile(self):
        """Fetch investment_profile """

        res = self.session.get(endpoints.investment_profile(), timeout=15)
        res.raise_for_status()  # will throw without auth
        data = res.json()

        return data

    def instruments(self, stock):
        """Fetch instruments endpoint
            Args:
                stock (str): stock ticker
            Returns:
                (:obj:`dict`): JSON contents from `instruments` endpoint
        """

        res = self.session.get(endpoints.instruments(), params={'query': stock.upper()}, timeout=15)
        res.raise_for_status()
        res = res.json()

        # if requesting all, return entire object so may paginate with ['next']
        if (stock == ""):
            return res

        return res['results']

    def instrument(self, id):
        """Fetch instrument info
            Args:
                id (str): instrument id
            Returns:
                (:obj:`dict`): JSON dict of instrument
        """
        url = str(endpoints.instruments()) + "?symbol=" + str(id)

        try:
            req = requests.get(url, timeout=15)
            req.raise_for_status()
            data = req.json()
        except requests.exceptions.HTTPError:
            raise RH_exception.InvalidInstrumentId()

        return data['results']

    def quote_data(self, stock=''):
        """Fetch stock quote
            Args:
                stock (str): stock ticker, prompt if blank
            Returns:
                (:obj:`dict`): JSON contents from `quotes` endpoint
        """

        url = None

        if stock.find(',') == -1:
            url = str(endpoints.quotes()) + str(stock) + "/"
        else:
            url = str(endpoints.quotes()) + "?symbols=" + str(stock)

        #Check for validity of symbol
        try:
            req = self.session.get(url, headers=self.headers, timeout=15)
            req.raise_for_status()
            data = req.json()
        except requests.exceptions.HTTPError:
            raise RH_exception.InvalidTickerSymbol()

        return data

my_trader = Robinhood()
print(my_trader.login(username="username",password="password"))

For my own purposes I'm not sure I want to run with an API like this with my money. If someone has a better fix or an idea to help, let me know. Hope I was some help. I'm disappointed that Robinhood would alienate all of their quantitative trader customers like this.

aamazie commented 5 years ago

There is the option to have the API request for the SMS code through the console when logging in, but that would mean this API would no longer be suitable to trade totally autonomously because you'd have to input your SMS every day.

HMSS013 commented 5 years ago

There is the option to have the API request for the SMS code through the console when logging in, but that would mean this API would no longer be suitable to trade totally autonomously because you'd have to input your SMS every day.

I struggled with the concept of automating the SMS code... the only thing I could think of was using Google Messages, which allows you to link your desktop to your mobile SMS using a QR Code, you would then receive all your text messages, including the authentication text, on both devices...

it's probably a terrible workaround... but i'm all out of ideas.

aamazie commented 5 years ago

This is what I'm going to do: I've made the API on my repository use the console to input the SMS code. I'm going to clean up the API's code more and it should be ready to go for trading tomorrow morning if all goes well. I'll test it.

In theory, one could use this new API to trade with an algorithm if they signed in every day they were going to trade with it. This should be more reliable than having to wait for a login from an email scrape, which could occur at a bad time.

aamazie commented 5 years ago

@Jamonek it's up to you what you want to do with the parent code

aamazie commented 5 years ago

Nevermind about it being ready for tomorrow. There are issues with how Robinhood will ask for an additional login before placing the first order of the day. I'm going to be testing for a while. Will let you know when I think it's good.