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
];