Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 63 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,44 @@ Promise.all([
)
```

## Using Oauth2.0

#### We strongly recommend using this beacuse of higher level of security. Reference to working of [oauth2](https://oauth.net/getting-started/).

In this example, we create login using oauth2.0.
1. Initialise the splitwise instance with `useOauth2:true`. You will alse need a `redirect_uri` for oauth2. This will be the same as the one you used while creating your app here https://secure.splitwise.com/oauth_clients.

```
const Splitwise = require('splitwise');
const sw = Splitwise({
consumerKey: 'your key here',
consumerSecret: 'your secret here',
useOauth2: true,
redirect_uri: 'your redirect_uri'
});
```
2. Get the authorization url before calling any APIs using `getAuthorizationUrl()`.
```
const authUrl = sw.getAuthorizationUrl();
```
This url will contain a login page returned by splitwise. This url will also contain the `redirect_uri`. This should match with the one you used while registering your app. After the user logs in, splitwise will call your `callback` url that you mentioned while creating your app. This will contain a state and a code in the query params.

3. Get the access token from the auth code using your callback API. Use the returned code and state to get the access token.
```
app.get('/callback', (req, res) => {
return sw.getAccessToken(req.query.code, req.query.state)
.then(accessToken => {
return res.json({status: 200, accessToken});
})
.catch(err => {
return res.json({status: 500, error: err});
})
});
```

Now your access token is registered and it will be used while calling any APIs. You can call library APIs after this like the examples below.


## API Reference

### `const sw = Splitwise({...})`
Expand All @@ -73,6 +111,8 @@ This is the entry point to the package. All of the other methods are in the form
|-|-|-|
| `consumerKey` | **yes** | Obtained by registering your application |
| `consumerSecret` | **yes** | Obtained by registering your application |
| `useOauth2` | no | Whether to use oauth2.0 to authenticate |
| `redirect_uri` | no | Required for `useOauth2`. |
| `accessToken` | no | Re-use an existing access token |
| `logger` | no | Will be called with info and error messages |
| `logLevel` | no | Set to `'error'` to only see error messages |
Expand Down Expand Up @@ -171,29 +211,29 @@ sw.verbResource({
Without further ado, here is the list of all available methods. In order to see the specifics of which parameters should be passed in, and which data can be expected in response, please refer to the [official API documentation](http://dev.splitwise.com/), or click on the method in question.

- [`sw.test()`](http://dev.splitwise.com/dokuwiki/doku.php?id=test)
- [`sw.getCurrencies()`](http://dev.splitwise.com/dokuwiki/doku.php?id=get_currencies)
- [`sw.getCategories()`](http://dev.splitwise.com/dokuwiki/doku.php?id=get_categories)
- [`sw.parseSentence()`](http://dev.splitwise.com/dokuwiki/doku.php?id=parse_sentence)
- [`sw.getCurrentUser()`](http://dev.splitwise.com/dokuwiki/doku.php?id=get_current_user)
- [`sw.getUser()`](http://dev.splitwise.com/dokuwiki/doku.php?id=get_user)
- [`sw.updateUser()`](http://dev.splitwise.com/dokuwiki/doku.php?id=update_user)
- [`sw.getGroups()`](http://dev.splitwise.com/dokuwiki/doku.php?id=get_groups)
- [`sw.getGroup()`](http://dev.splitwise.com/dokuwiki/doku.php?id=get_group)
- [`sw.createGroup()`](http://dev.splitwise.com/dokuwiki/doku.php?id=create_group)
- [`sw.deleteGroup()`](http://dev.splitwise.com/dokuwiki/doku.php?id=delete_group)
- [`sw.addUserToGroup()`](http://dev.splitwise.com/dokuwiki/doku.php?id=add_user_to_group)
- [`sw.removeUserFromGroup()`](http://dev.splitwise.com/dokuwiki/doku.php?id=remove_user_from_group)
- [`sw.getExpenses()`](http://dev.splitwise.com/dokuwiki/doku.php?id=get_expenses)
- [`sw.getExpense()`](http://dev.splitwise.com/dokuwiki/doku.php?id=get_expense)
- [`sw.createExpense()`](http://dev.splitwise.com/dokuwiki/doku.php?id=create_expense)
- [`sw.updateExpense()`](http://dev.splitwise.com/dokuwiki/doku.php?id=update_expense)
- [`sw.deleteExpense()`](http://dev.splitwise.com/dokuwiki/doku.php?id=delete_expense)
- [`sw.getFriends()`](http://dev.splitwise.com/dokuwiki/doku.php?id=get_friends)
- [`sw.getFriend()`](http://dev.splitwise.com/dokuwiki/doku.php?id=get_friend)
- [`sw.createFriend()`](http://dev.splitwise.com/dokuwiki/doku.php?id=create_friend)
- [`sw.createFriends()`](http://dev.splitwise.com/dokuwiki/doku.php?id=create_friends)
- [`sw.deleteFriend()`](http://dev.splitwise.com/dokuwiki/doku.php?id=delete_friend)
- [`sw.getNotifications()`](http://dev.splitwise.com/dokuwiki/doku.php?id=get_notifications)
- [`sw.getCurrencies()`](https://dev.splitwise.com/#tag/other/paths/~1get_currencies/get)
- [`sw.getCategories()`](https://dev.splitwise.com/#tag/other/paths/~1get_categories/get)
- [`sw.parseSentence()`](https://dev.splitwise.com/#tag/other/paths/~1parse_sentence/post)
- [`sw.getCurrentUser()`](https://dev.splitwise.com/#tag/users/paths/~1get_current_user/get)
- [`sw.getUser()`](https://dev.splitwise.com/#tag/users/paths/~1get_user~1{id}/get)
- [`sw.updateUser()`](https://dev.splitwise.com/#tag/users/paths/~1update_user~1{id}/post)
- [`sw.getGroups()`](https://dev.splitwise.com/#tag/groups/paths/~1get_groups/get)
- [`sw.getGroup()`](https://dev.splitwise.com/#tag/groups/paths/~1get_group~1{id}/get)
- [`sw.createGroup()`](https://dev.splitwise.com/#tag/groups/paths/~1create_group/post)
- [`sw.deleteGroup()`](https://dev.splitwise.com/#tag/groups/paths/~1delete_group~1{id}/post)
- [`sw.addUserToGroup()`](https://dev.splitwise.com/#tag/groups/paths/~1add_user_to_group/post)
- [`sw.removeUserFromGroup()`](https://dev.splitwise.com/#tag/groups/paths/~1remove_user_from_group/post)
- [`sw.getExpenses()`](https://dev.splitwise.com/#tag/expenses/paths/~1get_expenses/get)
- [`sw.getExpense()`](https://dev.splitwise.com/#tag/expenses/paths/~1get_expense~1{id}/get)
- [`sw.createExpense()`](https://dev.splitwise.com/#tag/expenses/paths/~1create_expense/post)
- [`sw.updateExpense()`](https://dev.splitwise.com/#tag/expenses/paths/~1update_expense~1{id}/post)
- [`sw.deleteExpense()`](https://dev.splitwise.com/#tag/expenses/paths/~1delete_expense~1{id}/post)
- [`sw.getFriends()`](https://dev.splitwise.com/#tag/friends/paths/~1get_friends/get)
- [`sw.getFriend()`](https://dev.splitwise.com/#tag/friends/paths/~1get_friend~1{id}/get)
- [`sw.createFriend()`](https://dev.splitwise.com/#tag/friends/paths/~1create_friend/post)
- [`sw.createFriends()`](https://dev.splitwise.com/#tag/friends/paths/~1create_friends/post)
- [`sw.deleteFriend()`](https://dev.splitwise.com/#tag/friends/paths/~1delete_friend~1{id}/post)
- [`sw.getNotifications()`](https://dev.splitwise.com/#tag/notifications/paths/~1get_notifications/get)
- `sw.getMainData()`

**NOTE**: Splitwise makes some important notes about their API that booleans and nested parameters don't work. You won't need to worry about this. That is, instead of calling:
Expand Down
92 changes: 82 additions & 10 deletions src/splitwise.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { OAuth2 } = require('oauth');
const querystring = require('querystring');
const crypto = require("crypto");
const { promisify } = require('es6-promisify');
const validate = require('validate.js');

Expand Down Expand Up @@ -217,6 +218,12 @@ const METHODS = {
propName: PROP_NAMES.NOTIFICATIONS,
paramNames: ['updated_after', 'limit'],
},
CREATE_COMMENT: {
endpoint: 'create_comment',
methodName: 'createComment',
verb: METHOD_VERBS.POST,
paramNames: ['expense_id', 'content'],
},
GET_MAIN_DATA: {
endpoint: 'get_main_data',
methodName: 'getMainData',
Expand Down Expand Up @@ -517,7 +524,7 @@ const getEndpointMethodGenerator = (logger, accessTokenPromise, defaultIDs, oaut

let url = `${endpoint}/${id}`;
// Get the access token
let resultPromise = accessTokenPromise;
let resultPromise = accessTokenPromise();

resultPromise.then(
() => {
Expand Down Expand Up @@ -600,7 +607,13 @@ const getEndpointMethodGenerator = (logger, accessTokenPromise, defaultIDs, oaut
*/
class Splitwise {
constructor(options = {}) {
const { consumerKey, consumerSecret, accessToken } = options;

const { consumerKey, consumerSecret, accessToken, useOauth2 = false, redirect_uri = null } = options;
const SPLITWISE_ENDPOINTS = {
"baseUrl": 'https://secure.splitwise.com/',
"authorizeUrl": 'oauth/authorize',
"accessTokenUrl": 'oauth/token',
}
const defaultIDs = {
groupID: options.group_id,
userID: options.user_id,
Expand All @@ -614,28 +627,79 @@ class Splitwise {
logger({ level: LOG_LEVELS.ERROR, message });
throw new Error(message);
}
// check if no redirect url supplied to useOauth2
if (useOauth2 && !redirect_uri) {
const message = 'Redirect url required for OAuth2';
logger({ level: LOG_LEVELS.ERROR, message });
throw new Error(message);
}

const oauth2 = new OAuth2(
consumerKey,
consumerSecret,
'https://secure.splitwise.com/',
null,
'oauth/token',
SPLITWISE_ENDPOINTS.baseUrl,
useOauth2 ? SPLITWISE_ENDPOINTS.authorizeUrl : null,
SPLITWISE_ENDPOINTS.accessTokenUrl,
null
);

const accessTokenPromise = (() => {
const generateState = () => {
this.state = crypto.randomBytes(20).toString('hex');
return this.state;
}

this.getAuthorizationUrl = () => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would probably do something like adding state as an argument to this method

if (!useOauth2) return "";
return oauth2.getAuthorizeUrl({
redirect_uri,
scope: '',
state: generateState(),
response_type: 'code'
});
}

const verifyState = state => {
if (!useOauth2) return true;
return state === this.state;
}

const getAccessTokenFromAuthCode = () => {
return new Promise((resolve, reject) => {
if(!this.authCode)
return reject(`No auth code generated yet. Visit ${this.getAuthorizationUrl()} and login. You need a callback url in your project as mentioned in callback url in your registered splitwise app. Then call \`getAccessToken()\` to register the access token.`);
if(this.accessToken) return resolve(this.accessToken);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry, I realize now this might not work either. What we have to remember is that with the client_credentials flow, the client will be receiving authorization on behalf of exactly one person (the person owning the credentials) so it makes sense to store data on this, however in the authorization_code flow, we have 1 client <=> 1 instance of this library <=> many users, which means that there isn't just one access token, there are many. This line here would give away the access token of the first user who authenticates to whoever comes next, a security problem! In general, we should probably entirely avoid using this for any code that handles authorization_code flow

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@keriwarr Yaa I thought about this. With this, our clients will need to use the authorization_code to allow multiple users interaction. Because, with client_credentials, client can just use his/her account only. As it logs into his own account be default. So authorization_code would become mandatory in case of multiple users.
Like wise, talking about the state, it should be supplied in getAuthorizationUrl and then once logged in, we would give away the access token and we wont have track of it at all as multiple users would be interacting with the client.
So, summing up,

  1. We can remove the use of this but we wont have the access_token and then the client will have to supply the access_token for each function. Not a good idea
  2. Client can generate and share the state and we wont have to deal with it at all. Better for both, client as well as the library.
  3. Else, to track the state, we can have a redis connection to persist the state. But to which user does it belong to?

Also, as per you, will this library be used for a project of multiple users?

return oauth2.getOAuthAccessToken(this.authCode, {
code: this.authCode,
redirect_uri,
grant_type: 'authorization_code'
}, (err, accessToken) => {
if (err) {
return reject(err);
}
oauth2.useAuthorizationHeaderforGET(true);
this.accessToken = accessToken;
return resolve(true);
})
})
}


const accessTokenPromise = () => {
if (accessToken) {
logger({ message: 'using provided access token' });
return Promise.resolve(accessToken);
}
logger({ message: 'making request for access token' });
return getAccessTokenPromise(logger, oauth2);
})();
};
if (!useOauth2) {
// earlier an IIFE. But now, this is called only in case of client_credentials
accessTokenPromise();
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code would really benefit from having a clear separation between the client credentials code and the authorization code code. maybe something like a switch statement over the new grantType argument which contains the relevant pieces.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Called by default while initializing so as to acquire a token right away. This is just a single case. We cant get the token right away in case of authorization_code


const generateEndpointMethod = getEndpointMethodGenerator(
logger,
accessTokenPromise,
useOauth2 ? getAccessTokenFromAuthCode : accessTokenPromise,
defaultIDs,
oauth2
);
Expand All @@ -645,8 +709,16 @@ class Splitwise {
R.values(METHODS).forEach((method) => {
this[method.methodName] = generateEndpointMethod(method);
});

this.getAccessToken = () => accessTokenPromise;
if (useOauth2) {
this.getAccessToken = (code, state) => {
if (!verifyState(state))
return Promise.reject(`State verification failed: ${state}`);
this.authCode = code;
return getAccessTokenFromAuthCode();
}
}
else
this.getAccessToken = () => accessTokenPromise;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would this still work sinceit's not an IIFE anymore?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this works perfectly

}

// Bonus utility method for easily making transactions from one person to one person
Expand Down