Added an interceptor to handle WooCommerce API authentication

This commit is contained in:
Christopher Allford 2020-06-19 12:08:48 -07:00
parent 5e15271f95
commit 9f1decd4c6
7 changed files with 349 additions and 11 deletions

View File

@ -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",

View File

@ -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"
}

View File

@ -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',
} );
} );
} );

View File

@ -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;
}
}

View File

View File

@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"incremental": true,
"noEmit": false
},
"exclude": [
"node_modules",
"**/*.spec.ts",
"**/*.test.ts"
]
}

View File

@ -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"
]
}