diff --git a/lib/authorizer.js b/lib/authorizer.js index b90b2e6..6b65a73 100644 --- a/lib/authorizer.js +++ b/lib/authorizer.js @@ -18,11 +18,20 @@ AuthorizerOAuth2.prototype.authorize = AuthorizerOAuth2.prototype.whoami = funct if (err) return cb(err) else if (!user.accessToken) return _this.session.oauthURL(cb, token) else { - _this._checkToken(user.accessToken, function (err) { - if (err) { - return _this.session.oauthURL(cb, token) - } else { + // we hold a lock in redis for a few minutes, to prevent + // a thundering herd of auth requests. + _this.session.checkLock(token, function (locked) { + if (locked) { return cb(null, user) + } else { + _this._checkToken(user.accessToken, function (err) { + if (err) { + return _this.session.oauthURL(cb, token) + } else { + _this.session.lock(token) + return cb(null, user) + } + }) } }) } diff --git a/lib/session.js b/lib/session.js index 6135003..1a91157 100644 --- a/lib/session.js +++ b/lib/session.js @@ -5,6 +5,8 @@ var redis = require('redis') function SessionOAuth2 (opts) { _.extend(this, { sessionLookupPrefix: 'user-', + sessionLockPrefix: 'user-token-lock-', + lockTime: 300, client: redis.createClient(process.env.LOGIN_CACHE_REDIS), clientId: process.env.OAUTH2_CLIENT_ID, clientSecret: process.env.OAUTH2_CLIENT_SECRET, @@ -41,6 +43,17 @@ SessionOAuth2.prototype.get = function (key, cb) { }) } +SessionOAuth2.prototype.checkLock = function (key, cb) { + var _this = this + + key = normalizeKey(key) + + this.client.get(_this.sessionLockPrefix + key, function (_err, lock) { + if (lock) return cb(true) + else return cb(false) + }) +} + SessionOAuth2.prototype.set = function (key, user, cb) { var _this = this @@ -49,6 +62,22 @@ SessionOAuth2.prototype.set = function (key, user, cb) { _this.client.set(this.sessionLookupPrefix + key, JSON.stringify(user), cb) } +SessionOAuth2.prototype.lock = function (key) { + var _this = this + + key = normalizeKey(key) + + _this.client.setex(this.sessionLockPrefix + key, this.lockTime, 'locked') +} + +SessionOAuth2.prototype.unlock = function (key) { + var _this = this + + key = normalizeKey(key) + + _this.client.del(this.sessionLockPrefix + key) +} + SessionOAuth2.prototype.delete = function (key, cb) { key = normalizeKey(key) diff --git a/test/authorizer.js b/test/authorizer.js index f26871d..d1c47c7 100644 --- a/test/authorizer.js +++ b/test/authorizer.js @@ -17,6 +17,8 @@ tap.test('it responds with session object if SSO dance is complete', function (t .get('/user') .reply(200) + session.unlock('ben@example.com-abc123') + session.set('ben@example.com-abc123', userComplete, function (err) { t.equal(err, null) authorizer.authorize({ @@ -25,6 +27,7 @@ tap.test('it responds with session object if SSO dance is complete', function (t } }, function (err, user) { authorizer.end() + session.unlock('ben@example.com-abc123') session.delete('ben@example.com-abc123') profile.done() @@ -35,6 +38,28 @@ tap.test('it responds with session object if SSO dance is complete', function (t }) }) +tap.test('does not check github if request is within timeout window', function (t) { + var authorizer = new Authorizer() + + session.lock('ben@example.com-abc123') + session.set('ben@example.com-abc123', userComplete, function (err) { + t.equal(err, null) + authorizer.authorize({ + headers: { + authorization: 'Bearer ben@example.com-abc123' + } + }, function (err, user) { + authorizer.end() + session.delete('ben@example.com-abc123') + session.unlock('ben@example.com-abc123') + + t.equal(err, null) + t.equal(user.email, 'ben@example.com') + t.end() + }) + }) +}) + tap.test('it returns error with login url if access token is no longer valid', function (t) { var authorizer = new Authorizer() var profile = nock('https://api.github.com') @@ -50,6 +75,7 @@ tap.test('it returns error with login url if access token is no longer valid', f }, function (err, user) { authorizer.end() session.delete('ben@example.com-abc123') + session.unlock('ben@example.com-abc123') profile.done() t.ok(err.message.indexOf('visit https://auth.example.com') !== -1) @@ -69,6 +95,7 @@ tap.test('it returns error with login url if SSO dance is not complete', functio }, function (err, user) { authorizer.end() session.delete('ben@example.com-abc123') + session.unlock('ben@example.com-abc123') t.ok(err.message.indexOf('visit https://auth.example.com') !== -1) t.end()