Skip to content

Commit

Permalink
🐟 Support 2FA (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
n3a9 authored Nov 28, 2022
1 parent 36dabd9 commit 7d947db
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 45 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ A NodeJS wrapper for the Venmo API.

## Usage

### Login
### Login / Logout

Call `Venmo.login(username, password)` with username and password to sign in, generate an access key, and automatically set it.
Call `Venmo.login(username, password)` with username and password to sign in, generate an access key, and automatically set it. May require a 2FA code to be input. It will also return the personal access token. To logout, call `Venmo.logout()`. Optionally provide the access token to logout of a specific account with `Venmo.logout(accessToken)`.

### User Information

Expand Down Expand Up @@ -64,7 +64,7 @@ Call `Venmo.getTransactions` with a userID to retrieve a list of transactions in

### Errors

Errors will be thrown if there are invalid credentials (or not passed), or if you hit Venmo's rate limit.
Errors will be thrown if there are invalid credentials (or not passed), or if you hit Venmo's rate limit (quite low).

### Example

Expand All @@ -84,3 +84,7 @@ Venmo.getUserIDfromUsername("username")
Venmo.logout();
});
```

### Practically

You can build a easy server with this package to serve a frontend such as [venmo.lol](https://github.com/pineapplelol/venmo.lol). Note that Venmo's rate limit is quite low, so you should aim to cache results of API calls to prevent duplicate calls.
3 changes: 0 additions & 3 deletions index.js

This file was deleted.

28 changes: 14 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "venmojs",
"version": "0.0.3",
"version": "0.0.4",
"description": "Venmo API wrapper for NodeJS",
"type": "module",
"main": "index.js",
"main": "src/index.js",
"scripts": {
"start": "node index.js"
"start": "node src/index.js"
},
"author": "",
"author": "pineapplelol",
"license": "MIT",
"dependencies": {
"node-fetch": "^3.2.4"
Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Venmo from "./venmo.js";

export default Venmo;
117 changes: 96 additions & 21 deletions lib/venmo.js → src/venmo.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import fetch from "node-fetch";
import readline from "readline";

const baseUrl = "https://api.venmo.com/v1";

// Generates a random device ID
const deviceId = "88884260-05O3-8U81-58I1-2WA76F357GR9"
.replace(/[a-z]/gi, function (letter) {
return String.fromCharCode(Math.floor(Math.random() * 26) + 65);
})
.replace(/[0-9]/gi, function (number) {
return Math.floor(Math.random() * 10);
});

// Headers that are sent with every request
const persistentHeaders = {
"device-id": deviceId,
"Content-Type": "application/json",
};

const Venmo = {
/**
* Gets the access token for the Venmo object.
Expand All @@ -20,38 +36,97 @@ const Venmo = {
},

/**
* Logins given a username and password. Currently does not support
* 2FA (coming soon). This function will automatically set the access token
* for the user.
* Performs a login attempt given a username, password, and optionally 2FA authentication
* credentials. This function does not set the access token for the Venmo object. It can return
* two kinds of values:
* 1. If 2FA is required, it will return {type: '2fa', value: otp_secret}.
* 2. If login is successful, it will return {type: 'accessToken', value: accessToken}.
*
* @param {string} username The username for the Venmo account.
* @param {string} password The password for the Venmo account.
* @returns {Promise} A promise that resolves to the user accessToken.
* @example
* await Venmo.login("pineapplelol", "pineapplesaregreat")
* @returns {Promise} A promise that resolves to a dictionary containing the value type and
* the value (either the 2FA secret or the access token).
**/
login: function (username, password) {
const deviceId = "88884260-05O3-8U81-58I1-2WA76F357GR9"
.replace(/[a-z]/gi, function (letter) {
return String.fromCharCode(Math.floor(Math.random() * 26) + 65);
})
.replace(/[0-9]/gi, function (number) {
return Math.floor(Math.random() * 10);
});

initLogin: async function (username, password, twofaHeaders = {}) {
const resourcePath = baseUrl + "/oauth/access_token";
const header_params = {
"device-id": deviceId,
"Content-Type": "application/json",
Host: "api.venmo.com",
...persistentHeaders,
...twofaHeaders,
};
const body = { phone_email_or_username: username, client_id: "1", password: password };

let type = "",
value = "";
return fetch(resourcePath, { method: "POST", headers: header_params, body: JSON.stringify(body) })
.then((res) => res.json())
.then((json) => {
this.setAccessToken(json.access_token);
return json.access_token;
.then((res) => {
if (res.status == 401) {
console.log("2fa required in initLogin");
type = "2fa";
value = res.headers.get("venmo-otp-secret");
}
return res.json();
})
.then((data) => {
if (type !== "2fa") {
console.log(data);
type = "accessToken";
value = data.access_token;
}
})
.then(() => {
return { type, value };
});
},

/**
* Will send a request to the Venmo API to complete the login process with 2FA.
* @param {string} otpSecret The otp secret returned in the header of the initLogin request.
*/
sendTwoFactor: function (otpSecret) {
const resourcePath = baseUrl + "/account/two-factor/token";
const header_params = {
"venmo-otp-secret": otpSecret,
...persistentHeaders,
};
const body = { via: "sms" };
return fetch(resourcePath, { method: "POST", headers: header_params, body: JSON.stringify(body) }).then(
(res) => {}
);
},

/**
* Will complete an end-to-end login for a user given their username and password. If 2FA is required, it will
* prompt the user for the 2FA code and then complete the login. It will automatically set the access token for
* the Venmo object.
* @param {string} username The username for the Venmo account.
* @param {string} password The password for the Venmo account.
* @returns The personal access token for the user.
*/
login: async function (username, password) {
const loginRes = await this.initLogin(username, password);
let { type, value } = loginRes;

if (type === "2fa") {
console.log("2FA required. Please enter the code sent to your phone.");
this.sendTwoFactor(value);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const code = await new Promise((res) => {
rl.question("Enter OTP code:", (answer) => res(answer));
});
const loginRes = await this.initLogin(username, password, {
"venmo-otp-secret": value,
"venmo-otp": code,
});
value = loginRes.value;
}

this.setAccessToken(value);
console.log("Logged in successfully! Your access token is: " + value);
return value;
},

/**
Expand Down

0 comments on commit 7d947db

Please sign in to comment.