UNPKG

express-token-api-middleware

Version:

An express middleware that allows to protect an api behind token authentication, rate limiting and endpoint permissions.

157 lines (144 loc) 6.52 kB
const events = require('events'); const util = require('util'); const Limiter = require('./lib/limiter'); const Tokens = require('./lib/tokens'); /** * @typedef {Object} Middleware * @property {Limiter.nodes} nodes * @property {Tokens.encode} getToken * @property {Limiter.notify} notify * @this Context */ /** * @typedef {Object} Context * @property {EventEmitter} emitter The event emitter * @property {MiddlewareConfig} config The global configuration for the middleware instance * @property {Tokens} tokens The token encoder/decoder * @property {Limiter} limiter The rate limiter */ /** * @typedef {Object} MiddlewareConfig * @property {string} [param=token] The get/cookie parameter name to use to look for the auth token * @property {number} [nodes=1] If this node is part of a round robin cluster you can specify how many nodes there are. * This will be used to determine the proper rate limit for a user without having to * synchronize with other nodes. (a simple "ratelimit * nodes" calculation is applied) * @property {string} password The password used to encrypt and decrypt user tokens * @property {String|Buffer} salt Used to generate unique passwords for this server. You can easily generate a salt using * the nodejs crypto library with crypto.randomBytes(16). * @property {function} [logger] A logger function that accepts a log message as first parameter (e.g. console.log) * @property {number} [timeout] A maximum waiting time for incoming requests. If the queue of requests being processed * is going to wait longer than this value (in ms) any subsequent requests will be rejected * until the queue is cleared up again. * @property {ErrorHandler} error Specify your own custom error handler if you don't want the middleware to respond with * standard error codes. */ /** * @callback ErrorHandler * @param {ClientRequest} req The client request object * @param {ServerResponse} res The server response object * @param {function} next The chain handler function to progress to the next request handler in the request chain * @param {number} statusCode The status code that the middleware would send to the client * @param {string} errorMessage The error message generated by the middleware */ /** * @typedef {Object} TokenConfig * @property {string|number} id The user id with which to identify the incoming user * @property {string|RegExp} [path] An optional request path that the user is allowed to access (falsey means no restrictions) * @property {RateLimit|number} [rate] A request rate limit that will prevent a user from sending to many requests per second * @property {string|number|Date} [exp] An expiration date when the token will no longer be valid */ /** * @typedef {Object} RateLimit * @property {number} value * @property {string} unit */ /** * * @param {MiddlewareConfig} config * @returns {Middleware} */ function Middleware(config = {}) { if (!config.password) { throw new Error('Unable to initialize token api middleware without password'); } if (!config.salt) { throw new Error('Unable to initialize token api middleware without a password salt'); } else { config.salt = config.salt instanceof Buffer ? config.salt : new Buffer(config.salt, 'utf8'); if (config.salt.length < 16) { throw new Error('The given salt is too short, please generate one with at lest 16 bytes length'); } } config.param = config.param || 'token'; config.logger = config.logger || (() => {}); config.nodes = parseInt(config.nodes) || 1; config.error = config.error || error; let context = { config, emitter: new events.EventEmitter(), tokens: new Tokens(config), limiter: new Limiter(config) }; let middleware = handler.bind(context); for (let prop in context.emitter) { if (typeof context.emitter[prop] == 'function') { middleware[prop] = context.emitter[prop].bind(context.emitter); } } middleware.getToken = context.tokens.encode.bind(context.tokens); middleware.notify = context.limiter.notify.bind(context.limiter); middleware.__defineSetter__('nodes', val => context.config.nodes = val); return middleware; } /** * The handler that check for valid api tokens and converts them to a usable user object. * @param {ClientRequest} req * @param {ServerResponse} res * @param {function} next * @this Context */ function handler(req, res, next) { let token = req.header('Authorization') || req.query[this.config.param] || req.cookies && req.cookies[this.config.params]; if (!token || !token.trim().length) { this.emitter.emit('missing', req); let message = 'No token found on request'; this.config.logger(message); return this.config.error(req, res, next, 401, message); } let user = this.tokens.decode(token); if (!user) { this.emitter.emit('fail', req); let message = 'Unable to decode token on request'; this.config.logger(message); return this.config.error(req, res, next, 403, message); } req.user = user; if (user.path && !user.path.test(req.originalUrl)) { this.emitter.emit('reject', req); let message = 'The user has not been granted access to this endpoint: ' + req.originalUrl; this.config.logger(message); return this.config.error(req, res, next, 403, message); } if (user.exp && user.exp < Date.now()) { this.emitter.emit('expired', req); let message = 'The user token has expired: ' + new Date(user.exp).toISOString(); this.config.logger(message); return this.config.error(req, res, next, 403, message); } try { this.limiter.check(user, next); this.emitter.emit('success', req); } catch (e) { this.emitter.emit('timeout', req); let message = 'The user has exceeded the timeout threshold by making too many requests'; this.config.logger(message); return this.config.error(req, res, next, 429, message); } } /** * @type ErrorHandler */ function error(req, res, next, status, message) { res.status(status).end(); } module.exports = Middleware;