Merge branch 'hotfix/21.56.9' into master
[gitter.git] / server / web / middlewares / rememberme-middleware.js
blobedfd69c7209443b13ef3865aa9db91c69e1b1e5d
1 'use strict';
3 var env = require('gitter-web-env');
4 var logger = env.logger;
5 var nconf = env.config;
6 var stats = env.stats;
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:';
30 /**
31  * Generate an auth token for a user and save it in redis
32  */
33 var generateAuthToken = Promise.method(function(userId) {
34   debug('Generate auth token: userId=%s', userId);
35   var key = uuid();
36   var token = uuid();
38   var promiseLike = sechash.strongHash(token, {
39     algorithm: 'sha512'
40   });
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);
49       });
50     })
51     .return(key + ':' + token);
52 });
54 /**
55  * Delete a token
56  */
57 var deleteAuthToken = Promise.method(function(authCookieValue) {
58   debug('Delete auth token: token=%s', authCookieValue);
60   /* Auth cookie */
61   if (!authCookieValue) return;
63   var authToken = authCookieValue.split(':', 2);
64   if (authToken.length !== 2) return;
66   var key = authToken[0];
68   if (!key) return;
70   debug('Deleting rememberme token %s', key);
72   var redisKey = 'rememberme:' + key;
74   return Promise.fromCallback(function(callback) {
75     redisClient.del(redisKey, callback);
76   });
77 });
79 /**
80  * Validate an existing token and then remove it a short while later
81  */
82 var validateAuthToken = Promise.method(function(authCookieValue) {
83   /* Auth cookie */
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];
92   if (!key) return;
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) {
99     return redisClient
100       .multi()
101       .get(redisKey)
102       .pexpire(redisKey, tokenGracePeriodMillis)
103       .exec(callback);
104   }).then(function(replies) {
105     var tokenInfo = replies[0];
106     if (!tokenInfo) {
107       return;
108     }
110     var stored = parseToken(tokenInfo);
111     if (!stored) {
112       logger.info('rememberme: Saved token is corrupt.', { key: key, tokenInfo: tokenInfo });
113       return;
114     }
116     var serverHash = stored.hash;
118     var promise = sechash.testHash(clientToken, serverHash, {
119       algorithm: 'sha512'
120     });
122     return Promise.resolve(promise).then(function(match) {
123       if (!match) {
124         logger.warn('rememberme: testHash failed. Illegal token', {
125           serverHash: serverHash,
126           clientToken: clientToken,
127           key: key
128         });
130         return;
131       }
133       var userId = stored.userId;
134       return userId;
135     });
136   });
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
144  */
145 function processRememberMeToken(presentedCookie) {
146   return validateAuthToken(presentedCookie).then(function(userId) {
147     debug('Resolved userId=%s for token=%s', userId, presentedCookie);
148     if (!userId) return;
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
164           return null;
165         })
166         .then(function(newCookieValue) {
167           return {
168             user: user,
169             newCookieValue: newCookieValue
170           };
171         });
172     });
173   });
176 function setRememberMeCookie(res, cookieValue) {
177   res.cookie(cookieName, cookieValue, {
178     domain: cookieDomain,
179     maxAge: 1000 * 60 * 60 * 24 * timeToLiveDays,
180     httpOnly: true,
181     secure: cookieSecure,
182     sameSite: cookieSecure ? 'none' : 'lax'
183   });
187  * Safe parse a token
188  */
189 function parseToken(tokenInfo) {
190   try {
191     return JSON.parse(tokenInfo);
192   } catch (e) {
193     /* */
194   }
197 module.exports = {
198   deleteRememberMeToken: function(cookie, callback) {
199     return deleteAuthToken(cookie).asCallback(callback);
200   },
202   generateRememberMeTokenMiddleware: function(req, res, next) {
203     return generateAuthToken(req.user.id)
204       .then(function(newCookieValue) {
205         setRememberMeCookie(res, newCookieValue);
206         return null;
207       })
208       .asCallback(next);
209   },
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) {
218         if (!loginDetails) {
219           stats.event('rememberme_rejected');
220           res.clearCookie(cookieName, { domain: nconf.get('web:cookieDomain') });
221           return;
222         }
224         var user = loginDetails.user;
225         var newCookieValue = loginDetails.newCookieValue;
227         if (newCookieValue) {
228           setRememberMeCookie(res, newCookieValue);
229         }
231         // Remove the old token for this user
232         req.accessToken = null;
234         // Tracking
235         var properties = useragentTagger(req);
236         stats.userUpdate(user, properties);
238         stats.event('rememberme_accepted');
240         stats.event(
241           'user_login',
242           _.extend(
243             {
244               userId: user._id,
245               method: 'auto',
246               email: user.email
247             },
248             properties
249           )
250         );
252         // Finally, log the user in
253         return passportLogin(req, user);
254       })
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') });
259         throw err;
260       })
261       .asCallback(next);
262   },
264   testOnly: {
265     processRememberMeToken: processRememberMeToken,
266     generateAuthToken: generateAuthToken,
267     validateAuthToken: validateAuthToken,
268     deleteAuthToken: deleteAuthToken,
269     setTokenGracePeriodMillis: function(time) {
270       tokenGracePeriodMillis = time;
271     }
272   }