1 |
|
2 |
|
3 |
|
4 |
|
5 | var qs = require('qs')
|
6 | var url = require('url')
|
7 | var async = require('async')
|
8 | var request = require('request-promise')
|
9 | var clientRoles = require('./rest/clientRoles')
|
10 | var clients = require('./rest/clients')
|
11 | var roles = require('./rest/roles')
|
12 | var roleScopes = require('./rest/roleScopes')
|
13 | var scopes = require('./rest/scopes')
|
14 | var users = require('./rest/users')
|
15 | var userRoles = require('./rest/userRoles')
|
16 | var IDToken = require('./lib/IDToken')
|
17 | var AccessToken = require('./lib/AccessToken')
|
18 | var UnauthorizedError = require('./errors/UnauthorizedError')
|
19 | var JWT = require('anvil-connect-jwt')
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | function AnvilConnect (options) {
|
41 | options = options || {}
|
42 |
|
43 |
|
44 | this.issuer = options.issuer
|
45 | this.client_id = options.client_id
|
46 | this.client_secret = options.client_secret
|
47 | this.redirect_uri = options.redirect_uri
|
48 | this.agentOptions = options.agentOptions
|
49 |
|
50 | this.clients = {
|
51 | list: clients.list.bind(this),
|
52 | get: clients.get.bind(this),
|
53 | create: clients.create.bind(this),
|
54 | update: clients.update.bind(this),
|
55 | delete: clients.delete.bind(this),
|
56 | roles: {
|
57 | list: clientRoles.listRoles.bind(this),
|
58 | add: clientRoles.addRole.bind(this),
|
59 | delete: clientRoles.deleteRole.bind(this)
|
60 | }
|
61 | }
|
62 |
|
63 | this.roles = {
|
64 | list: roles.list.bind(this),
|
65 | get: roles.get.bind(this),
|
66 | create: roles.create.bind(this),
|
67 | update: roles.update.bind(this),
|
68 | delete: roles.delete.bind(this),
|
69 | scopes: {
|
70 | list: roleScopes.listScopes.bind(this),
|
71 | add: roleScopes.addScope.bind(this),
|
72 | delete: roleScopes.deleteScope.bind(this)
|
73 | }
|
74 | }
|
75 |
|
76 | this.scopes = {
|
77 | list: scopes.list.bind(this),
|
78 | get: scopes.get.bind(this),
|
79 | create: scopes.create.bind(this),
|
80 | update: scopes.update.bind(this),
|
81 | delete: scopes.delete.bind(this)
|
82 | }
|
83 |
|
84 | this.users = {
|
85 | list: users.list.bind(this),
|
86 | get: users.get.bind(this),
|
87 | create: users.create.bind(this),
|
88 | update: users.update.bind(this),
|
89 | delete: users.delete.bind(this),
|
90 | roles: {
|
91 | list: userRoles.listRoles.bind(this),
|
92 | add: userRoles.addRole.bind(this),
|
93 | delete: userRoles.deleteRole.bind(this)
|
94 | }
|
95 | }
|
96 |
|
97 |
|
98 | var defaultScope = ['openid', 'profile']
|
99 | if (typeof options.scope === 'string') {
|
100 | this.scope = defaultScope.concat(options.scope.split(' ')).join(' ')
|
101 | } else if (Array.isArray(options.scope)) {
|
102 | this.scope = defaultScope.concat(options.scope).join(' ')
|
103 | } else {
|
104 | this.scope = defaultScope.join(' ')
|
105 | }
|
106 | }
|
107 |
|
108 |
|
109 |
|
110 |
|
111 | AnvilConnect.UnauthorizedError = UnauthorizedError
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 | function discover () {
|
120 | var self = this
|
121 |
|
122 |
|
123 | var uri = url.parse(this.issuer)
|
124 | uri.pathname = '.well-known/openid-configuration'
|
125 | uri = url.format(uri)
|
126 |
|
127 |
|
128 | return new Promise(function (resolve, reject) {
|
129 | request({
|
130 | url: uri,
|
131 | method: 'GET',
|
132 | json: true,
|
133 | agentOptions: self.agentOptions
|
134 | })
|
135 | .then(function (data) {
|
136 |
|
137 | if (typeof data === 'object') {
|
138 | self.configuration = data
|
139 | resolve(data)
|
140 |
|
141 |
|
142 | } else {
|
143 | reject(new Error('Unable to retrieve OpenID Connect configuration'))
|
144 | }
|
145 | })
|
146 | .catch(function (err) {
|
147 | reject(err)
|
148 | })
|
149 | })
|
150 | }
|
151 | AnvilConnect.prototype.discover = discover
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 | function extractIssuer (token) {
|
159 | if (!token) {
|
160 | return
|
161 | }
|
162 |
|
163 | var claims = JWT.decode(token, null, { noVerify: true })
|
164 | return claims.payload.iss
|
165 | }
|
166 | AnvilConnect.prototype.extractIssuer = extractIssuer
|
167 |
|
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 | function getClientAccessToken () {
|
192 | return this
|
193 | .token({
|
194 | grant_type: 'client_credentials',
|
195 | scope: 'realm'
|
196 | })
|
197 | .then(function (tokenResponse) {
|
198 | return tokenResponse.access_token
|
199 | })
|
200 | }
|
201 | AnvilConnect.prototype.getClientAccessToken = getClientAccessToken
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 | function getJWKs () {
|
210 | var self = this
|
211 | var uri = this.configuration.jwks_uri
|
212 |
|
213 | return new Promise(function (resolve, reject) {
|
214 | request({
|
215 | url: uri,
|
216 | method: 'GET',
|
217 | json: true,
|
218 | agentOptions: self.agentOptions
|
219 | })
|
220 | .then(function (data) {
|
221 |
|
222 | data.keys.forEach(function (jwk) {
|
223 | data[jwk.use] = jwk
|
224 | })
|
225 |
|
226 |
|
227 | self.jwks = data
|
228 | resolve(data)
|
229 | })
|
230 | .catch(function (err) {
|
231 | reject(err)
|
232 | })
|
233 | })
|
234 | }
|
235 | AnvilConnect.prototype.getJWKs = getJWKs
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 | function initProvider () {
|
257 | if (!this.issuer) {
|
258 | throw new Error('initClient requires an issuer to be configured')
|
259 | }
|
260 | var self = this
|
261 | return self.discover()
|
262 | .then(function () {
|
263 | return self.getJWKs()
|
264 | })
|
265 | }
|
266 | AnvilConnect.prototype.initProvider = initProvider
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 | function register (options) {
|
300 | var self = this
|
301 | var url = this.configuration.registration_endpoint
|
302 | var token = options.token
|
303 | if (!options.redirect_uris) {
|
304 | throw new Error('Missing required redirect_uris parameter for registration')
|
305 | }
|
306 | var requestOptions = {
|
307 | url: url,
|
308 | method: 'POST',
|
309 | json: options,
|
310 | agentOptions: self.agentOptions
|
311 | }
|
312 | if (token) {
|
313 | requestOptions.headers = {
|
314 | 'Authorization': 'Bearer ' + token
|
315 | }
|
316 | }
|
317 | return Promise.resolve()
|
318 | .then(function () {
|
319 | return request(requestOptions)
|
320 | })
|
321 | .then(function (data) {
|
322 | self.client_id = data.client_id
|
323 | self.client_secret = data.client_secret
|
324 | self.registration = data
|
325 | return data
|
326 | })
|
327 | }
|
328 | AnvilConnect.prototype.register = register
|
329 |
|
330 |
|
331 |
|
332 |
|
333 | function authorizationUri (options) {
|
334 | var u = url.parse(this.configuration.authorization_endpoint)
|
335 |
|
336 |
|
337 | var endpoint = 'authorize'
|
338 | if (typeof options === 'string') {
|
339 | endpoint = options
|
340 | options = {}
|
341 | } else if (typeof options === 'object') {
|
342 | endpoint = options.endpoint
|
343 | } else {
|
344 | options = {}
|
345 | }
|
346 |
|
347 |
|
348 | u.pathname = endpoint
|
349 |
|
350 |
|
351 | u.query = this.authorizationParams(options)
|
352 |
|
353 | return url.format(u)
|
354 | }
|
355 | AnvilConnect.prototype.authorizationUri = authorizationUri
|
356 |
|
357 |
|
358 |
|
359 |
|
360 | function authorizationParams (options) {
|
361 |
|
362 | options = options || {}
|
363 |
|
364 |
|
365 | var params = {
|
366 | response_type: options.response_type || 'code',
|
367 | client_id: this.client_id,
|
368 | redirect_uri: options.redirect_uri || this.redirect_uri,
|
369 | scope: options.scope || this.scope
|
370 | }
|
371 |
|
372 |
|
373 | var optionalParameters = [
|
374 | 'email',
|
375 | 'password',
|
376 | 'provider',
|
377 | 'state',
|
378 | 'response_mode',
|
379 | 'nonce',
|
380 | 'display',
|
381 | 'prompt',
|
382 | 'max_age',
|
383 | 'ui_locales',
|
384 | 'id_token_hint',
|
385 | 'login_hint',
|
386 | 'acr_values'
|
387 | ]
|
388 |
|
389 |
|
390 | optionalParameters.forEach(function (param) {
|
391 | if (options[param]) {
|
392 | params[param] = options[param]
|
393 | }
|
394 | })
|
395 |
|
396 | return params
|
397 | }
|
398 |
|
399 | AnvilConnect.prototype.authorizationParams = authorizationParams
|
400 |
|
401 |
|
402 |
|
403 |
|
404 | function refresh (options) {
|
405 | options = options || {}
|
406 |
|
407 | var self = this
|
408 | var refreshToken = options.refresh_token
|
409 | return new Promise(function (resolve, reject) {
|
410 | if (!refreshToken) {
|
411 | return reject(new Error('Missing refresh_token'))
|
412 | }
|
413 | AccessToken.refresh(refreshToken, {
|
414 | issuer: self.issuer,
|
415 | client_id: self.client_id,
|
416 | client_secret: self.client_secret
|
417 | }, function (err, token) {
|
418 | if (err) {
|
419 | return reject(err)
|
420 | }
|
421 | AccessToken.verify(token.access_token, {
|
422 | key: self.jwks.keys[ 0 ],
|
423 | issuer: self.issuer
|
424 | }, function (err) {
|
425 | if (err) {
|
426 | return reject(err)
|
427 | }
|
428 | return resolve(token)
|
429 | })
|
430 | })
|
431 | })
|
432 | }
|
433 | AnvilConnect.prototype.refresh = refresh
|
434 |
|
435 |
|
436 |
|
437 |
|
438 |
|
439 |
|
440 |
|
441 |
|
442 |
|
443 |
|
444 |
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 |
|
457 |
|
458 |
|
459 |
|
460 |
|
461 |
|
462 |
|
463 | function token (options) {
|
464 | options = options || {}
|
465 |
|
466 | var self = this
|
467 | var uri = this.configuration.token_endpoint
|
468 | var code = options.code
|
469 | var grantType = options.grant_type || 'authorization_code'
|
470 | var scope = options.scope || self.scope
|
471 | var redirectUri = options.redirect_uri || self.redirect_uri
|
472 | var refreshToken = options.refresh_token
|
473 | var formRequestData
|
474 |
|
475 | if (grantType === 'client_credentials') {
|
476 |
|
477 |
|
478 |
|
479 | formRequestData = {
|
480 | grant_type: grantType,
|
481 | scope: scope
|
482 | }
|
483 | } else if (grantType === 'refresh_token') {
|
484 | if (!refreshToken) {
|
485 | return Promise.reject(
|
486 | new Error('Refresh token grant types require refresh_token'))
|
487 | }
|
488 | formRequestData = {
|
489 | grant_type: grantType,
|
490 | refresh_token: refreshToken,
|
491 | scope: scope
|
492 | }
|
493 | } else {
|
494 |
|
495 |
|
496 | if (!code && options.responseUri) {
|
497 | var u = url.parse(options.responseUri)
|
498 | code = qs.parse(u.query).code
|
499 | }
|
500 | if (!code) {
|
501 | return Promise.reject(new Error('Missing authorization code'))
|
502 | }
|
503 | formRequestData = {
|
504 | code: code,
|
505 | grant_type: grantType,
|
506 | redirect_uri: redirectUri
|
507 | }
|
508 | }
|
509 |
|
510 | return new Promise(function (resolve, reject) {
|
511 | request({
|
512 | url: uri,
|
513 | method: 'POST',
|
514 | form: formRequestData,
|
515 | json: true,
|
516 | auth: {
|
517 | user: self.client_id,
|
518 | pass: self.client_secret
|
519 | },
|
520 | agentOptions: self.agentOptions
|
521 | })
|
522 | .then(function (data) {
|
523 | var verifyClaims = {
|
524 | access_claims: function (done) {
|
525 | AccessToken.verify(data.access_token, {
|
526 | key: self.jwks.keys[0],
|
527 | issuer: self.issuer
|
528 | }, function (err, claims) {
|
529 | if (err) { return done(err) }
|
530 | done(null, claims)
|
531 | })
|
532 | }
|
533 | }
|
534 |
|
535 |
|
536 | if (formRequestData.grant_type !== 'client_credentials') {
|
537 | verifyClaims.id_claims = function (done) {
|
538 | IDToken.verify(data.id_token, {
|
539 | iss: self.issuer,
|
540 | aud: self.client_id,
|
541 | key: self.jwks.keys[0]
|
542 | }, function (err, token) {
|
543 | if (err) { return done(err) }
|
544 | done(null, token.payload)
|
545 | })
|
546 | }
|
547 | }
|
548 |
|
549 | async.parallel(verifyClaims, function (err, result) {
|
550 | if (err) {
|
551 | return reject(err)
|
552 | }
|
553 |
|
554 | data.id_claims = result.id_claims
|
555 | data.access_claims = result.access_claims
|
556 |
|
557 | resolve(data)
|
558 | })
|
559 | })
|
560 | .catch(function (err) {
|
561 | reject(err)
|
562 | })
|
563 | })
|
564 | }
|
565 | AnvilConnect.prototype.token = token
|
566 |
|
567 |
|
568 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
574 |
|
575 |
|
576 | function userInfo (options) {
|
577 | options = options || {}
|
578 | var uri = this.configuration.userinfo_endpoint
|
579 | var agentOptions = this.agentOptions
|
580 |
|
581 | if (!options.token) {
|
582 | return Promise.reject(new Error('Missing access token'))
|
583 | }
|
584 |
|
585 | return Promise.resolve()
|
586 | .then(function () {
|
587 |
|
588 | return request({
|
589 | url: uri,
|
590 | method: 'GET',
|
591 | headers: {
|
592 | 'Authorization': 'Bearer ' + options.token
|
593 | },
|
594 | json: true,
|
595 | agentOptions: agentOptions
|
596 | })
|
597 | })
|
598 | }
|
599 | AnvilConnect.prototype.userInfo = userInfo
|
600 |
|
601 |
|
602 |
|
603 |
|
604 |
|
605 |
|
606 |
|
607 |
|
608 |
|
609 |
|
610 |
|
611 |
|
612 |
|
613 |
|
614 | function verify (token, options) {
|
615 | options = options || {}
|
616 | options.issuer = options.issuer || this.issuer
|
617 | options.client_id = options.client_id || this.client_id
|
618 | options.client_secret = options.client_secret || this.client_secret
|
619 | options.scope = options.scope || this.scope
|
620 | options.key = options.key || this.jwks.sig
|
621 |
|
622 | return new Promise(function (resolve, reject) {
|
623 | AccessToken.verify(token, options, function (err, claims) {
|
624 | if (err) { return reject(err) }
|
625 | resolve(claims)
|
626 | })
|
627 | })
|
628 | }
|
629 | AnvilConnect.prototype.verify = verify
|
630 |
|
631 |
|
632 |
|
633 |
|
634 | module.exports = AnvilConnect
|