3 var env = require('gitter-web-env');
4 var logger = env.logger;
5 var nconf = env.config;
8 var _ = require('lodash');
9 var uuid = require('uuid/v4');
10 var sechash = require('sechash');
11 var userService = require('gitter-web-users');
12 var useragentTagger = require('../user-agent-tagger');
13 var debug = require('debug')('gitter:infra:rememberme-middleware');
14 var userScopes = require('gitter-web-identity/lib/user-scopes');
15 var passportLogin = require('../passport-login');
16 var Promise = require('bluebird');
17 var validateUserAgentFromReq = require('../validate-user-agent-from-req');
19 var cookieName = nconf.get('web:cookiePrefix') + 'auth';
20 var cookieDomain = nconf.get('web:cookieDomain');
21 var cookieSecure = nconf.get('web:secureCookies');
22 var timeToLiveDays = nconf.get('web:rememberMeTTLDays');
24 var redisClient = env.redis.getClient();
26 var tokenGracePeriodMillis = 5000; /* How long after a token has been used can you reuse it? */
28 var REMEMBER_ME_PREFIX = 'rememberme:';
31 * Generate an auth token for a user and save it in redis
33 var generateAuthToken = Promise.method(function(userId) {
34 debug('Generate auth token: userId=%s', userId);
38 var promiseLike = sechash.strongHash(token, {
42 return Promise.resolve(promiseLike)
43 .then(function(hash3) {
44 var json = JSON.stringify({ userId: userId, hash: hash3 });
46 /* The server doesn't keep a copy of the token anywhere, only the hash */
47 return Promise.fromCallback(function(callback) {
48 redisClient.setex(REMEMBER_ME_PREFIX + key, 60 * 60 * 24 * timeToLiveDays, json, callback);
51 .return(key + ':' + token);
57 var deleteAuthToken = Promise.method(function(authCookieValue) {
58 debug('Delete auth token: token=%s', authCookieValue);
61 if (!authCookieValue) return;
63 var authToken = authCookieValue.split(':', 2);
64 if (authToken.length !== 2) return;
66 var key = authToken[0];
70 debug('Deleting rememberme token %s', key);
72 var redisKey = 'rememberme:' + key;
74 return Promise.fromCallback(function(callback) {
75 redisClient.del(redisKey, callback);
80 * Validate an existing token and then remove it a short while later
82 var validateAuthToken = Promise.method(function(authCookieValue) {
84 if (!authCookieValue) return;
86 var authToken = authCookieValue.split(':', 2);
87 if (authToken.length !== 2) return;
89 var key = authToken[0];
90 var clientToken = authToken[1];
94 debug('Client has presented a rememberme auth cookie, attempting reauthentication: %s', key);
96 var redisKey = REMEMBER_ME_PREFIX + key;
98 return Promise.fromCallback(function(callback) {
102 .pexpire(redisKey, tokenGracePeriodMillis)
104 }).then(function(replies) {
105 var tokenInfo = replies[0];
110 var stored = parseToken(tokenInfo);
112 logger.info('rememberme: Saved token is corrupt.', { key: key, tokenInfo: tokenInfo });
116 var serverHash = stored.hash;
118 var promise = sechash.testHash(clientToken, serverHash, {
122 return Promise.resolve(promise).then(function(match) {
124 logger.warn('rememberme: testHash failed. Illegal token', {
125 serverHash: serverHash,
126 clientToken: clientToken,
133 var userId = stored.userId;
140 * Authenticate a user with the presented cookie.
142 * Returns the user and a new cookie for the user
143 * or null if the token is invalid
145 function processRememberMeToken(presentedCookie) {
146 return validateAuthToken(presentedCookie).then(function(userId) {
147 debug('Resolved userId=%s for token=%s', userId, presentedCookie);
150 return userService.findById(userId).then(function(user) {
151 if (!user) return null;
153 // Account disabled? Go away
154 if (user.state === 'DISABLED' || user.isRemoved()) return null;
156 /* No token, user will need to relogin */
157 if (userScopes.isMissingTokens(user)) return null;
159 return generateAuthToken(user._id)
160 .catch(function(err) {
161 logger.warn('rememberme: generateAuthToken failed', { exception: err });
163 // Ignore errors that occur while generating a new token
166 .then(function(newCookieValue) {
169 newCookieValue: newCookieValue
176 function setRememberMeCookie(res, cookieValue) {
177 res.cookie(cookieName, cookieValue, {
178 domain: cookieDomain,
179 maxAge: 1000 * 60 * 60 * 24 * timeToLiveDays,
181 secure: cookieSecure,
182 sameSite: cookieSecure ? 'none' : 'lax'
189 function parseToken(tokenInfo) {
191 return JSON.parse(tokenInfo);
198 deleteRememberMeToken: function(cookie, callback) {
199 return deleteAuthToken(cookie).asCallback(callback);
202 generateRememberMeTokenMiddleware: function(req, res, next) {
203 return generateAuthToken(req.user.id)
204 .then(function(newCookieValue) {
205 setRememberMeCookie(res, newCookieValue);
211 rememberMeMiddleware: function(req, res, next) {
212 /* If the user is logged in or doesn't have cookies, continue */
213 if (req.user || !req.cookies || !req.cookies[cookieName]) return next();
215 return validateUserAgentFromReq(req)
216 .then(() => processRememberMeToken(req.cookies[cookieName]))
217 .then(function(loginDetails) {
219 stats.event('rememberme_rejected');
220 res.clearCookie(cookieName, { domain: nconf.get('web:cookieDomain') });
224 var user = loginDetails.user;
225 var newCookieValue = loginDetails.newCookieValue;
227 if (newCookieValue) {
228 setRememberMeCookie(res, newCookieValue);
231 // Remove the old token for this user
232 req.accessToken = null;
235 var properties = useragentTagger(req);
236 stats.userUpdate(user, properties);
238 stats.event('rememberme_accepted');
252 // Finally, log the user in
253 return passportLogin(req, user);
255 .catch(function(err) {
256 debug('Rememberme token failed with %s', err);
257 stats.event('rememberme_rejected');
258 res.clearCookie(cookieName, { domain: nconf.get('web:cookieDomain') });
265 processRememberMeToken: processRememberMeToken,
266 generateAuthToken: generateAuthToken,
267 validateAuthToken: validateAuthToken,
268 deleteAuthToken: deleteAuthToken,
269 setTokenGracePeriodMillis: function(time) {
270 tokenGracePeriodMillis = time;