Skip to content

Commit

Permalink
feat: add optional cookie jar support (#85)
Browse files Browse the repository at this point in the history
If the `jar` field is set om the base service, the axios instance is wrapped in a library that adds cookie support. Values for `jar` can be an instance of cookie jar library or `true`, which will cause a default cookie jar to be created using the `tough-cookie` package.

Co-authored-by: Christian Compton <cbcompto@us.ibm.com>
  • Loading branch information
dpopp07 and Christian Compton committed Mar 23, 2020
1 parent 11836e1 commit 27e0060
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 24 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,24 @@ The debug logger can be configured to be used for more than one library. In exam

``DEBUG=ibm-cloud-sdk-core:debug,other-lib:debug``

## Cookie Jar Support
By default, cookies are not supported in the SDK requests. If your SDK would benefit from this functionality, simply edit your code to instantiate a cookie jar (or instruct your users to do so) and pass it in the object containing configuration options to the `BaseService` class, as shown below.

```ts
import tough = require('tough-cookie');

class MyClass extends BaseService {
constructor(options: MyOptions) {
// pass the cookie jar object or simply pass the value `true`
// and a tough-cookie instance will be created by default
options.jar = new tough.CookieJar();
super(options);
}
}
```

The example above uses [Tough Cookie](https://www.npmjs.com/package/tough-cookie) to provide these capabilities, but other cookie jar libraries can be used.

## Issues
If you encounter an issue with this project, you are welcome to submit a [bug report](https://github.com/IBM/node-sdk-core/issues).
Before opening a new issue, please search for similar issues. It's possible that someone has already reported it.
Expand Down
2 changes: 2 additions & 0 deletions lib/base-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface UserOptions {
version?: string;
/** Set to `true` to allow unauthorized requests - not recommended for production use. */
disableSslVerification?: boolean;
/** Set your own cookie jar object */
jar?: any;
/** Deprecated. Use `serviceUrl` instead. */
url?: string;
/** Allow additional request config parameters */
Expand Down
13 changes: 11 additions & 2 deletions lib/request-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import axios from 'axios';
import axiosCookieJarSupport from 'axios-cookiejar-support';
import extend = require('extend');
import FormData = require('form-data');
import https = require('https');
Expand All @@ -27,7 +28,7 @@ const isBrowser = typeof window === 'object';
const globalTransactionId = 'x-global-transaction-id';

// Limit the type of axios configs to be customizable
const allowedAxiosConfig = ['transformRequest', 'transformResponse', 'paramsSerializer', 'paramsSerializer', 'timeout', 'withCredentials', 'adapter', 'responseType', 'responseEncoding', 'xsrfCookieName', 'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'maxContentLength', 'validateStatus', 'maxRedirects', 'socketPath', 'httpAgent', 'httpsAgent', 'proxy', 'cancelToken'];
const allowedAxiosConfig = ['transformRequest', 'transformResponse', 'paramsSerializer', 'paramsSerializer', 'timeout', 'withCredentials', 'adapter', 'responseType', 'responseEncoding', 'xsrfCookieName', 'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'maxContentLength', 'validateStatus', 'maxRedirects', 'socketPath', 'httpAgent', 'httpsAgent', 'proxy', 'cancelToken', 'jar'];

export class RequestWrapper {
private axiosInstance;
Expand Down Expand Up @@ -67,8 +68,16 @@ export class RequestWrapper {

this.axiosInstance = axios.create(axiosConfig);

// if a cookie jar is provided, wrap the axios instance and update defaults
if (axiosOptions.jar) {
axiosCookieJarSupport(this.axiosInstance);

this.axiosInstance.defaults.withCredentials = true;
this.axiosInstance.defaults.jar = axiosOptions.jar;
}

// set debug interceptors
if(process.env.NODE_DEBUG === 'axios' || process.env.DEBUG) {
if (process.env.NODE_DEBUG === 'axios' || process.env.DEBUG) {
this.axiosInstance.interceptors.request.use(config => {
logger.debug('Request:');
try {
Expand Down
107 changes: 85 additions & 22 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@types/jest": "^24.0.23",
"@types/node": "~10.14.19",
"axios": "^0.18.0",
"axios-cookiejar-support": "^0.5.1",
"camelcase": "^5.3.1",
"debug": "^4.1.1",
"dotenv": "^6.2.0",
Expand Down
9 changes: 9 additions & 0 deletions test/unit/base-service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ describe('Base Service', () => {
expect(testService.baseOptions.disableSslVerification).toBe(true);
});

it('should store a cookie jar when set', () => {
const testService = new TestService({
authenticator: AUTHENTICATOR,
jar: true,
});

expect(testService.baseOptions.jar).toBe(true);
});

it('should default disableSslVerification to false', () => {
const testService = new TestService({
authenticator: AUTHENTICATOR,
Expand Down
62 changes: 62 additions & 0 deletions test/unit/cookiejar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict';

// the `toBeInstanceOf` assertion compares for function reference equality
// importing our own `tough-cookie` dependency would create a different function
// reference and render the assertion unusable. the solution is to use the
// dependency within axios-cookiejar-support
const tough = require('axios-cookiejar-support/node_modules/tough-cookie');
const { RequestWrapper } = require('../../dist/lib/request-wrapper');

describe('cookie jar support', () => {
it('should not wrap the axios instance by default', () => {
const wrapper = new RequestWrapper();
expect(wrapper.axiosInstance.defaults.withCredentials).not.toBeDefined();
expect(wrapper.axiosInstance.interceptors.request.handlers.length).toBe(0);
});

it('passing a value for `jar` should produce interceptors and set flags', () => {
const wrapper = new RequestWrapper({ jar: true });
expect(wrapper.axiosInstance.defaults.withCredentials).toBe(true);
expect(wrapper.axiosInstance.interceptors.request.handlers.length).toBe(1);
});

it('given `true` for `jar`, the interceptors should create an instance of tough-cookie', () => {
const wrapper = new RequestWrapper({ jar: true });

expect(wrapper.axiosInstance.interceptors.request.handlers.length).toBe(1);
expect(wrapper.axiosInstance.interceptors.request.handlers[0].fulfilled).toBeInstanceOf(
Function
);

// should initially set the default to true
expect(wrapper.axiosInstance.defaults.jar).toBe(true);

// invoke the interceptor - it should be the one added by the cookie jar library
// it should see that `jar` is `true` and create a default instance of tough.CookieJar
// this would noramlly happen just before a request is sent
wrapper.axiosInstance.interceptors.request.handlers[0].fulfilled(
wrapper.axiosInstance.defaults
);

expect(wrapper.axiosInstance.defaults.jar).toBeInstanceOf(tough.CookieJar);
});

it('given arbitrary value for `jar`, the interceptor should use it as cookie jar', () => {
// the axios-cookiejar-support interceptor requires the jar object
// to have the method `getCookieString`
const mockCookieJar = { getCookieString: () => 'mock-string' };
const wrapper = new RequestWrapper({ jar: mockCookieJar });

// should still set interceptors and withCredentials flag
expect(wrapper.axiosInstance.interceptors.request.handlers.length).toBe(1);
expect(wrapper.axiosInstance.defaults.withCredentials).toBe(true);
expect(wrapper.axiosInstance.defaults.jar).toEqual(mockCookieJar);

// invoke the interceptor, the default jar should remain the same
wrapper.axiosInstance.interceptors.request.handlers[0].fulfilled(
wrapper.axiosInstance.defaults
);

expect(wrapper.axiosInstance.defaults.jar).toEqual(mockCookieJar);
});
});

0 comments on commit 27e0060

Please sign in to comment.