Gitter migration: Setup redirects (rollout pt. 3)
[gitter.git] / modules / gitlab / lib / get-gitlab-access-token-from-user.js
blob7d8a66cfafe34f14cc06111aae3140f16963615a
1 'use strict';
3 const assert = require('assert');
4 const url = require('url');
5 const util = require('util');
6 const request = util.promisify(require('request'));
7 const Promise = require('bluebird');
8 const StatusError = require('statuserror');
9 const debug = require('debug')('gitter:app:gitlab:get-gitlab-access-token-from-user');
11 const env = require('gitter-web-env');
12 const config = env.config;
13 const logger = env.logger;
14 const mongoUtils = require('gitter-web-persistence-utils/lib/mongo-utils');
15 const obfuscateToken = require('gitter-web-github').obfuscateToken;
16 const identityService = require('gitter-web-identity');
17 const callbackUrlBuilder = require('gitter-web-oauth/lib/callback-url-builder');
19 const parseAccessTokenExpiresMsFromRes = require('./parse-access-token-expires-ms-from-res');
21 async function refreshGitlabAccessToken(identity) {
22   debug(
23     `refreshGitlabAccessToken ${identity.username} (${identity.providerKey}) refreshToken=${identity.refreshToken}`
24   );
25   // We're trying to construct a URL that looks like:
26   // https://gitlab.com/oauth/token?grant_type=refresh_token&client_id=abc&client_secret=abc&refresh_token=abc&redirect_uri=abc
27   const gitlabRefreshTokenUrl = new url.URL('https://gitlab.com/oauth/token');
28   gitlabRefreshTokenUrl.searchParams.set('grant_type', 'refresh_token');
29   gitlabRefreshTokenUrl.searchParams.set('client_id', config.get('gitlaboauth:client_id'));
30   gitlabRefreshTokenUrl.searchParams.set('client_secret', config.get('gitlaboauth:client_secret'));
31   gitlabRefreshTokenUrl.searchParams.set('refresh_token', identity.refreshToken);
32   gitlabRefreshTokenUrl.searchParams.set('redirect_uri', callbackUrlBuilder('gitlab'));
34   const refreshRes = await request({
35     method: 'POST',
36     uri: gitlabRefreshTokenUrl.toString(),
37     json: true,
38     headers: {
39       'Content-Type': 'application/json'
40     }
41   });
43   if (refreshRes.statusCode !== 200) {
44     const apiUrlLogSafe = new url.URL(gitlabRefreshTokenUrl.toString());
45     apiUrlLogSafe.searchParams.set('client_id', 'xxx');
46     apiUrlLogSafe.searchParams.set('client_secret', 'xxx');
47     apiUrlLogSafe.searchParams.set('refresh_token', 'xxx');
49     logger.warn(
50       `Failed to refresh GitLab access token for ${identity.username} (${
51         identity.providerKey
52       }) using refreshToken=${obfuscateToken(
53         identity.refreshToken
54       )}. GitLab API (POST ${apiUrlLogSafe.toString()}) returned ${refreshRes.statusCode}: ${
55         typeof refreshRes.body === 'object' ? JSON.stringify(refreshRes.body) : refreshRes.body
56       }`
57     );
58     throw new StatusError(
59       500,
60       `Unable to refresh expired GitLab access token. You will probably need to sign out and back in to get a new access token or the GitLab API is down. GitLab API returned ${refreshRes.statusCode}`
61     );
62   }
64   const accessTokenExpiresMs = parseAccessTokenExpiresMsFromRes(refreshRes.body);
65   assert(accessTokenExpiresMs);
66   const accessToken = refreshRes.body.access_token;
67   assert(accessToken);
68   const refreshToken = refreshRes.body.refresh_token;
69   assert(refreshToken);
71   const newGitlabIdentityData = {
72     accessToken,
73     accessTokenExpires: new Date(accessTokenExpiresMs),
74     refreshToken
75   };
77   await identityService.updateById(identity._id || identity.id, newGitlabIdentityData);
79   return accessToken;
82 // Cache of ongoing promises to refresh access tokens
83 const waitingForNewTokenPromiseMap = {};
85 module.exports = function getGitlabAccessTokenFromUser(user) {
86   if (!user) return Promise.resolve();
88   return identityService
89     .getIdentityForUser(user, identityService.GITLAB_IDENTITY_PROVIDER)
90     .then(async function(glIdentity) {
91       if (!glIdentity) return null;
93       let accessToken = glIdentity.accessToken;
94       // If the access token is expired or about to expire by 2 minutes, grab a
95       // new access token. 2 minutes is arbitrary but we just want a buffer so
96       // that we don't try to use a token which expires right before we try to
97       // use it.
98       if (
99         glIdentity.accessTokenExpires &&
100         glIdentity.accessTokenExpires.getTime() - 120 * 1000 < Date.now()
101       ) {
102         // `getGitlabAccessTokenFromUser` can be called multiple times in quick
103         // succession in the same request but we can only exchange the
104         // refreshToken once for a new token, so we need to only do this once
105         // and re-use this work for all of the callers. This way they all get
106         // the new token after we successfully refresh.
107         const serializedUserId = mongoUtils.serializeObjectId(user._id || user.id);
108         const ongoingPromise = waitingForNewTokenPromiseMap[serializedUserId];
109         if (ongoingPromise) {
110           return ongoingPromise;
111         }
113         try {
114           waitingForNewTokenPromiseMap[serializedUserId] = refreshGitlabAccessToken(glIdentity);
115           accessToken = await waitingForNewTokenPromiseMap[serializedUserId];
116         } finally {
117           // Regardless of if this failed or succeeded, we are no longer waiting
118           // anymore and can clean up our cache for them to try again.
119           delete waitingForNewTokenPromiseMap[serializedUserId];
120         }
121       }
123       return accessToken;
124     });