From 9f1decd4c6e27efe9f7fe3dfc15704065777e753 Mon Sep 17 00:00:00 2001 From: Christopher Allford Date: Fri, 19 Jun 2020 12:08:48 -0700 Subject: [PATCH] Added an interceptor to handle WooCommerce API authentication --- tests/e2e/factories/package-lock.json | 175 +++++++++++++++++- tests/e2e/factories/package.json | 11 +- .../src/http/api-auth-interceptor.spec.ts | 76 ++++++++ .../src/http/api-auth-interceptor.ts | 77 ++++++++ tests/e2e/factories/src/index.js | 0 tests/e2e/factories/tsconfig.build.json | 12 ++ tests/e2e/factories/tsconfig.json | 9 +- 7 files changed, 349 insertions(+), 11 deletions(-) create mode 100644 tests/e2e/factories/src/http/api-auth-interceptor.spec.ts create mode 100644 tests/e2e/factories/src/http/api-auth-interceptor.ts create mode 100644 tests/e2e/factories/src/index.js create mode 100644 tests/e2e/factories/tsconfig.build.json diff --git a/tests/e2e/factories/package-lock.json b/tests/e2e/factories/package-lock.json index d8190bc5df9..b2c8af6f674 100644 --- a/tests/e2e/factories/package-lock.json +++ b/tests/e2e/factories/package-lock.json @@ -711,6 +711,15 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/create-hmac": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/create-hmac/-/create-hmac-1.1.0.tgz", + "integrity": "sha512-BNYNdzdhOZZQWCOpwvIll3FSvgo3e55Y2M6s/jOY6TuOCwqt3cLmQsK4tSmJ5fayDot8EG4k3+hcZagfww9JlQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/graceful-fs": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", @@ -755,6 +764,15 @@ "pretty-format": "^25.2.1" } }, + "@types/moxios": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@types/moxios/-/moxios-0.4.9.tgz", + "integrity": "sha512-Sd1b24QRW2N194j2LEDPQAZK1h0TBtpN+2EIH+rERCgm38qm14JZwC7NlpE7n3jULhlCIPZBG8uNcbjF8KcCaQ==", + "dev": true, + "requires": { + "axios": "^0.19.0" + } + }, "@types/node": { "version": "13.13.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.5.tgz", @@ -975,6 +993,14 @@ "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", "dev": true }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, "babel-jest": { "version": "25.5.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.5.1.tgz", @@ -1239,6 +1265,15 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -1363,6 +1398,31 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -1837,6 +1897,29 @@ "path-exists": "^4.0.0" } }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -2025,6 +2108,23 @@ } } }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "hosted-git-info": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", @@ -2101,8 +2201,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ip-regex": { "version": "2.1.0", @@ -3076,6 +3175,16 @@ "object-visit": "^1.0.0" } }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3158,6 +3267,12 @@ "minimist": "^1.2.5" } }, + "moxios": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/moxios/-/moxios-0.4.0.tgz", + "integrity": "sha1-/A2ixlR31yXKa5Z51YNw7QxS9Ts=", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3262,6 +3377,11 @@ "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", "dev": true }, + "oauth-1.0a": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz", + "integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==" + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -3568,6 +3688,16 @@ "type-fest": "^0.8.1" } }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "realpath-native": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", @@ -3724,6 +3854,15 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -3733,8 +3872,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", @@ -3936,6 +4074,15 @@ } } }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -4276,6 +4423,21 @@ } } }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -4578,6 +4740,11 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", diff --git a/tests/e2e/factories/package.json b/tests/e2e/factories/package.json index 7ebf5db34e8..a731c271a4b 100644 --- a/tests/e2e/factories/package.json +++ b/tests/e2e/factories/package.json @@ -16,14 +16,21 @@ "main": "dist/index.js", "sideEffects": false, "scripts": { - "build": "tsc", + "build": "tsc -p tsconfig.build.json", "test": "jest" }, - "dependencies": {}, + "dependencies": { + "axios": "0.19.2", + "create-hmac": "1.1.7", + "oauth-1.0a": "2.2.6" + }, "devDependencies": { + "@types/create-hmac": "1.1.0", "@types/jest": "25.2.1", + "@types/moxios": "0.4.9", "@types/node": "13.13.5", "jest": "25.5.4", + "moxios": "0.4.0", "ts-jest": "25.5.0", "typescript": "3.8.3" } diff --git a/tests/e2e/factories/src/http/api-auth-interceptor.spec.ts b/tests/e2e/factories/src/http/api-auth-interceptor.spec.ts new file mode 100644 index 00000000000..18bb0eb5a15 --- /dev/null +++ b/tests/e2e/factories/src/http/api-auth-interceptor.spec.ts @@ -0,0 +1,76 @@ +import axios, { AxiosInstance } from 'axios'; +import moxios from 'moxios'; +import { APIAuthInterceptor } from './api-auth-interceptor'; + +describe( 'APIAuthInterceptor', () => { + let apiAuthInterceptor: APIAuthInterceptor; + let axiosInstance: AxiosInstance; + + beforeEach( () => { + axiosInstance = axios.create(); + moxios.install( axiosInstance ); + apiAuthInterceptor = new APIAuthInterceptor( + axiosInstance, + 'consumer_key', + 'consumer_secret' + ); + apiAuthInterceptor.start(); + } ); + + afterEach( () => { + apiAuthInterceptor.stop(); + moxios.uninstall( axiosInstance ); + } ); + + it( 'should not run unless started', async () => { + moxios.stubRequest( 'https://api.test', { status: 200 } ); + + apiAuthInterceptor.stop(); + await axiosInstance.get( 'https://api.test' ); + + let request = moxios.requests.mostRecent(); + expect( request.headers ).not.toHaveProperty( 'Authorization' ); + + apiAuthInterceptor.start(); + await axiosInstance.get( 'https://api.test' ); + + request = moxios.requests.mostRecent(); + expect( request.headers ).toHaveProperty( 'Authorization' ); + } ); + + it( 'should use basic auth for HTTPS', async () => { + moxios.stubRequest( 'https://api.test', { status: 200 } ); + await axiosInstance.get( 'https://api.test' ); + + const request = moxios.requests.mostRecent(); + + expect( request.headers ).toHaveProperty( 'Authorization' ); + expect( request.headers.Authorization ).toEqual( + 'Basic ' + btoa( 'consumer_key:consumer_secret' ) + ); + } ); + + it( 'should use OAuth 1.0a for HTTP', async () => { + moxios.stubRequest( 'http://api.test', { status: 200 } ); + await axiosInstance.get( 'http://api.test' ); + + const request = moxios.requests.mostRecent(); + + expect( request.headers ).toHaveProperty( 'Authorization' ); + expect( request.headers.Authorization ).toMatch( /^OAuth / ); + const header = request.headers.Authorization; + + // We're going to assume that the oauth-1.0a package added the signature data correctly so we will + // focus on ensuring that the header looks roughly correct given what we readily know. + const oauthArgs: any = {}; + for ( const arg of header.matchAll( /([A-Za-z0-9_]+)="([^"]+)"/g ) ) { + oauthArgs[ arg[ 1 ] ] = arg[ 2 ]; + } + + expect( oauthArgs ).toMatchObject( { + oauth_consumer_key: 'consumer_key', + oauth_signature_method: 'HMAC-SHA256', + oauth_version: '1.0', + } ); + } ); +} ); diff --git a/tests/e2e/factories/src/http/api-auth-interceptor.ts b/tests/e2e/factories/src/http/api-auth-interceptor.ts new file mode 100644 index 00000000000..7a6ff2cc8a7 --- /dev/null +++ b/tests/e2e/factories/src/http/api-auth-interceptor.ts @@ -0,0 +1,77 @@ +import { AxiosInstance, AxiosRequestConfig } from 'axios'; +import createHmac from 'create-hmac'; +import OAuth from 'oauth-1.0a'; + +/** + * A utility class for managing the lifecycle of an authentication interceptor. + */ +export class APIAuthInterceptor { + private readonly client: AxiosInstance; + private interceptorID: number | null; + private oauth: OAuth; + + public constructor( + client: AxiosInstance, + consumerKey: string, + consumerSecret: string + ) { + this.client = client; + this.interceptorID = null; + this.oauth = new OAuth( { + consumer: { + key: consumerKey, + secret: consumerSecret, + }, + signature_method: 'HMAC-SHA256', + hash_function: ( base: any, key: any ) => { + return createHmac( 'sha256', key ) + .update( base ) + .digest( 'base64' ); + }, + } ); + } + + /** + * Starts adding WooCommerce API authentication details to requests made using the contained client. + */ + public start(): void { + if ( null === this.interceptorID ) { + this.interceptorID = this.client.interceptors.request.use( + ( request ) => this.handleRequest( request ) + ); + } + } + + /** + * Stops adding WooCommerce API authentication details to requests made using the contained client. + */ + public stop(): void { + if ( null !== this.interceptorID ) { + this.client.interceptors.request.eject( this.interceptorID ); + this.interceptorID = null; + } + } + + /** + * Adds WooCommerce API authentication details to the outgoing request. + * + * @param {AxiosRequestConfig} request + */ + protected handleRequest( request: AxiosRequestConfig ): AxiosRequestConfig { + if ( request.url!.startsWith( 'https' ) ) { + request.auth = { + username: this.oauth.consumer.key, + password: this.oauth.consumer.secret, + }; + } else { + request.headers.Authorization = this.oauth.toHeader( + this.oauth.authorize( { + url: request.url!, + method: request.method!, + } ) + ).Authorization; + } + + return request; + } +} diff --git a/tests/e2e/factories/src/index.js b/tests/e2e/factories/src/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/e2e/factories/tsconfig.build.json b/tests/e2e/factories/tsconfig.build.json new file mode 100644 index 00000000000..abc2742f81f --- /dev/null +++ b/tests/e2e/factories/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "incremental": true, + "noEmit": false + }, + "exclude": [ + "node_modules", + "**/*.spec.ts", + "**/*.test.ts" + ] +} diff --git a/tests/e2e/factories/tsconfig.json b/tests/e2e/factories/tsconfig.json index 60df497a1dd..0bb65499684 100644 --- a/tests/e2e/factories/tsconfig.json +++ b/tests/e2e/factories/tsconfig.json @@ -1,17 +1,16 @@ { "compilerOptions": { - "incremental": true, "target": "es2015", "module": "commonjs", - "types": [ "node", "jest" ], + "types": [ "node", "jest", "axios", "moxios", "create-hmac" ], "outDir": "dist", "declaration": true, "strict": true, - "esModuleInterop": true + "esModuleInterop": true, + "noEmit": true }, "include": [ "./src/**/*" ], "exclude": [ - "**/*.spec.ts", - "**/*.test.ts" + "node_modules" ] }