diff --git a/README.md b/README.md index 3fd8899..2586670 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,56 @@ Security can be specified per path using the `paths.security` field. } ``` +### Authentication & Request Headers + +To retrieve Swagger schemas that are access protected, basic auth information (username and password) or any headers to be sent with the http request can be specified: + +```json +{ + "swagger": "2.0", + "info": { + "title": "Swagger Combine Authentication Example", + "version": "1.0.0" + }, + "apis": [ + { + "url": "http://petstore.swagger.io/v2/swagger.json", + "resolve": { + "http": { + "auth": { + "username": "admin", + "password": "secret12345" + } + } + } + }, + { + "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml", + "resolve": { + "http": { + "headers": { + "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWV9.44lJS0jlltzcglq7vgjXMXYRTecBxseN3Dec_LO_osI" + } + } + } + }, + { + "url": "https://api.apis.guru/v2/specs/deutschebahn.com/betriebsstellen/v1/swagger.json", + "resolve": { + "http": { + "headers": { + "authorization": "Basic YWRtaW46c2VjcmV0MTIz" + } + } + } + } + ] +} +``` + +For all possible resolve options have a look at the [documentation of json-schema-ref-parser](https://github.com/BigstickCarpet/json-schema-ref-parser/blob/master/docs/options.md#resolve-options). + + ## API ### swaggerCombine(config, [options], [callback]) diff --git a/examples/auth.js b/examples/auth.js new file mode 100644 index 0000000..71d77dc --- /dev/null +++ b/examples/auth.js @@ -0,0 +1,49 @@ +const swaggerCombine = require('../src'); + +const config = (module.exports = { + swagger: '2.0', + info: { + title: 'Swagger Combine Authentication Example', + version: { + $ref: './package.json#/version', + }, + }, + apis: [ + { + url: 'http://petstore.swagger.io/v2/swagger.json', + resolve: { + http: { + auth: { + username: 'admin', + password: 'secret12345' + } + } + } + }, + { + url: 'https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml', + resolve: { + http: { + headers: { + authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWV9.44lJS0jlltzcglq7vgjXMXYRTecBxseN3Dec_LO_osI' + } + } + } + }, + { + url: 'https://api.apis.guru/v2/specs/deutschebahn.com/betriebsstellen/v1/swagger.json', + resolve: { + http: { + headers: { + authorization: 'Basic YWRtaW46c2VjcmV0MTIz' + } + } + } + }, + ] +}); + +if (!module.parent) { + swaggerCombine(config).then(res => console.log(JSON.stringify(res, false, 2))).catch(err => console.error(err)); + +} diff --git a/package.json b/package.json index 0ae3c50..929ab81 100644 --- a/package.json +++ b/package.json @@ -45,15 +45,15 @@ "url-join": "^2.0.2" }, "devDependencies": { - "@maxdome/prettier": "^1.2.1", + "@maxdome/prettier": "^1.3.2", "chai": "^4.1.2", "chai-http": "^3.0.0", "chai-somewhere": "^1.0.2", - "express": "^4.15.4", + "express": "^4.16.1", "mocha": "^3.5.0", - "nock": "^9.0.14", + "nock": "^9.0.22", "sinon": "^4.0.0", - "sinon-chai": "^2.13.0" + "sinon-chai": "^2.14.0" }, "directories": { "example": "examples" diff --git a/src/SwaggerCombine.js b/src/SwaggerCombine.js index 9fd3015..457bfdb 100644 --- a/src/SwaggerCombine.js +++ b/src/SwaggerCombine.js @@ -41,15 +41,28 @@ class SwaggerCombine { this.combinedSchema = _.omit(configSchema, 'apis'); return Promise.all( - this.apis.map((api, idx) => - SwaggerParser.dereference(api.url, this.opts).catch(err => { - if (this.opts.continueOnError) { - return; - } + this.apis.map((api, idx) => { + const opts = _.cloneDeep(this.opts); + opts.resolve = Object.assign({}, opts.resolve, api.resolve); + + if (_.has(opts, 'resolve.http.auth.username') && _.has(opts, 'resolve.http.auth.password')) { + const basicAuth = + 'Basic ' + + new Buffer(`${opts.resolve.http.auth.username}:${opts.resolve.http.auth.password}`).toString('base64'); + _.set(opts, 'resolve.http.headers.authorization', basicAuth); + } - throw err; - }) - ) + return $RefParser + .dereference(api.url, opts) + .then(res => SwaggerParser.dereference(res, opts)) + .catch(err => { + if (this.opts.continueOnError) { + return; + } + + throw err; + }); + }) ); }) .then(apis => { diff --git a/src/middleware.js b/src/middleware.js index 09418f5..ef20331 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -16,15 +16,13 @@ exports.middleware = (config, opts = {}) => { }; exports.middlewareAsync = (config, opts = {}) => { - return new SwaggerCombine(config, opts) - .combine() - .then(sc => { - return function (req, res, next) { - if (opts && (opts.format === 'yaml' || opts.format === 'yml')) { - return res.type('yaml').send(sc.toString()); - } - - res.json(sc.combinedSchema); + return new SwaggerCombine(config, opts).combine().then(sc => { + return function(req, res, next) { + if (opts && (opts.format === 'yaml' || opts.format === 'yml')) { + return res.type('yaml').send(sc.toString()); } - }); + + res.json(sc.combinedSchema); + }; + }); }; diff --git a/test/unit.spec.js b/test/unit.spec.js index 05745fb..f1c92a8 100644 --- a/test/unit.spec.js +++ b/test/unit.spec.js @@ -1,4 +1,5 @@ const chai = require('chai'); +const http = require('http'); const sinon = require('sinon'); chai.use(require('sinon-chai')); @@ -125,6 +126,81 @@ describe('[Unit] SwaggerCombine.js', () => { }); }); + describe('load()', () => { + beforeEach(() => { + sandbox.stub(http, 'get'); + }); + + it('transforms auth to authorization header and sends it on http request', () => { + instance.config = { + apis: [ + { + url: 'http://test/swagger.json', + resolve: { + http: { + auth: { + username: 'admin', + password: 'secret12345', + }, + }, + }, + }, + ], + }; + + return instance + .load() + .then(() => { + throw new Error('Should fail'); + }) + .catch(err => { + expect(http.get).to.have.been.calledWithMatch( + sinon.match({ + headers: { + authorization: sinon.match.string, + }, + }) + ); + }); + }); + + it('sets authorization headers on http request', () => { + const token = + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWV9.44lJS0jlltzcglq7vgjXMXYRTecBxseN3Dec_LO_osI'; + instance.config = { + apis: [ + { + url: 'http://test/swagger.json', + resolve: { + http: { + headers: { + authorization: token, + }, + }, + }, + }, + ], + }; + + return instance + .load() + .then(() => { + throw new Error('Should fail'); + }) + .catch(err => { + expect(http.get).to.have.been.calledWithMatch( + sinon.match({ + headers: { + authorization: token, + }, + }) + ); + }); + }); + + afterEach(() => sandbox.restore()); + }); + describe('filterPaths()', () => { it('filters included path', () => { instance.apis = [