From f4b4a87ff6c7e724d26e01cb4354e581aeca38a8 Mon Sep 17 00:00:00 2001 From: Morgan Patch Date: Wed, 24 May 2017 15:20:00 -0700 Subject: [PATCH] Use a Redis server to maintain auth tokens. --- README.md | 15 +++++++++++++++ package.json | 1 + src/app.js | 8 ++++++++ src/login.js | 31 ++++++++----------------------- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index d0a4ec4..e3cc029 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,21 @@ Valid values of NODE_ENV are ``mocha`` (PostgreSQL), ``mocha_sqlite`` (SQLite), ``development`` (SQLite), ``development_pg`` (PostgreSQL), ``production_pg`` (PostgreSQL), ``production_mysql`` (MySQL), and ``production_sqlite`` (SQLite). +Authentication Token Caching +---------------------------- + +In order to ensure that authentication tokens remain in memory even if the +TimeSync server restarts, TimeSync depends on a Redis server in which to cache +the tokens. + +The connection to this server is created on application startup, and can be +configured with the REDIS_HOST (default `localhost`), REDIS_PORT (default `6379`), +REDIS_PASSWORD (default NULL), and REDIS_DB (default NULL) environment variables. + +As well, the MAX_TOKEN_AGE environment variable can be set to the number of +seconds for which a token should remain in memory and be accepted. The default +value is 30 minutes. + Documentation ------------- diff --git a/package.json b/package.json index 0567fb3..81717df 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "passport-localapikey": "^0.0.3", "pg": "^4.5.1", "prompt": "^1.0.0", + "redis": "^2.7.1", "sqlite3": "^3.1.1", "uuid": "^2.0.1", "valid-url": "^1.0.9" diff --git a/src/app.js b/src/app.js index eeed1df..eba46fa 100644 --- a/src/app.js +++ b/src/app.js @@ -68,6 +68,14 @@ app.use(function(err, req, res, next) { // Set API version prefix app.set('version', '/v0'); +// Configure the Redis cache +app.set('redis', require('redis').createClient({ + host: process.env.REDIS_HOST || 'localhost', + port: process.env.REDIS_PORT || '6379', + password: process.env.REDIS_PASSWORD || null, + db: process.env.REDIS_DB || null, +})); + // Set up authentication const passport = require('passport'); diff --git a/src/login.js b/src/login.js index 73e987d..ecdcb20 100644 --- a/src/login.js +++ b/src/login.js @@ -3,14 +3,14 @@ const errors = require('./errors'); const passport = require('passport'); const crypto = require('crypto'); -const tokens = {}; - -const MAX_AGE = 30 * 60 * 1000; - module.exports = function(app) { const log = app.get('log'); + const redis = app.get('redis'); let authType; + const MAX_AGE = process.env.MAX_TOKEN_AGE || 1800; // Default 30 minutes + const MAX_AGE_MS = MAX_AGE * 1000; + if (!process.env.INSTANCE_NAME || !process.env.SECRET_KEY) { log.error('login.js', 'INSTANCE_NAME or SECRET_KEY not set!'); process.exit(1); @@ -60,7 +60,7 @@ module.exports = function(app) { const payload = { iss: process.env.INSTANCE_NAME, sub: req.body.auth.username, - exp: Date.now() + MAX_AGE, // Expires after 30 minutes + exp: Date.now() + MAX_AGE_MS, // Expires after 30 minutes iat: Date.now(), }; let encoded = new Buffer(JSON.stringify(header)).toString('base64'); @@ -74,7 +74,7 @@ module.exports = function(app) { const token = encoded + '.' + signature; - tokens[token] = {created: Date.now()}; + redis.set(token, Date.now(), 'nx', 'ex', MAX_AGE); res.set({ 'Cache-control': 'no-cache no-store must-validate max-age=0', @@ -88,27 +88,12 @@ module.exports = function(app) { passport.authenticate(authType, caller)(req, res, next); }); - const clearTokens = function() { - if (tokens.length > 0) { - /* eslint-disable prefer-const */ - for (let key in tokens) { - /* eslint-enable prefer-const */ - if (tokens[key].created + MAX_AGE < Date.now()) { - delete tokens[key]; - } - } - } - setTimeout(clearTokens, 1000 * 60 * 60); - }; - - setTimeout(clearTokens, 1000 * 60 * 60); - return { authToken: function(unescapedToken) { return new Promise(function(resolve, reject) { const token = unescapedToken.replace(/\s/g, '+'); - if (!tokens[token] || tokens[token].created + MAX_AGE < Date.now()) { + if (!redis.exists(token)) { return reject({message: 'Bad API token'}); } @@ -132,7 +117,7 @@ module.exports = function(app) { ); if (payload.iss !== process.env.INSTANCE_NAME || - payload.exp < Date.now() || payload.iat + MAX_AGE < Date.now()) { + payload.exp < Date.now() || payload.iat + MAX_AGE_MS < Date.now()) { return reject({message: 'Bad API token'}); }