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) {
23 `refreshGitlabAccessToken ${identity.username} (${identity.providerKey}) refreshToken=${identity.refreshToken}`
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({
36 uri: gitlabRefreshTokenUrl.toString(),
39 'Content-Type': 'application/json'
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');
50 `Failed to refresh GitLab access token for ${identity.username} (${
52 }) using refreshToken=${obfuscateToken(
54 )}. GitLab API (POST ${apiUrlLogSafe.toString()}) returned ${refreshRes.statusCode}: ${
55 typeof refreshRes.body === 'object' ? JSON.stringify(refreshRes.body) : refreshRes.body
58 throw new StatusError(
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}`
64 const accessTokenExpiresMs = parseAccessTokenExpiresMsFromRes(refreshRes.body);
65 assert(accessTokenExpiresMs);
66 const accessToken = refreshRes.body.access_token;
68 const refreshToken = refreshRes.body.refresh_token;
71 const newGitlabIdentityData = {
73 accessTokenExpires: new Date(accessTokenExpiresMs),
77 await identityService.updateById(identity._id || identity.id, newGitlabIdentityData);
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
99 glIdentity.accessTokenExpires &&
100 glIdentity.accessTokenExpires.getTime() - 120 * 1000 < Date.now()
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;
114 waitingForNewTokenPromiseMap[serializedUserId] = refreshGitlabAccessToken(glIdentity);
115 accessToken = await waitingForNewTokenPromiseMap[serializedUserId];
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];