1 |
|
2 |
|
3 |
|
4 |
|
5 | var 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 |
|
18 |
|
19 |
|
20 | module.exports = {
|
21 |
|
22 |
|
23 | |
24 |
|
25 |
|
26 |
|
27 | provider: {
|
28 |
|
29 |
|
30 | },
|
31 |
|
32 |
|
33 | |
34 |
|
35 |
|
36 |
|
37 | client: {
|
38 |
|
39 |
|
40 | },
|
41 |
|
42 |
|
43 | |
44 |
|
45 |
|
46 |
|
47 | params: {
|
48 |
|
49 |
|
50 |
|
51 | },
|
52 |
|
53 |
|
54 | |
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 | clients: undefined,
|
61 |
|
62 |
|
63 | |
64 |
|
65 |
|
66 |
|
67 | configure: function (options) {
|
68 |
|
69 |
|
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 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
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 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
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 |
|
181 |
|
182 |
|
183 | return uri + qs.stringify(params);
|
184 | },
|
185 |
|
186 |
|
187 | |
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
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 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 | signin: function (options) {
|
221 | options = options || {};
|
222 | options.endpoint = 'signin';
|
223 | return this.authorize(options);
|
224 | },
|
225 |
|
226 |
|
227 | |
228 |
|
229 |
|
230 |
|
231 |
|
232 | signup: function (options) {
|
233 | options = options || {};
|
234 | options.endpoint = 'signup';
|
235 | return this.authorize(options);
|
236 | },
|
237 |
|
238 |
|
239 | |
240 |
|
241 |
|
242 |
|
243 |
|
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 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
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 |
|
291 | if (authResponse.error) {
|
292 | return callback(new CallbackError(authResponse));
|
293 | }
|
294 |
|
295 |
|
296 | var tokenRequest = qs.stringify({
|
297 | grant_type: 'authorization_code',
|
298 | redirect_uri: params.redirectUri,
|
299 | code: authResponse.code
|
300 | });
|
301 |
|
302 |
|
303 | request
|
304 | .post(provider.uri + '/token')
|
305 | .set('Authorization', 'Basic ' + credentials)
|
306 | .send(tokenRequest)
|
307 | .end(function (err, tokenResponse) {
|
308 |
|
309 |
|
310 | if (tokenResponse.error) {
|
311 | return callback(new CallbackError(tokenResponse.body))
|
312 | }
|
313 |
|
314 |
|
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 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
370 |
|
371 |
|
372 |
|
373 |
|
374 |
|
375 |
|
376 |
|
377 |
|
378 |
|
379 |
|
380 |
|
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 |
|
396 | if (response.error) {
|
397 | return callback(new UnauthorizedError(response.body));
|
398 | }
|
399 |
|
400 |
|
401 | callback(null, response.body);
|
402 | });
|
403 | },
|
404 |
|
405 |
|
406 | |
407 |
|
408 |
|
409 |
|
410 |
|
411 |
|
412 |
|
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 |
|
419 |
|
420 |
|
421 |
|
422 |
|
423 |
|
424 |
|
425 |
|
426 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
517 | else {
|
518 | AccessToken.verify(accessToken, {
|
519 |
|
520 |
|
521 |
|
522 | client: client,
|
523 | key: provider.key,
|
524 | issuer: provider.uri,
|
525 | clients: clients,
|
526 | scope: scope
|
527 |
|
528 | }, function (err, token) {
|
529 |
|
530 |
|
531 | if (err) {
|
532 | return next(err);
|
533 | }
|
534 |
|
535 |
|
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 |
|