UNPKG

13.7 kBJavaScriptView Raw
1/**
2 * Module dependencies
3 */
4
5var URL = require('url')
6 , qs = require('qs')
7 , async = require('async')
8 , request = require('superagent')
9 , CallbackError = require('./errors/CallbackError')
10 , IDToken = require('./lib/IDToken')
11 , AccessToken = require('./lib/AccessToken')
12 , UnauthorizedError = require('./errors/UnauthorizedError')
13 ;
14
15
16/**
17 * Anvil Connect Client
18 */
19
20module.exports = {
21
22
23 /**
24 * Anvil Connect Provider Settings
25 */
26
27 provider: {
28 // uri
29 // key
30 },
31
32
33 /**
34 * Registered Client Settings
35 */
36
37 client: {
38 // id
39 // secret
40 },
41
42
43 /**
44 * Default Authorization Request Params
45 */
46
47 params: {
48 // responseType
49 // redirectUri
50 // scope
51 },
52
53
54 /**
55 * Client whitelist
56 *
57 * If this is undefined, all clients are authorized.
58 */
59
60 clients: undefined,
61
62
63 /**
64 * Client Configuration Setter
65 */
66
67 configure: function (options) {
68
69 // validate configuration
70 if (!options) {
71 throw new Error('A valid configuration is required.');
72 }
73
74 if (!options.provider) {
75 throw new Error('A valid provider configuration is required');
76 }
77
78 if (!options.provider.uri) {
79 throw new Error('Provider uri is required');
80 }
81
82 if (!options.provider.key) {
83 request
84 .get(options.provider.uri + '/jwks')
85 .end(function (err, response) {
86 if (err) {
87 throw new Error(
88 "Can't find the signing key. Check your provider uri configuration."
89 );
90 }
91
92 var jwks;
93 if (Array.isArray(response.body)) {
94 jwks = response.body;
95 } else if (response.body && response.body.keys) {
96 jwks = response.body.keys;
97 }
98
99 if (!jwks) {
100 throw new Error(
101 "Can't parse JWK endpoint response."
102 );
103 }
104
105 jwks.forEach(function (jwk) {
106 if (jwk && jwk.use === 'sig') {
107 options.provider.key = jwk;
108 }
109 })
110 });
111 }
112
113 if (!options.client) {
114 throw new Error('A valid client configuration is required');
115 }
116
117 if (!options.client.id) {
118 throw new Error('Client ID is required');
119 }
120
121 //if (!options.client.token) {
122 // throw new Error('Client token is required');
123 //}
124
125 //if (!options.params) {
126 // throw new Error('Valid authorization params configuration is required');
127 //}
128
129 //if (!options.params.redirectUri) {
130 // throw new Error('Redirect URI is required');
131 //}
132
133
134 //// default values
135 //if (!options.params.responseType) {
136 // options.params.responseType = 'code';
137 //}
138
139 //if (!options.params.scope) {
140 // options.params.scope = 'openid profile';
141 //}
142
143
144 // initialize settings
145 this.provider = options.provider;
146 this.client = options.client;
147 this.params = options.params;
148 this.clients = options.clients;
149 },
150
151
152 /**
153 * URI Generator
154 *
155 * Example:
156 *
157 * var uri = anvil.uri({
158 * endpoint: 'signin',
159 * // override defaults here
160 * })
161 */
162
163 uri: function (options) {
164 var anvil = this
165 , options = options || {}
166 , provider = anvil.provider
167 , client = anvil.client
168 , params = anvil.params
169 , uri = anvil.provider.uri + '/'
170 + (options.endpoint || 'authorize') + '?'
171 ;
172
173 var params = {
174 response_type: options.responseType || params.responseType || 'code',
175 redirect_uri: options.redirectUri || params.redirectUri,
176 client_id: options.clientId || client.id,
177 scope: options.scope || params.scope || 'openid profile'
178 };
179
180 // optionally add state onto params
181 // and any other options like prompt/display/etc
182
183 return uri + qs.stringify(params);
184 },
185
186
187 /**
188 * Authorize
189 * - redirect to the authorize endpoint
190 *
191 * app.get('/authorize', anvil.authorize({
192 * // options
193 * }));
194 */
195
196 authorize: function (options) {
197 var anvil = this
198 , options = options || {}
199 ;
200
201 return function (req, res, next) {
202 res.redirect(anvil.uri({
203 endpoint: options.endpoint || 'authorize',
204 responseType: options.responseType,
205 redirectUri: options.redirectUri,
206 clientId: options.clientId,
207 scope: options.scope
208 }));
209 };
210 },
211
212
213 /**
214 * Signin
215 * - redirect directly to signin endpoint
216 *
217 * app.get('/signin', anvil.signin());
218 */
219
220 signin: function (options) {
221 options = options || {};
222 options.endpoint = 'signin';
223 return this.authorize(options);
224 },
225
226
227 /**
228 * Signup
229 * - redirect directly to signup endpoint
230 */
231
232 signup: function (options) {
233 options = options || {};
234 options.endpoint = 'signup';
235 return this.authorize(options);
236 },
237
238
239 /**
240 * Connect a Third Party Account
241 *
242 * app.get('/signin/:provider', anvil.connect({
243 * provider: req.params.provider
244 * }));
245 */
246
247 connect: function (options) {
248 options = options || {};
249 options.provider = options.provider;
250 options.endpoint = 'connect/' + options.provider;
251 return this.authorize(options);
252 },
253
254
255 /**
256 * Callback Handler
257 *
258 * anvil.callback(req.url, function (err, authorization) {
259 *
260 * // `authorization.tokens` contains the auth server's token endpoint response
261 * //
262 * // authorization.tokens.access_token
263 * // authorization.tokens.refresh_token
264 * // authorization.tokens.expires_in
265 * // authorization.tokens.id_token
266 *
267 * // `authorization.identity` contains the decoded and verified claims of the id_token
268 * //
269 * // authorization.identity.iss
270 * // authorization.identity.sub
271 * // authorization.identity.aud
272 * // authorization.identity.exp
273 * // authorization.identity.iat
274 *
275 * });
276 *
277 * Can this be used inside a Passport Strategy?
278 */
279
280 callback: function (uri, callback) {
281 var anvil = this
282 , provider = anvil.provider
283 , client = anvil.client
284 , params = anvil.params
285 , authResponse = URL.parse(uri, true).query
286 , credentials = new Buffer(client.id + ':'
287 + client.secret).toString('base64')
288 ;
289
290 // handle error response from authorization server
291 if (authResponse.error) {
292 return callback(new CallbackError(authResponse));
293 }
294
295 // token request parameters
296 var tokenRequest = qs.stringify({
297 grant_type: 'authorization_code',
298 redirect_uri: params.redirectUri,
299 code: authResponse.code
300 });
301
302 // exchange authorization code for tokens
303 request
304 .post(provider.uri + '/token')
305 .set('Authorization', 'Basic ' + credentials)
306 .send(tokenRequest)
307 .end(function (err, tokenResponse) {
308
309 // Forbidden client or invalid request error
310 if (tokenResponse.error) {
311 return callback(new CallbackError(tokenResponse.body))
312 }
313
314 // Successful token response
315 else {
316
317 async.parallel({
318 id_claims: function (done) {
319 IDToken.verify(tokenResponse.body.id_token, {
320 iss: provider.uri,
321 aud: client.id,
322 key: provider.key
323 }, function (err, token) {
324 if (err) { return done(err); }
325 done(null, token.payload);
326 });
327 },
328
329 access_claims: function (done) {
330 AccessToken.verify(tokenResponse.body.access_token, {
331 client: client,
332 key: provider.key,
333 issuer: provider.uri
334 }, function (err, claims) {
335 if (err) { return done(err); }
336 done(null, claims);
337 });
338 }
339 }, function (err, result) {
340 if (err) { return callback(err); }
341
342 tokenResponse.body.id_claims = result.id_claims;
343 tokenResponse.body.access_claims = result.access_claims;
344
345
346 callback(null, tokenResponse.body);
347 });
348 }
349 });
350 },
351
352
353 /**
354 * UserInfo
355 *
356 * anvil.userInfo(accessToken, function (err, info) {
357 *
358 * // `info` contains basic account information for the user
359 * // represented by the accessToken argument.
360 * //
361 * // info.sub
362 * // info.name
363 * // info.given_name
364 * // info.family_name
365 * // info.middle_name
366 * // info.nickname
367 * // info.perferred_username
368 * // info.profile
369 * // info.picture
370 * // info.website
371 * // info.email
372 * // info.email_verified
373 * // info.gender
374 * // info.birthdate
375 * // info.zoneinfo
376 * // info.locale
377 * // info.phone_number
378 * // info.phone_number_verified
379 * // info.address
380 * // info.updated_at
381 *
382 * });
383 */
384
385 userInfo: function (accessToken, callback) {
386 var anvil = this
387 , provider = anvil.provider
388 ;
389
390 request
391 .get(anvil.provider.uri + '/userinfo')
392 .set('Authorization', 'Bearer ' + accessToken)
393 .set('Accept', 'application/json')
394 .end(function (err, response) {
395 // error response from authorization server
396 if (response.error) {
397 return callback(new UnauthorizedError(response.body));
398 }
399
400 // success
401 callback(null, response.body);
402 });
403 },
404
405
406 /**
407 * Verify credentials at API endpoints
408 *
409 * This should comply with RFC6750:
410 * http://tools.ietf.org/html/rfc6750
411 *
412 * Use as route specific middleware:
413 *
414 * var authorize = anvil.verify({ scope: 'research' });
415 *
416 * server.post('/protected', authorize, function (req, res, next) {
417 * // handle the request
418 * });
419 *
420 * Or protect the entire server:
421 *
422 * server.use(anvil.verify({
423 * scope: 'research',
424 * clients: [
425 * 'uuid1',
426 * 'uuid2'
427 * ]
428 * }));
429 *
430 */
431
432 verify: function (options) {
433 var anvil = this
434 , provider = anvil.provider
435 , client = anvil.client
436 , options = options || {}
437 , clients = options.clients || anvil.clients
438 , scope = options.scope
439 , key = provider.key
440 ;
441
442 return function (req, res, next) {
443 var accessToken;
444
445 // Check for an access token in the Authorization header
446 if (req.headers && req.headers.authorization) {
447 var components = req.headers.authorization.split(' ')
448 , scheme = components[0]
449 , credentials = components[1]
450 ;
451
452 if (components.length !== 2) {
453 return next(new UnauthorizedError({
454 error: 'invalid_request',
455 error_description: 'Invalid authorization header',
456 statusCode: 400
457 }));
458 }
459
460 if (scheme !== 'Bearer') {
461 return next(new UnauthorizedError({
462 error: 'invalid_request',
463 error_description: 'Invalid authorization scheme',
464 statusCode: 400
465 }));
466 }
467
468 accessToken = credentials;
469 }
470
471 // Check for an access token in the request URI
472 if (req.query && req.query.access_token) {
473 if (accessToken) {
474 return next(new UnauthorizedError({
475 error: 'invalid_request',
476 error_description: 'Multiple authentication methods',
477 statusCode: 400
478 }));
479 }
480
481 accessToken = req.query.access_token
482 }
483
484 // Check for an access token in the request body
485 if (req.body && req.body.access_token) {
486 if (accessToken) {
487 return next(new UnauthorizedError({
488 error: 'invalid_request',
489 error_description: 'Multiple authentication methods',
490 statusCode: 400
491 }));
492 }
493
494 if (req.headers
495 && req.headers['content-type'] !== 'application/x-www-form-urlencoded') {
496 return next(new UnauthorizedError({
497 error: 'invalid_request',
498 error_description: 'Invalid content-type',
499 statusCode: 400
500 }));
501 }
502
503 accessToken = req.body.access_token
504 }
505
506 // Missing access token
507 if (!accessToken) {
508 return next(new UnauthorizedError({
509 realm: 'user',
510 error: 'invalid_request',
511 error_description: 'An access token is required',
512 statusCode: 400
513 }));
514 }
515
516 // Access token found
517 else {
518 AccessToken.verify(accessToken, {
519
520 // Token validation parameters
521 //jwt: client.token,
522 client: client,
523 key: provider.key,
524 issuer: provider.uri,
525 clients: clients,
526 scope: scope
527
528 }, function (err, token) {
529
530 // Validation error
531 if (err) {
532 return next(err);
533 }
534
535 // Make the token metadata available downstream
536 req.token = token;
537 next();
538
539 });
540 }
541 }
542 },
543
544
545 IDToken: IDToken,
546 AccessToken: AccessToken,
547 CallbackError: CallbackError,
548 UnauthorizedError: UnauthorizedError
549
550};
551