robinhood-observer
Version:
Comprehensive client featuring RxJS Streams and a CLI for Robinhood Free Stock Trading. A drop in replacement for @aurbano obinhood which includes callback, promise and observable support.
301 lines (276 loc) • 9.84 kB
JavaScript
/* eslint-disable */
var RxJS = require('rxjs'),
Rx = require('rx'),
Promise = require("bluebird"),
request = require('request'),
rp = require('request-promise'),
_ = require("lodash"),
fs = require("fs"),
endpoints = require("./endpoints"),
headers = require("./headers"),
Device = require("./device");
const MFAService = require('./mfa');
var _apiUrl = 'https://api.robinhood.com/';
var _clientId = 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS'
class Auth {
headers = headers
_request = request.defaults()
_rp = rp.defaults()
constructor() {
// Set headers before sending any requests
this.setHeaders(this.headers)
// Do auth init and set headers
}
init(_private, device) {
this._private = _private;
this.device = device;
return new Promise((resolve, reject) => {
if(this.device.registered){
// Load device ID and authenticate?
console.log("Device previously registered: ", device.path)
this.headers["X-ROBINHOOD-CHALLENGE-RESPONSE-ID"] = device.challenge.id
this._build_auth_header(device.access_token);
this.setHeaders(this.headers);
_private.headers = this.headers
resolve(_private)
}else{
this.registerTokenWith(this.device, _private.username, _private.password)
.then((body) => {
// If cli
// Wait for 2fa token
return this.collect2fa()
.then(user_input => {
this.headers["X-ROBINHOOD-CHALLENGE-RESPONSE-ID"] = body.challenge.id
return this.respond2faChallenge(user_input, body.challenge.id)
})
})
.then((body) => {
// Check if 2fa succeeded
if(body.status == "validated"){
// Device is now registered.
return this.requestBearerToken(this.device, _private.username, _private.password)
}else if (body.detail == "Challenge response is invalid."){
console.log("The 2FA code you entered was incorrect.")
process.exit(1)
}else{
console.log("UNKNOWN CONDITIION")
}
})
.then((body)=> {
this.device.updateTokens(body)
this._build_auth_header(device.access_token);
this.setHeaders(this.headers);
_private.headers = this.headers
resolve(_private)
})
.catch(err => {
console.error(err)
})
}
})
}
get(options, callback){
if (callback && typeof callback == "function") {
return this._request.get(options, callback)
}else{
return this._rp.get(options)
}
}
post(options, callback){
if (callback && typeof callback == "function") {
return this._request.post(options, callback)
}else{
return this._rp.post(options)
}
}
setHeaders(headers){
this._request = request.defaults({
headers: headers,
json: true,
gzip: true
});
this._rp = rp.defaults({
headers: headers,
json: true,
gzip: true
});
}
_build_auth_header(token) {
this.headers.Authorization = 'Bearer ' + token;
}
// 1. Generate Device Token
// var device = new Device()
// 2. Register Device Token, with User Credentials
registerTokenWith(device, username, password) {
return new Promise((resolve, reject) => {
if(!username || !password){
reject(new Error("Username or Password is undefined, did you export ROBINHOOD_USERNAME and ROBINHOOD_PASSWORD?"))
}else{
this.post(
{
uri: _apiUrl + endpoints.login,
form: {
grant_type: 'password',
scope: 'internal',
client_id: _clientId,
expires_in: 86400,
device_token: device.device_token,
password: password,
username: username,
challenge_type: 'sms'
}
},
(err, httpResponse, body) => {
if (err) {
reject(err);
}else if(body.detail == "Request blocked, challenge issued."){
device.register(body)
resolve(body)
}else if(body.mfa_required == true && body.mfa_type == 'sms') {
console.log('2FA enabled on your account, add mfa token to /path/to/running/directory/2fa.json, with format: {"mfa_code":"YOUR_NUMERIC_MFA_CODE_HERE"}, for example {"mfa_code":"111111"}, then save and close the file')
var svc = new MFAService()
svc.watchFile((type, current, previous, two_fa_json)=>{
console.log(two_fa_json)
if (type != "Timed out.") {
return this.registerTokenWith2FA(this.device, this._private.username, this._private.password, two_fa_json.mfa_code)
}else{
reject(new Error('2FA enabled on your account, timeout occured after 60 seconds for mfa code.'))
}
}, 60000)
} else if(body.detail == "After 5 attempts, you must wait 10 minutes before trying again.") {
reject(new Error(body.detail+': ' + JSON.stringify(httpResponse)));
} else if(body.detail == "Unable to log in with provided credentials.") {
reject(new Error(body.detail+': ' + JSON.stringify(httpResponse)));
}else if (!body.access_token) {
reject(new Error('token not found ' + JSON.stringify(httpResponse)));
} else{
reject(new Error('token found ' + JSON.stringify(httpResponse)));
}
}
);
}
})
}
// 2.5 Register Device Token, with User Credentials, and MFA Code
registerTokenWith2FA(device, username, password, mfa_code) {
return new Promise((resolve, reject) => {
if(!username || !password){
reject(new Error("Username or Password is undefined, did you export ROBINHOOD_USERNAME and ROBINHOOD_PASSWORD?"))
}else{
this.post(
{
uri: _apiUrl + endpoints.login,
form: {
device_token: device.device_token,
client_id: _clientId,
expires_in: 86400,
grant_type: 'password',
scope: 'internal',
password: password,
username: username,
mfa_code: mfa_code
}
},
(err, httpResponse, body) => {
if (err) {
reject(err);
}else if(body.detail == "Request blocked, challenge issued."){
device.register(body)
resolve(body)
}else if(body.mfa_required == true && body.mfa_type == 'sms') {
console.log(body)
// reject(new Error('You must disable 2FA on your account for this to work.'))
} else if(body.detail == "After 5 attempts, you must wait 10 minutes before trying again.") {
reject(new Error(body.detail+': ' + JSON.stringify(httpResponse)));
} else if(body.detail == "Unable to log in with provided credentials.") {
reject(new Error(body.detail+': ' + JSON.stringify(httpResponse)));
}else if (!body.access_token) {
reject(new Error('token not found ' + JSON.stringify(httpResponse)));
} else{
// This means we can now authenticate
// Save token
this.device.registerWithTokens(body)
}
}
);
}
})
}
// 3. Collect User 2FA code via 2fa.json.
// 3. Collect User 2FA code via user input.
collect2fa() {
process.stdin.setEncoding('utf-8');
console.log("Enter the 2FA code that was sent to you via sms.");
return new Promise((resolve, reject) => {
// When user input data and hit enter key.
process.stdin.on('data', function (data) {
if(data){
resolve(data)
}else{
reject(null)
}
});
})
}
// 4. Respond to 2FA challenge with user_input
respond2faChallenge(user_input, challenge_id) {
var sixDigits = new RegExp("\\d{6}");
var self = this
return new Promise((resolve, reject) => {
if(sixDigits.test(user_input)){ // validate format of the sms token
self.post(
{
uri: _apiUrl + "challenge/"+ challenge_id+ "/respond/",
form: { "response" : parseInt(user_input) }
},
function (err, httpResponse, body) {
if (err) {
reject(err)
throw err;
}else{
if (body.detail == "Challenge response is invalid."){
reject(new Error("The 2FA code you entered was incorrect."))
}else{
resolve(body)
}
}
})
}else{
reject(new Error("Invalid User Input: " + user_input))
}
});
}
// 5. Request Bearer Token
requestBearerToken(device, username, password) {
var self = this
return new Promise(function (resolve, reject) {
self.post(
{
uri: _apiUrl + endpoints.login,
form: {
grant_type: 'password',
scope: 'internal',
client_id: _clientId,
expires_in: 86400,
device_token: device.device_token,
password: password,
username: username,
challenge_type: 'sms'
}
},
function (err, httpResponse, body) {
if (err) {
reject(err)
throw err;
}else{
resolve(body)
}
})
})
}
signIn () {
}
signOut () {
}
}
module.exports = Auth;