UNPKG

12 kBJavaScriptView Raw
1var EventEmitter = require('events').EventEmitter;
2var _ = require('lodash');
3var http = require('request');
4var RSVP = require('rsvp');
5var jwt = require('jwt-simple');
6var urls = require('url');
7var util = require('util');
8
9function HipChat(addon, app){
10 var self = this;
11
12 // override the following...
13 addon.middleware = self.middleware;
14 addon.loadClientInfo = self.loadClientInfo;
15 addon.authenticate = self.authenticate;
16 addon._configure = self._configure;
17 addon.getAccessToken = self.getAccessToken;
18
19 // Disable auto-registration... not necessary with HipChat
20 addon.register = function(){
21 self.logger.info('Auto registration not available with HipChat add-ons')
22 };
23
24 addon._verifyKeys = function(){};
25
26 // mixin the addon
27 _.extend(self, addon);
28}
29
30var proto = HipChat.prototype = Object.create(EventEmitter.prototype);
31
32proto.getAccessToken = function(clientInfo, scopes) {
33 var self = this;
34 function generateAccessToken(scopes){
35 return new RSVP.Promise(function(resolve, reject){
36 var tokenUrl = clientInfo.capabilitiesDoc.capabilities.oauth2Provider.tokenUrl;
37 http.post(tokenUrl, {
38 form: {
39 'grant_type': 'client_credentials',
40 'scope': scopes.join(' ')
41 },
42 auth: {
43 user: clientInfo.clientKey,
44 pass: clientInfo.oauthSecret
45 }
46 }, function(err, res, body){
47 if(!err) {
48 try {
49 var token = JSON.parse(body);
50 token.created = new Date().getTime() / 1000;
51 resolve(token);
52 } catch(e) {
53 reject(e);
54 }
55 } else {
56 reject(err);
57 }
58 });
59 });
60 }
61
62 return new RSVP.Promise(function(resolve, reject){
63 scopes = scopes || self.descriptor.capabilities.hipchatApiConsumer.scopes;
64 var scopeKey = scopes.join("|");
65
66 function generate() {
67 generateAccessToken(scopes).then(
68 function(token) {
69 self.settings.set(scopeKey, token, clientInfo.clientKey);
70 resolve(token);
71 },
72 function(err) {
73 reject(err);
74 }
75 );
76 }
77
78 self.settings.get(scopeKey, clientInfo.clientKey).then(function(token){
79 if (token) {
80 if (token.expires_in + token.created < (new Date().getTime() / 1000)) {
81 generate();
82 } else {
83 resolve(token);
84 }
85 } else {
86 generate();
87 }
88 }, function(err) {
89 reject(err);
90 });
91 });
92};
93
94proto._configure = function(){
95 var self = this;
96 var baseUrl = urls.parse(self.config.localBaseUrl());
97 var basePath = baseUrl.path && baseUrl.path.length > 1 ? baseUrl.path : '';
98
99 self.app.get(basePath + '/atlassian-connect.json', function (req, res) {
100 res.json(self.descriptor);
101 });
102
103 // HC Connect install verification flow
104 function verifyInstallation(url){
105 return new RSVP.Promise(function(resolve, reject){
106 http.get(url, function(err, res, body){
107 var data = JSON.parse(body);
108 if(!err){
109 if(data.links.self === url){
110 resolve(data);
111 } else {
112 reject("The capabilities URL " + url + " doesn't match the resource's self link " + data.links.self);
113 }
114 } else {
115 reject(err);
116 }
117 });
118 });
119 };
120
121 // register routes for installable handler
122 if (typeof self.descriptor.capabilities.installable != 'undefined') {
123 var callbackUrl = '/'+self.descriptor.capabilities.installable.callbackUrl.split('/').slice(3).join('/');
124
125 // Install handler
126 self.app.post(
127
128 // mount path
129 callbackUrl,
130
131 // TODO auth middleware
132
133 // request handler
134 function (req, res) {
135 try {
136 verifyInstallation(req.body.capabilitiesUrl)
137 .then(function(hcCapabilities){
138 var clientInfo = {
139 clientKey: req.body.oauthId,
140 oauthSecret: req.body.oauthSecret,
141 capabilitiesUrl: req.body.capabilitiesUrl,
142 capabilitiesDoc: hcCapabilities,
143 roomId: req.body.roomId
144 };
145 var clientKey = clientInfo.clientKey;
146 self.getAccessToken(clientInfo)
147 .then(function(tokenObj){
148 clientInfo.groupId = tokenObj.group_id;
149 clientInfo.groupName = tokenObj.group_name;
150 self.emit('installed', clientKey, clientInfo, req);
151 self.emit('plugin_enabled', clientKey, clientInfo, req);
152 self.settings.set('clientInfo', clientInfo, clientKey).then(function (data) {
153 self.logger.info("Saved tenant details for " + clientKey + " to database\n" + util.inspect(data));
154 self.emit('host_settings_saved', clientKey, data);
155 res.send(204);
156 }, function (err) {
157 res.send(500, 'Could not lookup stored client data for ' + clientKey + ': ' + err);
158 });
159 })
160 .then(null, function(err){
161 res.send(500, err);
162 });
163 })
164 .then(null, function(err){
165 res.send(500, err);
166 }
167 );
168 } catch (e) {
169 res.send(500, e);
170 }
171 }
172 );
173 }
174
175 // uninstall handler
176 self.app.delete(
177 callbackUrl + '/:oauthId',
178 // verify request,
179 function(req, res){
180 try {
181 self.emit('uninstalled', req.params.oauthId);
182 res.send(204);
183 } catch (e) {
184 res.send(500, e);
185 }
186 }
187 );
188}
189
190proto.middleware = function(){
191
192 var addon = this;
193 return function(req, res, next){
194 var hostUrl = req.param('xdmhost');
195 var params;
196
197 if (hostUrl) {
198 params = {
199 hostBaseUrl: hostUrl
200 };
201 _.extend(req.session, params);
202 } else {
203 params = req.session;
204 }
205
206 augmentRequest(params, req, res, next);
207
208 }
209
210 function augmentRequest(params, req, res, next) {
211 if (params && params.hostBaseUrl) {
212 res.locals = _.extend({}, res.locals || {}, params, {
213 title: addon.config.name,
214 appKey: addon.config.key,
215 localBaseUrl: addon.config.localBaseUrl(),
216 hostStylesheetUrl: hostResourceUrl(addon.app, params.hostBaseUrl, 'css'),
217 hostScriptUrl: hostResourceUrl(addon.app, params.hostBaseUrl, 'js')
218 });
219 }
220
221 next();
222 }
223
224 function hostResourceUrl(app, baseUrl, type) {
225 var suffix = app.get('env') === 'development' ? '-debug' : '';
226 return baseUrl + '/atlassian-connect/all' + suffix + '.' + type;
227 }
228}
229
230proto.loadClientInfo = function(clientKey) {
231 var self = this;
232 return new RSVP.Promise(function(resolve, reject){
233 self.settings.get('clientInfo', clientKey).then(function(d){
234 resolve(d);
235 }, function(err) {
236 reject(err);
237 });
238 });
239};
240
241// Middleware to verify jwt token
242proto.authenticate = function(){
243 var self = this;
244
245 return function(req, res, next){
246 function send(code, msg) {
247 self.logger.error('JWT verification error:', code, msg);
248 res.send(code, msg);
249 }
250
251 function success(jwtToken, clientInfo) {
252 if (jwtToken && jwtToken.iss && req.session) {
253 req.session.clientKey = jwtToken.iss;
254 }
255
256 // Refresh the JWT token
257 var now = Math.floor(Date.now()/1000);
258 jwtToken.iat = now
259 // Default maxTokenAge is 15m
260 jwtToken.exp = now + (self.config.maxTokenAge() / 1000);
261 res.locals.signed_request = jwt.encode(jwtToken, clientInfo.oauthSecret);
262 res.set('x-acpt', res.locals.signed_request);
263
264 req.context = jwtToken.context;
265 req.clientInfo = clientInfo;
266 next();
267 }
268
269 var signedRequest = req.query.signed_request || req.headers['x-acpt'];
270 if (signedRequest) {
271 try {
272 // First get the oauthId from the JWT context by decoding it without verifying
273 var unverifiedClaims = jwt.decode(signedRequest, null, true);
274
275 var issuer = unverifiedClaims.iss;
276 if (!issuer) {
277 send('JWT claim did not contain the issuer (iss) claim');
278 return;
279 }
280
281 // Then, let's look up the client's oauthSecret so we can verify the request
282 self.loadClientInfo(issuer).then(function(clientInfo){
283 // verify the signed request
284 if (clientInfo === null) {
285 return send(400, "Request can't be verified without an OAuth secret");
286 }
287 var verifiedClaims = jwt.decode(signedRequest, clientInfo.oauthSecret);
288
289 // JWT expiry can be overriden using the `validityInMinutes` config.
290 // If not set, will use `exp` provided by HC server (default is 1 hour)
291 var now = Math.floor(Date.now()/1000);;
292 if (self.config.maxTokenAge()) {
293 var issuedAt = verifiedClaims.iat;
294 var expiresInSecs = self.config.maxTokenAge() / 1000;
295 if(issuedAt && now >= (issuedAt + expiresInSecs)){
296 send(401, 'Authentication request has expired.');
297 return;
298 }
299
300 } else {
301 var expiry = verifiedClaims.exp;
302 if (expiry && now >= expiry) { // default is 1 hour
303 send(401, 'Authentication request has expired.');
304 return;
305 }
306 }
307
308 success(verifiedClaims, clientInfo);
309 }, function(err) {
310 return send(400, err.message);
311 });
312 } catch(e){
313 return send(400, e.message);
314 }
315 } else if (req.body.oauth_client_id) {
316 self.settings.get('clientInfo', req.body.oauth_client_id).then(function(d){
317 try {
318 req.clientInfo = d;
319 req.context = req.body;
320 next();
321 } catch(e){
322 return send(400, e.message);
323 }
324 });
325 } else {
326 return send(400, "Request not signed and therefore can't be verified");
327 }
328 }
329}
330
331module.exports = function(addon, app){
332 return new HipChat(addon, app);
333}