mulesoft / osprey

Generate Node.JS API middleware from a RAML definition
Other
431 stars 66 forks source link

Unable to use custom security scheme #117

Open ElridgeDMello opened 8 years ago

ElridgeDMello commented 8 years ago

We are unable to use a custom security scheme based on the instructions listed at osprey's README.

We are using passport to setup the authentication middleware.

The issue/observed behavior is that the custom authentication function (passport.authenticate('my-apikey', { session: false })) specified as part of myCustomAuthScheme is not executed when handling a request.

All the code that is needed to test this is specified as the three blocks below. Save the three files with the names listed, then npm install && npm start to start up the app.

api.raml:

#%RAML 0.8
title: Example API
baseUri: /
securitySchemes:
  - custom_auth:
      type: x-custom
      describedBy:
        headers:
          my-apikey:
            description: Valid Custom API key
            example: ABCDEF1234567890abcdef1234567890
        responses:
          401:
/users:
  securedBy: [custom_auth]
  get:
    queryParameters:
      sort:
        enum: [username, name]

app.js:

const express = require('express');
const osprey = require('osprey');
const join = require('path').join;
const ramlParser = require('raml-parser');
const passport = require('passport');
const passportStrategy = require('passport-strategy');
const util = require('util');

const PORT = process.env.PORT || 3000;

const app = express()
const router = express.Router();

////////////////////////////////////////////////
// DEFINE PASSPORT STRATEGY:////////////////////
////////////////////////////////////////////////
const AUTH_HEADER = 'my-apikey';

function extractApiKey(req) {
  return req.headers[AUTH_HEADER] || null;
}

/**
 * Strategy constructor
 */
function MyAPiKeyStrategy(options) {
  passportStrategy.Strategy.call(this);
  this.name = 'my-apikey';

  this._configuredApiKey = options.apiKey;
  if (!this._configuredApiKey) {
    throw new TypeError('MyAPiKeyStrategy options requires an API key');
  }
}

util.inherits(MyAPiKeyStrategy, passportStrategy.Strategy);

MyAPiKeyStrategy.prototype.authenticate = function authenticate(req, options) {
  const self = this;

  const suppliedApiKey = extractApiKey(req);
  if (!suppliedApiKey) {
    return self.fail(new Error('No API key'));
  }

  if (suppliedApiKey === this._configuredApiKey) {
    self.success({});
  } else {
    self.fail();
  }
};
////////////////////////////////////////////////
// END PASSPORT STRATEGY DEFINITION ////////////
////////////////////////////////////////////////

// DEFINE ROUTE HANDLER:
router.get('/users',
  function(req, res) {
    res.send([{
        id: 1234,
        name: 'User 1234'
      }, {
        id: 5678,
        name: 'User 5678',
      }]);
});

function myCustomAuthScheme(scheme, name) {
  passport.use(new MyAPiKeyStrategy({ apiKey: 'MYSUPERSECRETAPIKEY1234567891000' }));
  return {
    handler: function mySecurityHandler(options, reqPath) {
      return passport.authenticate('my-apikey', { session: false });
    },
  };
}

ramlParser.loadFile(join(__dirname, 'api.raml'))
  .then(function (raml) {
    app.use(osprey.server(raml, {
      discardUnknownBodies: false,
      discardUnknownQueryParameters: false,
      discardUnknownHeaders: false,
    }), router)

    // SETUP CUSTOM SECURITY:
    app.use(osprey.security(raml, {
      custom_auth: myCustomAuthScheme
    }));

    app.use(osprey.errorHandler());

    app.listen(PORT, function() {
      console.log('Express server listening on port ' + PORT);
    });
  });

package.json:

{
  "name": "express-osprey-validation-test",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "body-parser": "^1.13.3",
    "cookie-parser": "^1.3.3",
    "express": "^4.13.3",
    "jade": "^1.11.0",
    "morgan": "^1.6.1",
    "osprey": "^0.3.2",
    "passport": "0.3.0",
    "passport-strategy": "^1.0.0",
    "serve-favicon": "^2.3.0"
  },
  "devDependencies": {
    "grunt": "^1.0.1",
    "grunt-develop": "^0.4.0",
    "grunt-contrib-watch": "^1.0.0",
    "request": "^2.60.0",
    "time-grunt": "^1.2.1",
    "load-grunt-tasks": "^3.2.0"
  }
}

Test Case:

Execute this curl request:

curl http://localhost:3000/users -H "my-apikey:invalidkey" -v

Expected behavior:

The app should return a 401 Unauthorized response

Actual behavior:

The /users resource is returned.

daldridge-cs commented 7 years ago

Individually app.use(...)ing the server, security, and error handlers appears to be the problem. If I compose them together, it appears to function as intended:

ramlParser.loadFile(join(__dirname, 'api.raml'))
  .then(function (raml) {
    const middleware = [];
    const handler = osprey.server(raml, {
      discardUnknownBodies: false,
      discardUnknownQueryParameters: false,
      discardUnknownHeaders: false,
    });
    const security = osprey.security(raml, {
      custom_auth: myCustomAuthScheme
    });
    const error = osprey.errorHandler();

    middleware.push(security);
    middleware.push(handler);
    middleware.push(error);

    const result = compose(middleware);
    result.ramlUriParameters = handler.ramlUriParameters;

    app.use(result, router);

    app.listen(PORT, function() {
      console.log('Express server listening on port ' + PORT);
    });
  });