Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
31 changes: 8 additions & 23 deletions src/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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');
Expand All @@ -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',
Expand All @@ -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'});
}

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

Expand Down