Skip to content
This repository was archived by the owner on Nov 15, 2024. It is now read-only.

Commit 9ed0b4d

Browse files
committed
slack/msteams/glip auth support
1 parent e122023 commit 9ed0b4d

File tree

31 files changed

+699
-530
lines changed

31 files changed

+699
-530
lines changed

api_server/bin/api_server.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const PubNubConfig = require(ConfigDirectory + '/pubnub.js');
1717
const IpcConfig = require(ConfigDirectory + '/ipc.js');
1818
const MixPanelConfig = require(ConfigDirectory + '/mixpanel.js');
1919
const SlackConfig = require(ConfigDirectory + '/slack.js');
20+
const MSTeamsConfig = require(ConfigDirectory + '/msteams.js');
21+
const GlipConfig = require(ConfigDirectory + '/glip.js');
2022
const GithubConfig = require(ConfigDirectory + '/github.js');
2123
const AsanaConfig = require(ConfigDirectory + '/asana.js');
2224
const TrelloConfig = require(ConfigDirectory + '/trello.js');
@@ -80,6 +82,8 @@ const MyAPICluster = new ClusterWrapper(
8082
ipc: IpcConfig,
8183
mixpanel: MixPanelConfig,
8284
slack: SlackConfig,
85+
msteams: MSTeamsConfig,
86+
glip: GlipConfig,
8387
github: GithubConfig,
8488
asana: AsanaConfig,
8589
trello: TrelloConfig,

api_server/config/glip.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// MSTeams integration configuration
2+
3+
'use strict';
4+
5+
module.exports = {
6+
appClientId: process.env.CS_API_GLIP_CLIENT_ID,
7+
appClientSecret: process.env.CS_API_GLIP_CLIENT_SECRET
8+
};

api_server/config/msteams.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// MSTeams integration configuration
2+
3+
'use strict';
4+
5+
module.exports = {
6+
appClientId: process.env.CS_API_MSTEAMS_CLIENT_ID,
7+
appClientSecret: process.env.CS_API_MSTEAMS_CLIENT_SECRET
8+
};
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// provide service to handle OAuth2 based authorization
2+
3+
'use strict';
4+
5+
const APIServerModule = require(process.env.CS_API_TOP + '/lib/api_server/api_server_module.js');
6+
const fetch = require('node-fetch');
7+
const FS = require('fs');
8+
const FormData = require('form-data');
9+
const Base64 = require('base-64');
10+
11+
class OAuth2Module extends APIServerModule {
12+
13+
services () {
14+
const { provider } = this.oauthConfig;
15+
if (!this.api.config[provider]) {
16+
this.api.warn(`No configuration for ${provider}, auth service will be unavailable`);
17+
return;
18+
}
19+
return async () => {
20+
return { [`${provider}Auth`]: this };
21+
};
22+
}
23+
24+
// get redirect parameters and url to use in the redirect response
25+
getRedirectData (options) {
26+
const { provider, authUrl, scopes, additionalAuthCodeParameters } = this.oauthConfig;
27+
const { redirectUri, state } = options;
28+
const { appClientId } = this.api.config[provider];
29+
const parameters = {
30+
client_id: appClientId,
31+
redirect_uri: redirectUri,
32+
response_type: 'code',
33+
state
34+
};
35+
if (scopes) {
36+
parameters.scope = scopes;
37+
}
38+
if (additionalAuthCodeParameters) {
39+
Object.assign(parameters, additionalAuthCodeParameters);
40+
}
41+
const url = authUrl;
42+
return { url, parameters };
43+
}
44+
45+
// is an auth code for access token exchange required for this provider?
46+
exchangeRequired () {
47+
return !this.oauthConfig.noExchange;
48+
}
49+
50+
// does this provider support token refresh?
51+
supportsRefresh () {
52+
return this.oauthConfig.supportsRefresh;
53+
}
54+
55+
// given an auth code, exchange it for an access token
56+
async exchangeAuthCodeForToken (options) {
57+
const { tokenUrl, exchangeFormat } = this.oauthConfig;
58+
const { mockToken } = options;
59+
60+
// must exchange the provided authorization code for an access token,
61+
// prepare parameters for the token exchange request
62+
const parameters = this.prepareTokenExchangeParameters(options);
63+
const url = tokenUrl;
64+
65+
// for testing, we do a mock reply instead of an actual call out to the provider
66+
if (mockToken) {
67+
return this.makeMockData(url, parameters, mockToken);
68+
}
69+
70+
// perform the exchange, which can be from form data, json data, or query
71+
let response;
72+
if (exchangeFormat === 'form') {
73+
response = await this.fetchAccessTokenWithFormData(url, parameters);
74+
}
75+
else if (exchangeFormat === 'query') {
76+
response = await this.fetchAccessTokenWithQuery(url, parameters);
77+
}
78+
else if (exchangeFormat === 'json') {
79+
response = await this.fetchAccessTokenWithJson(url, parameters);
80+
}
81+
const responseData = await response.json();
82+
83+
// normalize and return the token data
84+
return this.normalizeTokenDataResponse(responseData);
85+
}
86+
87+
// prepare parameters for token exchange
88+
prepareTokenExchangeParameters (options) {
89+
const { provider, appIdInAuthorizationHeader, noGrantType } = this.oauthConfig;
90+
const { state, code, redirectUri, refreshToken } = options;
91+
const { appClientId, appClientSecret } = this.api.config[provider];
92+
const parameters = {
93+
redirect_uri: redirectUri
94+
};
95+
if (!noGrantType) {
96+
parameters.grant_type = refreshToken ? 'refresh_token' : 'authorization_code';
97+
}
98+
if (appIdInAuthorizationHeader) {
99+
parameters.__userAuth = Base64.encode(`${appClientId}:${appClientSecret}`);
100+
}
101+
else {
102+
parameters.client_id = appClientId;
103+
parameters.client_secret = appClientSecret;
104+
}
105+
if (refreshToken) {
106+
parameters.refresh_token = refreshToken;
107+
}
108+
else {
109+
parameters.code = code;
110+
parameters.state = state;
111+
}
112+
return parameters;
113+
}
114+
115+
// normalize the received response data after a token exchange
116+
normalizeTokenDataResponse (responseData) {
117+
const { accessTokenExpiresIn } = this.oauthConfig;
118+
if (responseData.error) {
119+
throw responseData;
120+
}
121+
const tokenData = {
122+
accessToken: responseData.access_token
123+
};
124+
if (responseData.refresh_token) {
125+
tokenData.refreshToken = responseData.refresh_token;
126+
}
127+
if (responseData.expires_in) {
128+
tokenData.expiresAt = Date.now() + (responseData.expires_in - 5) * 1000;
129+
}
130+
else if (accessTokenExpiresIn) {
131+
tokenData.expiresAt = Date.now() + (accessTokenExpiresIn - 5) * 1000;
132+
}
133+
const extraData = responseData.data || {};
134+
delete responseData.data;
135+
tokenData.data = Object.assign({}, responseData, extraData);
136+
delete tokenData.data.access_token;
137+
delete tokenData.data.refresh_token;
138+
return tokenData;
139+
}
140+
141+
// make mock token data, instead of an actual call to the provider, for test purposes
142+
makeMockData (url, parameters, mockToken) {
143+
const { accessTokenExpiresIn, mockAccessTokenExpiresIn, exchangeFormat, supportsRefresh } = this.oauthConfig;
144+
const { __userAuth } = parameters;
145+
delete parameters.__userAuth;
146+
147+
if (mockToken === 'error') {
148+
throw { error: 'invalid_grant' };
149+
}
150+
if (exchangeFormat === 'query') {
151+
const query = Object.keys(parameters)
152+
.map(key => `${key}=${encodeURIComponent(parameters[key])}`)
153+
.join('&');
154+
url += `?${query}`;
155+
}
156+
const testCall = { url, parameters };
157+
if (__userAuth) {
158+
testCall.userAuth = __userAuth;
159+
}
160+
const mockData = {
161+
accessToken: mockToken,
162+
_testCall: testCall
163+
};
164+
if (supportsRefresh) {
165+
mockData.refreshToken = 'refreshMe';
166+
}
167+
const expiresIn = mockAccessTokenExpiresIn || accessTokenExpiresIn;
168+
if (expiresIn) {
169+
mockData.expiresAt = Date.now() + (expiresIn - 5) * 1000;
170+
}
171+
return mockData;
172+
}
173+
174+
// fetch access token data by submitting form data in a POST request
175+
async fetchAccessTokenWithFormData (url, parameters) {
176+
const { __userAuth } = parameters;
177+
delete parameters.__userAuth;
178+
179+
const form = new FormData();
180+
Object.keys(parameters).forEach(key => {
181+
form.append(key, parameters[key]);
182+
});
183+
const fetchOptions = {
184+
method: 'post',
185+
body: form
186+
};
187+
if (__userAuth) {
188+
fetchOptions.headers = {
189+
'Authorization': `Basic ${__userAuth}`
190+
};
191+
}
192+
return await fetch(url, fetchOptions);
193+
}
194+
195+
// fetch access token data by submitting a POST request with a query
196+
async fetchAccessTokenWithQuery (url, parameters) {
197+
const query = Object.keys(parameters)
198+
.map(key => `${key}=${encodeURIComponent(parameters[key])}`)
199+
.join('&');
200+
url += `?${query}`;
201+
return await fetch(
202+
url,
203+
{
204+
method: 'post',
205+
headers: { 'Accept': 'application/json' }
206+
}
207+
);
208+
}
209+
210+
// fetch access token data by submitting a POST request with json data
211+
async fetchAccessTokenWithJson (url, parameters) {
212+
return await fetch(
213+
url,
214+
{
215+
method: 'post',
216+
body: JSON.stringify(parameters),
217+
headers: {
218+
'Content-Type': 'application/json'
219+
}
220+
}
221+
);
222+
}
223+
224+
// use a refresh token to obtain a new access token
225+
async refreshToken (options) {
226+
return await this.exchangeAuthCodeForToken(options);
227+
}
228+
229+
// get html to display once auth is complete
230+
getAfterAuthHtml () {
231+
return this.afterAuthHtml;
232+
}
233+
234+
// initialize the module
235+
initialize () {
236+
// read in the after-auth html to display once auth is complete
237+
this.afterAuthHtml = FS.readFileSync(this.path + '/afterAuth.html', { encoding: 'utf8' });
238+
}
239+
}
240+
241+
module.exports = OAuth2Module;

api_server/modules/asana_auth/asana_auth.js

Lines changed: 16 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -2,101 +2,22 @@
22

33
'use strict';
44

5-
const APIServerModule = require(process.env.CS_API_TOP + '/lib/api_server/api_server_module.js');
6-
const fetch = require('node-fetch');
7-
const FS = require('fs');
8-
const FormData = require('form-data');
9-
10-
class AsanaAuth extends APIServerModule {
11-
12-
services () {
13-
return async () => {
14-
return { asanaAuth: this };
15-
};
16-
}
17-
18-
// get redirect parameters and url to use in the redirect response
19-
getRedirectData (options) {
20-
const { request, redirectUri, state } = options;
21-
const { appClientId } = request.api.config.asana;
22-
const parameters = {
23-
client_id: appClientId,
24-
redirect_uri: redirectUri,
25-
response_type: 'code',
26-
state
27-
};
28-
const url = 'https://app.asana.com/-/oauth_authorize';
29-
return { url, parameters };
30-
}
31-
32-
// given an auth code, exchange it for an access token
33-
async exchangeAuthCodeForToken (options) {
34-
// must exchange the provided authorization code for an access token
35-
const { request, state, code, redirectUri, mockToken, refreshToken } = options;
36-
const { appClientId, appClientSecret } = request.api.config.asana;
37-
const parameters = {
38-
grant_type: refreshToken ? 'refresh_token' : 'authorization_code',
39-
client_id: appClientId,
40-
client_secret: appClientSecret,
41-
redirect_uri: redirectUri
42-
};
43-
if (refreshToken) {
44-
parameters.refresh_token = refreshToken;
45-
}
46-
else {
47-
parameters.code = code;
48-
parameters.state = state;
49-
}
50-
const expiresAt = Date.now() + (59 * 60 * 1000 + 55 * 1000); // token good for one hour, we'll give a 5-second margin
51-
const url = 'https://app.asana.com/-/oauth_token';
52-
if (mockToken) {
53-
if (mockToken === 'error') {
54-
throw { error: 'invalid_grant' };
55-
}
56-
return {
57-
accessToken: mockToken,
58-
refreshToken: 'refreshMe',
59-
expiresAt,
60-
_testCall: { url, parameters }
61-
};
62-
}
63-
const form = new FormData();
64-
Object.keys(parameters).forEach(key => {
65-
form.append(key, parameters[key]);
66-
});
67-
const response = await fetch(
68-
url,
69-
{
70-
method: 'post',
71-
body: form
72-
}
73-
);
74-
const responseData = await response.json();
75-
if (responseData.error) {
76-
throw responseData;
77-
}
78-
return {
79-
accessToken: responseData.access_token,
80-
refreshToken: responseData.refresh_token,
81-
expiresAt,
82-
data: responseData.data
83-
};
84-
}
85-
86-
// use a refresh token to obtain a new access token
87-
async refreshToken (options) {
88-
return await this.exchangeAuthCodeForToken(options);
89-
}
90-
91-
// get html to display once auth is complete
92-
getAfterAuthHtml () {
93-
return this.afterAuthHtml;
94-
}
95-
96-
// initialize the module
97-
initialize () {
98-
// read in the after-auth html to display once auth is complete
99-
this.afterAuthHtml = FS.readFileSync(this.path + '/afterAuth.html', { encoding: 'utf8' });
5+
const OAuth2Module = require(process.env.CS_API_TOP + '/lib/oauth2/oauth2_module.js');
6+
7+
const OAUTH_CONFIG = {
8+
provider: 'asana',
9+
authUrl: 'https://app.asana.com/-/oauth_authorize',
10+
tokenUrl: 'https://app.asana.com/-/oauth_token',
11+
exchangeFormat: 'form',
12+
accessTokenExpiresIn: 3600,
13+
supportsRefresh: true
14+
};
15+
16+
class AsanaAuth extends OAuth2Module {
17+
18+
constructor (config) {
19+
super(config);
20+
this.oauthConfig = OAUTH_CONFIG;
10021
}
10122
}
10223

0 commit comments

Comments
 (0)