Commit 64056b2b authored by Eric Eastwood's avatar Eric Eastwood

Add ability to revoke OAuth clients

parent b8b63cfb
......@@ -14,7 +14,8 @@ var OAuthClientSchema = new Schema({
clientSecret: String,
registeredRedirectUri: String,
canSkipAuthorization: Boolean,
ownerUserId: ObjectId
ownerUserId: ObjectId,
revoked: Boolean
});
OAuthClientSchema.index({ clientKey: 1 });
OAuthClientSchema.index({ ownerUserId: 1 });
......
"use strict";
var Promise = require('bluebird');
var debug = require('debug')('gitter:tests:test-fixtures');
var crypto = require('crypto');
var OAuthAccessToken = require('gitter-web-persistence').OAuthAccessToken;
function createOAuthAccessToken(fixtureName, f) {
debug('Creating %s', fixtureName);
return OAuthAccessToken.create({
token: f.token || crypto.randomBytes(20).toString('hex'),
userId: f.userId,
clientId: f.clientId,
});
}
function createOAuthAccessTokens(expected, fixture) {
return Promise.map(Object.keys(expected), function(key) {
if (key.match(/^oAuthAccessToken/)) {
var expectedOAuthAccessToken = expected[key];
expectedOAuthAccessToken.userId = fixture[expectedOAuthAccessToken.user]._id;
expectedOAuthAccessToken.clientId = fixture[expectedOAuthAccessToken.client]._id;
return createOAuthAccessToken(key, expectedOAuthAccessToken)
.then(function(oAuthAccessToken) {
fixture[key] = oAuthAccessToken;
});
}
return null;
});
}
module.exports = createOAuthAccessTokens;
"use strict";
var Promise = require('bluebird');
var debug = require('debug')('gitter:tests:test-fixtures');
var crypto = require('crypto');
var fixtureUtils = require('./fixture-utils');
var OAuthClient = require('gitter-web-persistence').OAuthClient;
function createOAuthClient(fixtureName, f) {
debug('Creating %s', fixtureName);
var name = f.name || fixtureUtils.generateName();
return OAuthClient.create({
name: name,
clientKey: f.clientKey || crypto.randomBytes(20).toString('hex'),
clientSecret: f.clientSecret || crypto.randomBytes(20).toString('hex'),
revoked: f.revoked || false
});
}
function createOAuthClients(expected, fixture) {
return Promise.map(Object.keys(expected), function(key) {
if (key.match(/^oAuthClient/)) {
var expectedOAuthClient = expected[key];
return createOAuthClient(key, expectedOAuthClient)
.then(function(oAuthClient) {
fixture[key] = oAuthClient;
});
}
return null;
});
}
module.exports = createOAuthClients;
......@@ -11,6 +11,8 @@ var integrationFixtures = require('./integration-fixtures');
var fixtureSteps = [
require('./delete-documents'),
require('./create-users'),
require('./create-oauth-clients'),
require('./create-oauth-access-tokens'),
require('./create-identities'),
require('./create-forums'),
require('./create-categories'),
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<title>Sign in failed</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link id="favicon" rel="shortcut icon" href="{{ cdn 'images/favicon-normal.ico' }}">
<link href='{{cdn "styles/login.css" }}' rel="stylesheet">
</head>
<body>
<div class="background">
<div class="login">
<header>
<h1>Token was revoked</h1>
</header>
<section>
<p>
We're very sorry, but the token you were using was revoked.
</p>
<p>
You probably need to <a href="{{ appsLink }}">update your client, {{ appsLink }}</a>
</p>
</section>
</div>
</div>
{{{ generateEnv }}}
{{{ bootScript "just-tracking" }}}
{{> live_reload }}
</body>
</html>
......@@ -63,6 +63,16 @@ router.get('/failed',
});
});
router.get('/token-revoked',
identifyRoute('token-revoked'),
function(req, res) {
res.status(401);
res.render('token-revoked', {
appsLink: config.get('web:basepath') + '/apps'
});
});
// ----------------------------------------------------------
// GitHub
// ----------------------------------------------------------
......
......@@ -7,6 +7,7 @@ var logger = env.logger;
var persistenceService = require('gitter-web-persistence');
var Promise = require('bluebird');
var StatusError = require('statuserror');
var userService = require('./user-service');
var tokenProvider = require('./tokens/');
var MongooseCachedLookup = require('../utils/mongoose-cached-lookup');
......@@ -64,7 +65,7 @@ function findAuthorizationCode(code, callback) {
* Returns { user / client / accessToken } hash. If the token is for an anonymous user,
* user is null;
*/
function validateAccessTokenAndClient(token, callback) {
function validateAccessTokenAndClient(token) {
return tokenProvider.validateToken(token)
.then(function(result) {
if (!result) {
......@@ -89,6 +90,13 @@ function validateAccessTokenAndClient(token, callback) {
logger.warn('Invalid token presented (client not found): ', { token: token, clientId: clientId });
return null;
}
else if(client.revoked) {
logger.warn('Token can not be accepted (client has been revoked): ', { token: token, clientId: clientId });
var e = new StatusError(401);
e.clientRevoked = true;
throw e;
}
if(userId && !user) {
logger.warn('Invalid token presented (user not found): ', { token: token, userId: userId });
......@@ -102,11 +110,10 @@ function validateAccessTokenAndClient(token, callback) {
return { user: user, client: client };
});
})
.nodeify(callback);
});
}
function removeAllAccessTokensForUser(userId, callback) {
return persistenceService.OAuthAccessToken.remove({ userId: userId })
.exec()
......
......@@ -78,38 +78,38 @@ module.exports = bayeuxExtension({
statsd.increment('bayeux.handshake.attempt', 1, 0.25, tags);
oauth.validateAccessTokenAndClient(ext.token, function(err, tokenInfo) {
if(err) return callback(err);
if(!tokenInfo) {
return callback(new StatusError(401, "Invalid access token"));
}
var user = tokenInfo.user;
var oauthClient = tokenInfo.client;
var userId = user && user.id;
if(user && oauthClient) {
clientUsageStats.record(user, oauthClient);
}
debug('bayeux: handshake. appVersion=%s, username=%s, client=%s', ext.appVersion, user && user.username, oauthClient.name);
var connectionType = getConnectionType(ext.connType);
message._private.authenticator = {
userId: userId,
connectionType: connectionType,
client: ext.client,
troupeId: ext.troupeId,
oauthClientId: oauthClient.id,
uniqueClientId: ext.uniqueClientId,
realtimeLibrary: ext.realtimeLibrary,
eyeballState: parseInt(ext.eyeballs, 10) || 0
};
return callback(null, message);
});
oauth.validateAccessTokenAndClient(ext.token)
.then(function(tokenInfo) {
if(!tokenInfo) {
return callback(new StatusError(401, "Invalid access token"));
}
var user = tokenInfo.user;
var oauthClient = tokenInfo.client;
var userId = user && user.id;
if(user && oauthClient) {
clientUsageStats.record(user, oauthClient);
}
debug('bayeux: handshake. appVersion=%s, username=%s, client=%s', ext.appVersion, user && user.username, oauthClient.name);
var connectionType = getConnectionType(ext.connType);
message._private.authenticator = {
userId: userId,
connectionType: connectionType,
client: ext.client,
troupeId: ext.troupeId,
oauthClientId: oauthClient.id,
uniqueClientId: ext.uniqueClientId,
realtimeLibrary: ext.realtimeLibrary,
eyeballState: parseInt(ext.eyeballs, 10) || 0
};
return message;
})
.asCallback(callback);
},
......
'use strict';
var oauthService = require('../../services/oauth-service');
var env = require('gitter-web-env');
var logger = env.logger;
var StatusError = require('statuserror');
var oauthService = require('../../services/oauth-service');
function getAccessToken(req) {
......@@ -18,7 +19,7 @@ function getAccessToken(req) {
var parts = authHeader.split(' ');
if (parts.length == 2) {
if (parts.length === 2) {
var scheme = parts[0];
if (/Bearer/i.test(scheme)) {
......@@ -54,10 +55,10 @@ module.exports = function(req, res, next) {
return oauthService.validateAccessTokenAndClient(accessToken)
.then(function(tokenInfo) {
// Token not found
if(!tokenInfo) return next(401);
if(!tokenInfo) return next(new StatusError(401, 'Token not found'));
// Anonymous tokens cannot be used for Bearer tokens
if(!tokenInfo.user) return next(401);
if(!tokenInfo.user) return next(new StatusError(401, 'Anonymous tokens cannot be used for Bearer tokens'));
var user = tokenInfo.user;
var client = tokenInfo.client;
......
'use strict';
var oauthService = require('../../services/oauth-service');
var debug = require('debug')('gitter:infra:configure-csrf');
var Promise = require('bluebird');
var env = require('gitter-web-env');
var stats = env.stats;
var debug = require('debug')('gitter:infra:configure-csrf');
var oauthService = require('../../services/oauth-service');
function setAccessToken(req, userId, accessToken) {
if(req.session) {
......@@ -57,14 +58,21 @@ module.exports = function(req, res, next) {
var sessionAccessToken = getSessionAccessToken(req, userId);
if(sessionAccessToken) {
return oauthService.validateAccessTokenAndClient(sessionAccessToken)
.then(function(result) {
if (!result) {
.then(function(tokenInfo) {
if(!tokenInfo) {
return generateAccessToken();
}
req.accessToken = sessionAccessToken;
})
.catch(function(err) {
// We shouldn't try to regenerate something that was revoked
if(err.clientRevoked) {
throw err;
}
debug('csrf: OAuth access token validation failed: %j', err);
// Refresh anonymous tokens
return generateAccessToken();
})
.nodeify(next);
......
"use strict";
var Promise = require('bluebird');
var env = require('gitter-web-env');
var config = env.config;
var _ = require('lodash');
var userScopes = require('gitter-web-identity/lib/user-scopes');
var logout = Promise.promisify(require('./logout'));
function linkStack(stack) {
if(!stack) return;
......@@ -31,19 +33,25 @@ module.exports = function(err, req, res, next) { // eslint-disable-line no-unuse
var status = res.statusCode;
/* Got a 401, the user isn't logged in and this is a browser? */
if(status === 401 && req.accepts(['json','html']) === 'html' && !req.user) {
if(status === 401 && req.accepts(['json','html']) === 'html') {
var returnUrl = req.originalUrl.replace(/\/~(\w+)$/,"");
if(req.session) {
if(err.clientRevoked) {
return logout(req, res)
.then(() => {
return res.redirect('/login/token-revoked');
});
}
else if(!req.user && req.session) {
req.session.returnTo = returnUrl;
res.redirect('/login');
} else {
return res.redirect('/login');
}
else if(!req.user) {
// This should not really be happening but
// may do if the gitter client isn't doing
// oauth properly
res.redirect('/login?returnTo=' + encodeURIComponent(returnUrl));
return res.redirect('/login?returnTo=' + encodeURIComponent(returnUrl));
}
return;
}
var template = getTemplateForStatus(status);
......@@ -68,7 +76,7 @@ module.exports = function(err, req, res, next) { // eslint-disable-line no-unuse
res.send({ error: message });
},
text: function() {
res.send('Error: ', message);
res.send('Error: ' + message);
}
});
......
"use strict";
var Promise = require('bluebird');
var testRequire = require('../test-require');
var assert = require('assert');
var mongoUtils = require('gitter-web-persistence-utils/lib/mongo-utils');
var oauthService = testRequire("./services/oauth-service");
var Promise = require('bluebird');
var oauthService = testRequire('./services/oauth-service');
var fixtureLoader = require('gitter-web-test-utils/lib/test-fixtures');
describe('oauth-service', function() {
var fixture = fixtureLoader.setup({
user1: { }
user1: { },
oAuthClientRevoked1: {
revoked: true
},
oAuthAccessTokenRevoked1: {
user: 'user1',
client: 'oAuthClientRevoked1'
}
});
it('should create tokens', function() {
......@@ -133,10 +140,10 @@ describe('oauth-service', function() {
});
it('should use validate tokens', function(done) {
var userId = fixture.user1.id;
it('should validate tokens', function(done) {
var user = fixture.user1;
return oauthService.findOrGenerateWebToken(userId)
return oauthService.findOrGenerateWebToken(user._id)
.spread(function(token1, client) {
assert(token1);
assert.equal('string', typeof token1);
......@@ -147,12 +154,14 @@ describe('oauth-service', function() {
return oauthService.validateAccessTokenAndClient(token1)
.then(function(tokenInfo) {
assert(tokenInfo);
assert(mongoUtils.objectIDsEqual(tokenInfo.user._id, user._id));
assert(mongoUtils.objectIDsEqual(tokenInfo.client._id, client._id));
});
})
.nodeify(done);
});
it('should use validate anonymous tokens', function(done) {
it('should validate anonymous tokens', function(done) {
return oauthService.generateAnonWebToken()
.spread(function(token1, client) {
assert(token1);
......@@ -164,11 +173,23 @@ describe('oauth-service', function() {
return oauthService.validateAccessTokenAndClient(token1)
.then(function(tokenInfo) {
assert(tokenInfo);
assert.equal(tokenInfo.user, null);
assert(mongoUtils.objectIDsEqual(tokenInfo.client._id, client._id));
});
})
.nodeify(done);
});
it('should consider a revoked client as an invalid token', function(done) {
var token = fixture.oAuthAccessTokenRevoked1.token;
return oauthService.validateAccessTokenAndClient(token)
.catch(function(err) {
assert.equal(err.clientRevoked, true);
})
.nodeify(done);
});
it('should reuse cached tokens', function(done) {
var userId = fixture.user1.id;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment