From 27e00606046c77d3060e1b62b9e46d1d25d0dceb Mon Sep 17 00:00:00 2001 From: Dustin Popp Date: Mon, 23 Mar 2020 14:34:49 -0500 Subject: [PATCH] feat: add optional cookie jar support (#85) 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 --- README.md | 18 ++++++ lib/base-service.ts | 2 + lib/request-wrapper.ts | 13 +++- package-lock.json | 107 ++++++++++++++++++++++++++------- package.json | 1 + test/unit/base-service.test.js | 9 +++ test/unit/cookiejar.test.js | 62 +++++++++++++++++++ 7 files changed, 188 insertions(+), 24 deletions(-) create mode 100644 test/unit/cookiejar.test.js diff --git a/README.md b/README.md index ae925d9f9..0c72423e6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/base-service.ts b/lib/base-service.ts index 6f962b9f9..e683000cd 100644 --- a/lib/base-service.ts +++ b/lib/base-service.ts @@ -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 */ diff --git a/lib/request-wrapper.ts b/lib/request-wrapper.ts index 302619655..2f396a1de 100644 --- a/lib/request-wrapper.ts +++ b/lib/request-wrapper.ts @@ -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'); @@ -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; @@ -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 { diff --git a/package-lock.json b/package-lock.json index 29a632115..e04b484e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1512,6 +1512,11 @@ "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", "dev": true }, + "@types/tough-cookie": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.6.tgz", + "integrity": "sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==" + }, "@types/yargs": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.3.tgz", @@ -1933,6 +1938,34 @@ "is-buffer": "^2.0.2" } }, + "axios-cookiejar-support": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-0.5.1.tgz", + "integrity": "sha512-mmMbNDjpkAKlyxVOYjkpvV6rDRoSjBXwHbfkWvnsplRTGYCergbHvZInRB1G3lqumllUQwo0A4uPoqEsYfzq3A==", + "requires": { + "@types/tough-cookie": "^2.3.3", + "is-redirect": "^1.0.0", + "pify": "^4.0.0", + "tough-cookie": "^3.0.1" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -4951,6 +4984,11 @@ "loose-envify": "^1.0.0" } }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" + }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -5127,6 +5165,11 @@ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", "dev": true }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -6243,6 +6286,18 @@ "whatwg-url": "^6.4.1", "ws": "^5.2.0", "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } } }, "jsesc": { @@ -10974,8 +11029,7 @@ "psl": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", - "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==", - "dev": true + "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==" }, "pump": { "version": "3.0.0", @@ -10990,8 +11044,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "q": { "version": "1.5.1", @@ -11206,6 +11259,22 @@ "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } } } }, @@ -11235,6 +11304,18 @@ "request-promise-core": "1.1.3", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } } }, "require-directory": { @@ -12742,24 +12823,6 @@ "is-number": "^7.0.0" } }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, "tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", diff --git a/package.json b/package.json index ff5a67cef..6cf06690c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/unit/base-service.test.js b/test/unit/base-service.test.js index 1d7c6eebb..375c62d6e 100644 --- a/test/unit/base-service.test.js +++ b/test/unit/base-service.test.js @@ -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, diff --git a/test/unit/cookiejar.test.js b/test/unit/cookiejar.test.js new file mode 100644 index 000000000..71ce4131b --- /dev/null +++ b/test/unit/cookiejar.test.js @@ -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); + }); +});