2 /* eslint-disable capitalized-comments, sonarjs/no-duplicate-string, sonarjs/no-identical-functions */
6 const assert
= require('assert');
7 const sinon
= require('sinon'); // eslint-disable-line node/no-unpublished-require
9 const Manager
= require('../../src/manager');
10 const Config
= require('../../config');
11 const Enum
= require('../../src/enum');
12 const { ResponseError
} = require('../../src/errors');
13 const { UnexpectedResult
} = require('../../src/db/errors');
14 const dns
= require('dns');
16 const StubDatabase
= require('../stub-db');
17 const StubLogger
= require('../stub-logger');
19 const expectedException
= new Error('oh no');
20 const noExpectedException
= 'did not get expected exception';
22 describe('Manager', function () {
23 let manager
, options
, stubDb
, logger
;
26 beforeEach(function () {
27 logger
= new StubLogger();
29 stubDb
= new StubDatabase();
31 options
= new Config('test');
33 getHeader : sinon
.stub(),
37 setHeader: sinon
.stub(),
48 manager
= new Manager(logger
, stubDb
, options
);
49 sinon
.stub(manager
.communication
, 'fetchProfile');
50 sinon
.stub(manager
.communication
, 'fetchClientIdentifier');
51 sinon
.stub(manager
.communication
, 'deliverTicket');
52 sinon
.stub(manager
.communication
, 'redeemTicket');
53 sinon
.stub(dns
.promises
, 'lookup').resolves([{ family: 4, address: '10.11.12.13' }]);
54 sinon
.stub(manager
.queuePublisher
, 'connect');
55 sinon
.stub(manager
.queuePublisher
, 'establishAMQPPlumbing');
56 sinon
.stub(manager
.queuePublisher
, 'publish');
57 sinon
.stub(manager
.queueConsumer
, 'connect');
58 sinon
.stub(manager
.queueConsumer
, 'establishAMQPPlumbing');
59 sinon
.stub(manager
.queueConsumer
, 'consume');
62 afterEach(function () {
66 describe('constructor', function () {
67 it('instantiates', function () {
70 it('covers no queuing', function () {
71 options
.queues
.amqp
.url
= undefined;
72 manager
= new Manager(logger
, stubDb
, options
);
77 describe('initialize', function () {
79 beforeEach(function () {
80 spy
= sinon
.spy(manager
, '_connectQueues');
82 it('covers', async
function () {
83 await manager
.initialize();
86 it('covers no queue', async
function () {
87 delete options
.queues
.amqp
.url
;
88 manager
= new Manager(logger
, stubDb
, options
);
89 await manager
.initialize();
90 assert(spy
.notCalled
);
94 describe('getRoot', function () {
95 it('normal response', async
function () {
96 await manager
.getRoot(res
, ctx
);
97 assert(res
.end
.called
);
101 describe('getMeta', function () {
102 it('normal response', async
function () {
103 await manager
.getMeta(res
, ctx
);
104 assert(res
.end
.called
);
105 JSON
.parse(res
.end
.args
[0][0]);
107 it('covers no ticket queue', async
function () {
108 delete options
.queues
.amqp
.url
;
109 manager
= new Manager(logger
, stubDb
, options
);
110 await manager
.getMeta(res
, ctx
);
111 assert(res
.end
.called
);
115 describe('getHealthcheck', function () {
116 it('normal response', async
function () {
117 await manager
.getHealthcheck(res
, ctx
);
118 assert(res
.end
.called
);
120 }); // getHealthcheck
122 describe('getAuthorization', function () {
123 it('covers missing redirect fields', async
function () {
124 await manager
.getAuthorization(res
, ctx
);
125 assert
.strictEqual(res
.statusCode
, 400);
127 it('requires a configured profile', async
function () {
128 manager
.db
.profilesScopesByIdentifier
.resolves({
143 manager
.communication
.fetchClientIdentifier
.resolves({
146 ctx
.authenticationId
= 'username';
147 Object
.assign(ctx
.queryParams
, {
148 'client_id': 'https://client.example.com/',
149 'redirect_uri': 'https://client.example.com/action',
150 'response_type': 'code',
152 'code_challenge_method': 'S256',
153 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
154 'scope': 'profile email',
156 await manager
.getAuthorization(res
, ctx
);
157 assert
.strictEqual(res
.statusCode
, 302);
158 assert(ctx
.session
.error
);
159 assert(res
.setHeader
.called
);
161 it('covers valid', async
function () {
162 manager
.db
.profilesScopesByIdentifier
.resolves({
164 'https://profile.example.com/': {
167 profiles: ['https://profile.example.com'],
182 profiles: ['https://profile.example.com/'],
185 profiles: ['https://profile.example.com/'],
187 manager
.communication
.fetchClientIdentifier
.resolves({
190 ctx
.authenticationId
= 'username';
191 Object
.assign(ctx
.queryParams
, {
192 'client_id': 'https://client.example.com/',
193 'redirect_uri': 'https://client.example.com/action',
194 'response_type': 'code',
196 'code_challenge_method': 'S256',
197 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
198 'scope': 'profile email',
199 'me': 'https://profile.example.com/',
201 await manager
.getAuthorization(res
, ctx
);
202 assert
.strictEqual(res
.statusCode
, 200);
203 assert
.strictEqual(ctx
.session
.error
, undefined);
204 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 0);
205 assert
.strictEqual(ctx
.notifications
.length
, 0);
207 it('succeeds with mismatched profile hint', async
function () {
208 manager
.db
.profilesScopesByIdentifier
.resolves({
210 'https://profile.example.com/': {
213 profiles: ['https://profile.example.com'],
228 profiles: ['https://profile.example.com/'],
231 profiles: ['https://profile.example.com/'],
233 manager
.communication
.fetchClientIdentifier
.resolves({
236 ctx
.authenticationId
= 'username';
237 Object
.assign(ctx
.queryParams
, {
238 'client_id': 'https://client.example.com/',
239 'redirect_uri': 'https://client.example.com/action',
240 'response_type': 'code',
242 'code_challenge_method': 'S256',
243 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
244 'scope': 'profile email',
245 'me': 'https://somethingelse.example.com/',
247 await manager
.getAuthorization(res
, ctx
);
248 assert(!('me' in ctx
.session
));
249 assert
.strictEqual(res
.statusCode
, 200);
250 assert
.strictEqual(ctx
.session
.error
, undefined);
251 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 0);
253 it('covers invalid redirect', async
function () {
254 manager
.db
.profilesScopesByIdentifier
.resolves({
256 'https://profile.example.com/': {
259 profiles: ['https://profile.example.com'],
274 profiles: ['https://profile.example.com/'],
277 profiles: ['https://profile.example.com/'],
279 manager
.communication
.fetchClientIdentifier
.resolves({
282 ctx
.authenticationId
= 'username';
283 Object
.assign(ctx
.queryParams
, {
284 'client_id': 'https://client.example.com/',
285 'redirect_uri': 'https://client.example.com/action',
286 'response_type': 'blargl',
288 'code_challenge_method': 'S256',
289 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
291 await manager
.getAuthorization(res
, ctx
);
292 assert
.strictEqual(res
.statusCode
, 302);
293 assert
.strictEqual(ctx
.session
.error
, 'invalid_request');
294 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 2);
296 it('covers legacy non-PKCE missing fields', async
function () {
297 manager
.db
.profilesScopesByIdentifier
.resolves({
299 'https://profile.example.com/': {
302 profiles: ['https://profile.example.com'],
317 profiles: ['https://profile.example.com/'],
320 profiles: ['https://profile.example.com/'],
322 manager
.communication
.fetchClientIdentifier
.resolves({
325 ctx
.authenticationId
= 'username';
326 Object
.assign(ctx
.queryParams
, {
327 'client_id': 'https://client.example.com/',
328 'redirect_uri': 'https://client.example.com/action',
329 'response_type': 'code',
331 'scope': 'profile email',
332 'me': 'https://profile.example.com/',
334 manager
.options
.manager
.allowLegacyNonPKCE
= true;
336 await manager
.getAuthorization(res
, ctx
);
337 assert
.strictEqual(res
.statusCode
, 200);
338 assert
.strictEqual(ctx
.session
.error
, undefined);
339 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 0);
341 it('rejects legacy non-PKCE not missing all fields', async
function () {
342 manager
.db
.profilesScopesByIdentifier
.resolves({
344 'https://profile.example.com/': {
347 profiles: ['https://profile.example.com'],
362 profiles: ['https://profile.example.com/'],
365 profiles: ['https://profile.example.com/'],
367 manager
.communication
.fetchClientIdentifier
.resolves({
370 ctx
.authenticationId
= 'username';
371 Object
.assign(ctx
.queryParams
, {
372 'client_id': 'https://client.example.com/',
373 'redirect_uri': 'https://client.example.com/action',
374 'response_type': 'code',
375 'code_challenge_method': 'S256',
377 'scope': 'profile email',
378 'me': 'https://profile.example.com/',
380 manager
.options
.manager
.allowLegacyNonPKCE
= true;
382 await manager
.getAuthorization(res
, ctx
);
383 assert
.strictEqual(res
.statusCode
, 302);
384 assert
.strictEqual(ctx
.session
.error
, 'invalid_request');
385 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 1);
387 it('rejects legacy non-PKCE not missing all fields', async
function () {
388 manager
.db
.profilesScopesByIdentifier
.resolves({
390 'https://profile.example.com/': {
393 profiles: ['https://profile.example.com'],
408 profiles: ['https://profile.example.com/'],
411 profiles: ['https://profile.example.com/'],
413 manager
.communication
.fetchClientIdentifier
.resolves({
416 ctx
.authenticationId
= 'username';
417 Object
.assign(ctx
.queryParams
, {
418 'client_id': 'https://client.example.com/',
419 'redirect_uri': 'https://client.example.com/action',
420 'response_type': 'code',
421 'code_challenge': 'xxx',
423 'scope': 'profile email',
424 'me': 'https://profile.example.com/',
426 manager
.options
.manager
.allowLegacyNonPKCE
= true;
428 await manager
.getAuthorization(res
, ctx
);
429 assert
.strictEqual(res
.statusCode
, 302);
430 assert
.strictEqual(ctx
.session
.error
, 'invalid_request');
431 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 1);
433 }); // getAuthorization
435 describe('_setError', function () {
436 it('covers', function () {
437 const err
= 'invalid_request';
438 const errDesc
= 'something went wrong';
439 Manager
._setError(ctx
, err
, errDesc
);
441 it('covers bad error', function () {
442 const err
= 'floopy';
443 const errDesc
= 'something went wrong';
445 Manager
._setError(ctx
, err
, errDesc
);
446 assert
.fail(noExpectedException
);
448 assert(e
instanceof RangeError
);
451 it('covers invalid error description', function () {
452 const err
= 'invalid_scope';
453 const errDesc
= 'something "went wrong"!';
455 Manager
._setError(ctx
, err
, errDesc
);
456 assert
.fail(noExpectedException
);
458 assert(e
instanceof RangeError
);
463 describe('_clientIdRequired', function () {
464 let clientIdentifier
;
465 beforeEach(function () {
469 manager
.communication
.fetchClientIdentifier
.resolves(clientIdentifier
);
471 it('covers valid', async
function () {
472 ctx
.queryParams
['client_id'] = 'https://client.example.com/';
474 await manager
._clientIdRequired(ctx
);
476 assert
.deepStrictEqual(ctx
.session
.clientIdentifier
, clientIdentifier
);
477 assert
.strictEqual(ctx
.session
.error
, undefined);
478 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
480 it('requires client_id', async
function () {
481 ctx
.queryParams
['client_id'] = undefined;
483 await manager
._clientIdRequired(ctx
);
485 assert(ctx
.session
.error
);
486 assert(ctx
.session
.errorDescriptions
.length
);
488 it('requires valid client_id', async
function () {
489 ctx
.queryParams
['client_id'] = 'not a url';
491 await manager
._clientIdRequired(ctx
);
493 assert(ctx
.session
.error
);
494 assert(ctx
.session
.errorDescriptions
.length
);
496 it('rejects strange schema', async
function () {
497 ctx
.queryParams
['client_id'] = 'file:///etc/shadow';
499 await manager
._clientIdRequired(ctx
);
501 assert(ctx
.session
.error
);
502 assert(ctx
.session
.errorDescriptions
.length
);
504 it('rejects un-allowed parts', async
function () {
505 ctx
.queryParams
['client_id'] = 'https://user:pass@client.example.com/#here';
507 await manager
._clientIdRequired(ctx
);
509 assert(ctx
.session
.error
);
510 assert(ctx
.session
.errorDescriptions
.length
);
512 it('rejects relative paths', async
function () {
513 ctx
.queryParams
['client_id'] = 'https://client.example.com/x/../y/';
515 await manager
._clientIdRequired(ctx
);
517 assert(ctx
.session
.error
);
518 assert(ctx
.session
.errorDescriptions
.length
);
520 it('rejects ipv6 hostname', async
function () {
521 ctx
.queryParams
['client_id'] = 'https://[fd12:3456:789a:1::1]/';
523 await manager
._clientIdRequired(ctx
);
525 assert(ctx
.session
.error
);
526 assert(ctx
.session
.errorDescriptions
.length
);
528 it('allows ipv6 loopback hostname', async
function () {
529 ctx
.queryParams
['client_id'] = 'https://[::1]/';
531 await manager
._clientIdRequired(ctx
);
533 assert
.deepStrictEqual(ctx
.session
.clientIdentifier
, clientIdentifier
);
534 assert
.strictEqual(ctx
.session
.error
, undefined);
535 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
537 it('rejects ipv4 hostname', async
function () {
538 ctx
.queryParams
['client_id'] = 'https://10.9.8.7/';
540 await manager
._clientIdRequired(ctx
);
542 assert(ctx
.session
.error
);
543 assert(ctx
.session
.errorDescriptions
.length
);
545 it('allows ipv4 loopback hostname', async
function () {
546 ctx
.queryParams
['client_id'] = 'https:/127.0.10.100/';
548 await manager
._clientIdRequired(ctx
);
550 assert
.deepStrictEqual(ctx
.session
.clientIdentifier
, clientIdentifier
);
551 assert
.strictEqual(ctx
.session
.error
, undefined);
552 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
554 it('requires response', async
function () {
555 manager
.communication
.fetchClientIdentifier
.restore();
556 sinon
.stub(manager
.communication
, 'fetchClientIdentifier').resolves();
557 ctx
.queryParams
['client_id'] = 'https://client.example.com/';
559 await manager
._clientIdRequired(ctx
);
561 assert(ctx
.session
.error
);
562 assert(ctx
.session
.errorDescriptions
.length
);
564 }); // _clientIdRequired
566 describe('_redirectURIRequired', function () {
567 beforeEach(function () {
568 ctx
.session
.clientId
= new URL('https://client.example.com/');
569 ctx
.session
.clientIdentifier
= {
571 'redirect_uri': ['https://alternate.example.com/', 'https://other.example.com/'],
575 it('covers valid', function () {
576 ctx
.queryParams
['redirect_uri'] = 'https://client.example.com/return';
578 Manager
._redirectURIRequired(ctx
);
580 assert
.strictEqual(ctx
.session
.error
, undefined);
581 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
583 it('requires redirect_uri', function () {
584 ctx
.queryParams
['redirect_uri'] = undefined;
586 Manager
._redirectURIRequired(ctx
);
588 assert(ctx
.session
.error
);
589 assert(ctx
.session
.errorDescriptions
.length
);
591 it('requires valid redirect_uri', function () {
592 ctx
.queryParams
['redirect_uri'] = 'not a url';
594 Manager
._redirectURIRequired(ctx
);
596 assert(ctx
.session
.error
);
597 assert(ctx
.session
.errorDescriptions
.length
);
599 it('rejects no matching alternate redirect_uri from client_id', function () {
600 ctx
.queryParams
['redirect_uri'] = 'https://unlisted.example.com/';
602 Manager
._redirectURIRequired(ctx
);
604 assert(ctx
.session
.error
);
605 assert(ctx
.session
.errorDescriptions
.length
);
607 it('allows alternate redirect_uri from client_id', function () {
608 ctx
.queryParams
['redirect_uri'] = 'https://alternate.example.com/';
610 Manager
._redirectURIRequired(ctx
);
612 assert
.strictEqual(ctx
.session
.error
, undefined);
613 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
615 }); // _redirectURIRequired
617 describe('_responseTypeRequired', function () {
618 it('covers valid', function () {
619 ctx
.queryParams
['response_type'] = 'code';
621 Manager
._responseTypeRequired(ctx
);
623 assert
.strictEqual(ctx
.session
.error
, undefined);
624 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
626 it('requires response_type', function () {
627 ctx
.queryParams
['response_type'] = undefined;
629 Manager
._responseTypeRequired(ctx
);
631 assert(ctx
.session
.error
);
632 assert(ctx
.session
.errorDescriptions
.length
);
634 it('rejects invalid', function () {
635 ctx
.queryParams
['response_type'] = 'flarp';
637 Manager
._responseTypeRequired(ctx
);
639 assert(ctx
.session
.error
);
640 assert(ctx
.session
.errorDescriptions
.length
);
642 }); // _responseTypeRequired
644 describe('_stateRequired', function () {
645 it('covers valid', function () {
646 ctx
.queryParams
['state'] = 'StateStateState';
648 Manager
._stateRequired(ctx
);
650 assert
.strictEqual(ctx
.session
.error
, undefined);
651 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
653 it('requires state', function () {
654 ctx
.queryParams
['state'] = undefined;
656 Manager
._stateRequired(ctx
);
658 assert(ctx
.session
.error
);
659 assert(ctx
.session
.errorDescriptions
.length
);
661 }); // _stateRequired
663 describe('_codeChallengeMethodRequired', function () {
664 it('covers valid', function () {
665 ctx
.queryParams
['code_challenge_method'] = 'S256';
667 manager
._codeChallengeMethodRequired(ctx
);
669 assert
.strictEqual(ctx
.session
.error
, undefined);
670 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
672 it('requires code_challenge_method', function () {
673 ctx
.queryParams
['code_challenge_method'] = undefined;
675 manager
._codeChallengeMethodRequired(ctx
);
677 assert(ctx
.session
.error
);
678 assert(ctx
.session
.errorDescriptions
.length
);
680 it('rejects invalid', function () {
681 ctx
.queryParams
['code_challenge_method'] = 'MD5';
683 manager
._codeChallengeMethodRequired(ctx
);
685 assert(ctx
.session
.error
);
686 assert(ctx
.session
.errorDescriptions
.length
);
688 it('covers legacy non-PKCE', function () {
689 ctx
.queryParams
['code_challenge_method'] = undefined;
690 manager
.options
.manager
.allowLegacyNonPKCE
= true;
692 manager
._codeChallengeMethodRequired(ctx
);
694 assert
.strictEqual(ctx
.session
.error
, undefined);
695 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
697 }); // _codeChallengeMethodRequired
699 describe('_codeChallengeRequired', function () {
700 it('covers valid', function () {
701 ctx
.queryParams
['code_challenge'] = 'NBKNqs1TfjQFqpewPNOstmQ5MJnLoeTTbjqtQ9JbZOo';
703 manager
._codeChallengeRequired(ctx
);
705 assert
.strictEqual(ctx
.session
.error
, undefined);
706 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
708 it('requires code_challenge', function () {
709 ctx
.queryParams
['code_challenge'] = undefined;
711 manager
._codeChallengeRequired(ctx
);
713 assert(ctx
.session
.error
);
714 assert(ctx
.session
.errorDescriptions
.length
);
716 it('rejects invalid', function () {
717 ctx
.queryParams
['code_challenge'] = 'not base64/url encoded';
719 manager
._codeChallengeRequired(ctx
);
721 assert(ctx
.session
.error
);
722 assert(ctx
.session
.errorDescriptions
.length
);
724 it('covers legacy non-PKCE', function () {
725 ctx
.queryParams
['code_challenge'] = undefined;
726 manager
.options
.manager
.allowLegacyNonPKCE
= true;
728 manager
._codeChallengeRequired(ctx
);
730 assert
.strictEqual(ctx
.session
.error
, undefined);
731 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
734 }); // _codeChallengeRequired
736 describe('_redirectURIRequired', function () {
737 beforeEach(function () {
738 sinon
.stub(Manager
, '_setError');
739 ctx
.queryParams
['redirect_uri'] = 'https://example.com/redirect';
740 ctx
.session
.clientId
= new URL('https://example.com/');
742 it('requires redirect_uri', function () {
743 delete ctx
.queryParams
['redirect_uri'];
744 Manager
._redirectURIRequired(ctx
);
745 assert(Manager
._setError
.called
);
747 it('requires valid redirect_uri', function () {
748 ctx
.queryParams
['redirect_uri'] = 'not a uri';
749 Manager
._redirectURIRequired(ctx
);
750 assert(Manager
._setError
.called
);
752 it('sets redirectUri if no clientId', function () {
753 delete ctx
.session
.clientId
;
754 Manager
._redirectURIRequired(ctx
);
755 assert(Manager
._setError
.notCalled
);
756 assert(ctx
.session
.redirectUri
instanceof URL
);
758 it('sets redirectUri if clientId matches', function () {
759 Manager
._redirectURIRequired(ctx
);
760 assert(Manager
._setError
.notCalled
);
761 assert(ctx
.session
.redirectUri
instanceof URL
);
763 it('rejects mis-matched', function () {
764 ctx
.queryParams
['redirect_uri'] = 'https://example.com:8080/redirect';
765 Manager
._redirectURIRequired(ctx
);
766 assert(Manager
._setError
.called
);
767 assert
.strictEqual(ctx
.session
.redirectUri
, undefined);
769 it('allows client-specified alternate redirect uri', function () {
770 ctx
.session
.clientIdentifier
= {
772 'redirect_uri': ['https://alternate.example.com/redirect'],
775 ctx
.queryParams
['redirect_uri'] = 'https://alternate.example.com/redirect';
776 Manager
._redirectURIRequired(ctx
);
777 assert(Manager
._setError
.notCalled
);
778 assert(ctx
.session
.redirectUri
instanceof URL
);
780 }); // _redirectURIRequired
782 describe('_scopeOptional', function () {
783 it('covers valid', function () {
784 ctx
.queryParams
['scope'] = 'profile email';
785 manager
._scopeOptional(ctx
);
786 assert
.strictEqual(ctx
.session
.error
, undefined);
787 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
788 assert
.strictEqual(ctx
.session
.scope
.length
, 2);
790 it('allows empty', function () {
791 ctx
.queryParams
['scope'] = undefined;
792 manager
._scopeOptional(ctx
);
793 assert
.strictEqual(ctx
.session
.error
, undefined);
794 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
795 assert
.strictEqual(ctx
.session
.scope
.length
, 0);
797 it('rejects invalid scope combination', function () {
798 ctx
.queryParams
['scope'] = 'email';
799 manager
._scopeOptional(ctx
);
800 assert(ctx
.session
.error
);
801 assert(ctx
.session
.errorDescriptions
.length
);
803 it('ignores invalid scope', function () {
804 ctx
.queryParams
['scope'] = 'profile email "funny_business"';
805 manager
._scopeOptional(ctx
);
806 assert
.strictEqual(ctx
.session
.error
, undefined);
807 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
808 assert
.strictEqual(ctx
.session
.scope
.length
, 2);
810 }); // _scopeOptional
812 describe('_meOptional', function () {
813 this.beforeEach(function () {
814 ctx
.queryParams
['me'] = 'https://profile.example.com/';
816 it('covers valid', async
function () {
817 await manager
._meOptional(ctx
);
819 assert
.strictEqual(ctx
.session
.me
.href
, ctx
.queryParams
['me']);
821 it('ignore invalid', async
function () {
822 ctx
.queryParams
['me'] = 'not a url';
824 await manager
._meOptional(ctx
);
826 assert
.strictEqual(ctx
.session
.me
, undefined);
828 it('allows empty', async
function () {
829 ctx
.queryParams
['me'] = undefined;
831 await manager
._meOptional(ctx
);
833 assert
.strictEqual(ctx
.session
.me
, undefined);
837 describe('_profileValidForIdentifier', function () {
838 beforeEach(function () {
840 profiles: ['https://profile.example.com/', 'https://example.com/profile'],
841 me: new URL('https://example.com/profile'),
844 it('covers valid', async
function () {
846 const result
= await manager
._profileValidForIdentifier(ctx
);
848 assert
.strictEqual(result
, true);
850 it('covers missing me', async
function () {
851 delete ctx
.session
.me
;
853 const result
= await manager
._profileValidForIdentifier(ctx
);
855 assert
.strictEqual(result
, false);
857 }); // _profileValidForIdentifier
859 describe('_parseLifespan', function () {
860 let field
, customField
;
861 beforeEach(function () {
863 customField
= 'lifespan-seconds';
864 ctx
.parsedBody
['lifespan'] = undefined;
865 ctx
.parsedBody
['lifespan-seconds'] = undefined;
867 it('returns nothing without fields', function () {
868 const result
= manager
._parseLifespan(ctx
, field
, customField
);
869 assert
.strictEqual(result
, undefined);
871 it('returns nothing for unrecognized field', function () {
872 ctx
.parsedBody
['lifespan'] = 'a while';
873 const result
= manager
._parseLifespan(ctx
, field
, customField
);
874 assert
.strictEqual(result
, undefined);
876 it('returns recognized preset value', function () {
877 ctx
.parsedBody
['lifespan'] = '1d';
878 const result
= manager
._parseLifespan(ctx
, field
, customField
);
879 assert
.strictEqual(result
, 86400);
881 it('returns valid custom value', function () {
882 ctx
.parsedBody
['lifespan'] = 'custom';
883 ctx
.parsedBody
['lifespan-seconds'] = '123';
884 const result
= manager
._parseLifespan(ctx
, field
, customField
);
885 assert
.strictEqual(result
, 123);
887 it('returns nothing for invalid custom value', function () {
888 ctx
.parsedBody
['lifespan'] = 'custom';
889 ctx
.parsedBody
['lifespan-seconds'] = 'Not a number';
890 const result
= manager
._parseLifespan(ctx
, field
, customField
);
891 assert
.strictEqual(result
, undefined);
893 it('returns nothing for invalid custom value', function () {
894 ctx
.parsedBody
['lifespan'] = 'custom';
895 ctx
.parsedBody
['lifespan-seconds'] = '-50';
896 const result
= manager
._parseLifespan(ctx
, field
, customField
);
897 assert
.strictEqual(result
, undefined);
899 }); // _parseLifespan
901 describe('_parseConsentScopes', function () {
902 it('covers no scopes', function () {
903 const result
= manager
._parseConsentScopes(ctx
);
904 assert
.deepStrictEqual(result
, []);
906 it('filters invalid scopes', function () {
907 ctx
.parsedBody
['accepted_scopes[]'] = ['read', 'email'];
908 ctx
.parsedBody
['ad_hoc_scopes'] = 'bad"scope create ';
909 const result
= manager
._parseConsentScopes(ctx
);
910 assert
.deepStrictEqual(result
, ['read', 'create']);
912 }); // _parseConsentScopes
914 describe('_parseConsentMe', function () {
915 beforeEach(function () {
916 ctx
.session
.profiles
= ['https://me.example.com/'];
918 it('covers valid', function () {
919 const expected
= 'https://me.example.com/';
920 ctx
.parsedBody
['me'] = expected
;
921 const result
= manager
._parseConsentMe(ctx
);
923 assert
.strictEqual(result
.href
, expected
);
925 it('rejects unsupported', function () {
926 ctx
.parsedBody
['me'] = 'https://notme.example.com/';
927 const result
= manager
._parseConsentMe(ctx
);
929 assert(ctx
.session
.error
);
931 it('rejects invalid', function () {
932 ctx
.parsedBody
['me'] = 'bagel';
933 const result
= manager
._parseConsentMe(ctx
);
935 assert(ctx
.session
.error
);
937 }); // _parseConsentMe
939 describe('_fetchConsentProfileData', function () {
941 beforeEach(function () {
943 url: 'https://profile.example.com/',
945 manager
.communication
.fetchProfile
.resolves(profileResponse
);
947 it('covers success', async
function () {
948 const expected
= profileResponse
;
949 const result
= await manager
._fetchConsentProfileData(ctx
);
950 assert
.deepStrictEqual(result
, expected
);
951 assert(!ctx
.session
.error
);
953 it('covers empty response', async
function () {
954 manager
.communication
.fetchProfile
.resolves();
955 const result
= await manager
._fetchConsentProfileData(ctx
);
956 assert
.deepStrictEqual(result
, undefined);
957 assert(ctx
.session
.error
);
959 it('covers failure', async
function () {
960 manager
.communication
.fetchProfile
.rejects();
961 const result
= await manager
._fetchConsentProfileData(ctx
);
962 assert
.deepStrictEqual(result
, undefined);
963 assert(ctx
.session
.error
);
965 }); // _fetchConsentProfileData
967 describe('postConsent', function () {
969 beforeEach(function () {
970 sinon
.stub(manager
.mysteryBox
, 'unpack');
971 sinon
.stub(manager
.mysteryBox
, 'pack');
972 manager
.communication
.fetchProfile
.resolves({
973 url: 'https://profile.example.com/',
976 clientId: 'https://example.com/',
977 redirectUri: 'https://example.com/_redirect',
978 profiles: ['https://profile.example.com/'],
980 manager
.mysteryBox
.unpack
.resolves(oldSession
);
981 ctx
.parsedBody
['me'] = 'https://profile.example.com/';
982 ctx
.parsedBody
['accept'] = 'true';
984 it('covers valid', async
function () {
985 await manager
.postConsent(res
, ctx
);
986 assert(!ctx
.session
.error
, ctx
.session
.error
);
987 assert
.strictEqual(res
.statusCode
, 302);
989 it('covers valid with expiration and refresh', async
function () {
990 ctx
.parsedBody
['expires'] = '1d';
991 ctx
.parsedBody
['refresh'] = '1w';
992 await manager
.postConsent(res
, ctx
);
993 assert(!ctx
.session
.error
, ctx
.session
.error
);
994 assert
.strictEqual(res
.statusCode
, 302);
996 it('covers denial', async
function () {
997 ctx
.parsedBody
['accept'] = 'false';
998 await manager
.postConsent(res
, ctx
);
999 assert(ctx
.session
.error
);
1000 assert
.strictEqual(ctx
.session
.error
, 'access_denied');
1001 assert
.strictEqual(res
.statusCode
, 302);
1003 it('covers profile fetch failure', async
function () {
1004 manager
.communication
.fetchProfile
.resolves();
1005 await manager
.postConsent(res
, ctx
);
1006 assert
.strictEqual(res
.statusCode
, 302);
1007 assert(ctx
.session
.error
);
1009 it('covers bad code', async
function () {
1010 manager
.mysteryBox
.unpack
.rejects();
1011 await manager
.postConsent(res
, ctx
);
1012 assert
.strictEqual(res
.statusCode
, 400);
1013 assert(ctx
.session
.error
);
1015 it('removes email scope without profile', async
function () {
1016 ctx
.parsedBody
['accepted_scopes[]'] = ['email', 'create'];
1017 await manager
.postConsent(res
, ctx
);
1018 assert(!ctx
.session
.acceptedScopes
.includes('email'));
1020 it('merges valid ad-hoc scopes', async
function () {
1021 ctx
.parsedBody
['accepted_scopes[]'] = ['email', 'create'];
1022 ctx
.parsedBody
['ad_hoc_scopes'] = ' my:scope "badScope';
1023 await manager
.postConsent(res
, ctx
);
1024 assert(ctx
.session
.acceptedScopes
.includes('my:scope'));
1026 it('covers invalid selected me profile', async
function () {
1027 ctx
.parsedBody
['me'] = 'https://different.example.com/';
1028 await manager
.postConsent(res
, ctx
);
1029 assert(ctx
.session
.error
);
1031 it('covers invalid me url', async
function () {
1032 ctx
.parsedBody
['me'] = 'bagel';
1033 await manager
.postConsent(res
, ctx
);
1034 assert(ctx
.session
.error
);
1036 it('covers profile fetch error', async
function () {
1037 manager
.communication
.fetchProfile
.rejects(expectedException
);
1038 await manager
.postConsent(res
, ctx
);
1039 assert
.strictEqual(res
.statusCode
, 302);
1040 assert(ctx
.session
.error
);
1044 describe('postAuthorization', function () {
1045 let code
, parsedBody
;
1046 beforeEach(function () {
1047 sinon
.stub(manager
.mysteryBox
, 'unpack');
1049 codeId: 'cffe1558-35f0-11ec-98bc-0025905f714a',
1050 codeChallengeMethod: 'S256',
1051 codeChallenge: 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ',
1052 clientId: 'https://app.example.com/',
1053 redirectUri: 'https://app.example.com/_redirect',
1054 acceptedScopes: ['profile'],
1056 me: 'https://client.example.com/',
1057 identifier: 'username',
1059 name: 'Firsty McLastname',
1060 email: 'f.mclastname@example.com',
1064 code: 'codeCodeCode',
1065 'client_id': 'https://app.example.com/',
1066 'redirect_uri': 'https://app.example.com/_redirect',
1067 'grant_type': 'authorization_code',
1068 'code_verifier': 'verifier',
1071 it('covers valid', async
function () {
1072 manager
.db
.redeemCode
.resolves(true);
1073 manager
.mysteryBox
.unpack
.resolves(code
);
1074 Object
.assign(ctx
.parsedBody
, parsedBody
);
1076 await manager
.postAuthorization(res
, ctx
);
1077 assert(!ctx
.session
.error
, ctx
.session
.error
);
1078 assert(!res
.end
.firstCall
.args
[0].includes('email'));
1080 it('includes email if accepted in scope', async
function () {
1081 code
.acceptedScopes
= ['profile', 'email'];
1082 manager
.db
.redeemCode
.resolves(true);
1083 manager
.mysteryBox
.unpack
.resolves(code
);
1084 Object
.assign(ctx
.parsedBody
, parsedBody
);
1086 await manager
.postAuthorization(res
, ctx
);
1087 assert(!ctx
.session
.error
);
1088 assert(res
.end
.firstCall
.args
[0].includes('email'));
1090 it('fails if already redeemed', async
function () {
1091 manager
.db
.redeemCode
.resolves(false);
1092 manager
.mysteryBox
.unpack
.resolves(code
);
1093 Object
.assign(ctx
.parsedBody
, parsedBody
);
1095 await manager
.postAuthorization(res
, ctx
);
1096 assert(ctx
.session
.error
);
1098 it('covers bad request', async
function () {
1099 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1100 Object
.assign(ctx
.parsedBody
, parsedBody
);
1102 await manager
.postAuthorization(res
, ctx
);
1103 assert(ctx
.session
.error
);
1105 }); // postAuthorization
1107 describe('_ingestPostAuthorizationRequest', function () {
1108 beforeEach(function () {
1109 sinon
.stub(manager
, '_restoreSessionFromCode');
1110 sinon
.stub(manager
, '_checkSessionMatchingClientId');
1111 sinon
.stub(manager
, '_checkSessionMatchingRedirectUri');
1112 sinon
.stub(manager
, '_checkGrantType');
1113 sinon
.stub(manager
, '_checkSessionMatchingCodeVerifier');
1115 it('covers valid', async
function () {
1116 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1118 me: 'https://profile.example.com/',
1123 await manager
._ingestPostAuthorizationRequest(ctx
);
1124 assert(!ctx
.session
.error
);
1126 it('requires data', async
function () {
1127 delete ctx
.parsedBody
;
1128 await manager
._ingestPostAuthorizationRequest(ctx
);
1129 assert(ctx
.session
.error
);
1131 it('requires me field', async
function () {
1132 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1137 await manager
._ingestPostAuthorizationRequest(ctx
);
1138 assert(ctx
.session
.error
);
1140 it('requires minted field', async
function () {
1141 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1143 me: 'https://profile.example.com/',
1146 await manager
._ingestPostAuthorizationRequest(ctx
);
1147 assert(ctx
.session
.error
);
1149 it('rejects expired code', async
function () {
1150 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1152 me: 'https://profile.example.com/',
1153 minted: Date
.now() - 86400000,
1157 await manager
._ingestPostAuthorizationRequest(ctx
);
1158 assert(ctx
.session
.error
);
1160 }); // _ingestPostAuthorizationRequest
1162 describe('_restoreSessionFromCode', function () {
1164 beforeEach(function () {
1165 sinon
.stub(manager
.mysteryBox
, 'unpack');
1167 codeId: 'cffe1558-35f0-11ec-98bc-0025905f714a',
1168 codeChallengeMethod: 'S256',
1169 codeChallenge: 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ',
1170 clientId: 'https://app.example.com/',
1171 redirectUri: 'https://app.example.com/_redirect',
1172 acceptedScopes: ['profile'],
1174 me: 'https://client.example.com/',
1175 identifier: 'username',
1177 name: 'Firsty McLastname',
1178 email: 'f.mclastname@example.com',
1182 it('covers valid', async
function () {
1183 ctx
.parsedBody
['code'] = 'codeCodeCode';
1184 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1185 const expected
= Object
.assign({}, ctx
, {
1186 session: unpackedCode
,
1188 await manager
._restoreSessionFromCode(ctx
);
1189 assert
.deepStrictEqual(ctx
, expected
);
1190 assert(!ctx
.session
.error
);
1192 it('requires code', async
function () {
1193 ctx
.parsedBody
['code'] = '';
1194 manager
.mysteryBox
.unpack
.resolves({
1195 me: 'https://example.com/me',
1197 await manager
._restoreSessionFromCode(ctx
);
1198 assert(ctx
.session
.error
);
1200 it('covers invalid code', async
function () {
1201 ctx
.parsedBody
['code'] = 'codeCodeCode';
1202 manager
.mysteryBox
.unpack
.rejects();
1203 await manager
._restoreSessionFromCode(ctx
);
1204 assert(ctx
.session
.error
);
1206 it('covers missing code fields', async
function () {
1207 ctx
.parsedBody
['code'] = 'codeCodeCode';
1208 delete unpackedCode
.clientId
;
1209 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1210 await manager
._restoreSessionFromCode(ctx
);
1211 assert(ctx
.session
.error
);
1213 it('covers legacy non-PKCE missing fields', async
function () {
1214 ctx
.parsedBody
['code'] = 'codeCodeCode';
1215 delete unpackedCode
.codeChallengeMethod
;
1216 delete unpackedCode
.codeChallenge
;
1217 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1218 manager
.options
.manager
.allowLegacyNonPKCE
= true;
1219 const expected
= Object
.assign({}, ctx
, {
1220 session: unpackedCode
,
1222 await manager
._restoreSessionFromCode(ctx
);
1223 assert
.deepStrictEqual(ctx
, expected
);
1224 assert(!ctx
.session
.error
);
1226 }); // _restoreSessionFromCode
1228 describe('_checkSessionMatchingClientId', function () {
1229 it('covers valid', async
function () {
1231 clientId: 'https://client.example.com/',
1233 ctx
.parsedBody
['client_id'] = 'https://client.example.com/';
1235 manager
._checkSessionMatchingClientId(ctx
);
1236 assert(!ctx
.session
.error
);
1238 it('covers missing', async
function () {
1240 clientId: 'https://client.example.com/',
1242 ctx
.parsedBody
['client_id'] = undefined;
1244 manager
._checkSessionMatchingClientId(ctx
);
1245 assert(ctx
.session
.error
);
1247 it('covers un-parsable', async
function () {
1249 clientId: 'https://client.example.com/',
1251 ctx
.parsedBody
['client_id'] = 'not a url';
1253 manager
._checkSessionMatchingClientId(ctx
);
1254 assert(ctx
.session
.error
);
1256 it('covers mismatch', async
function () {
1258 clientId: 'https://client.example.com/',
1260 ctx
.parsedBody
['client_id'] = 'https://otherclient.example.com/';
1262 manager
._checkSessionMatchingClientId(ctx
);
1263 assert(ctx
.session
.error
);
1265 }); // _checkSessionMatchingClientId
1267 describe('_checkSessionMatchingRedirectUri', function () {
1268 it('covers valid', async
function () {
1269 ctx
.parsedBody
['redirect_uri'] = 'https://client.example.com/_redirect';
1270 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1272 manager
._checkSessionMatchingRedirectUri(ctx
);
1273 assert(!ctx
.session
.error
);
1275 it('requires field', async
function () {
1276 ctx
.parsedBody
['redirect_uri'] = undefined;
1277 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1279 manager
._checkSessionMatchingRedirectUri(ctx
);
1280 assert(ctx
.session
.error
);
1282 it('requires valid field', async
function () {
1283 ctx
.parsedBody
['redirect_uri'] = 'not a url';
1284 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1286 manager
._checkSessionMatchingRedirectUri(ctx
);
1287 assert(ctx
.session
.error
);
1289 it('requires match', async
function () {
1290 ctx
.parsedBody
['redirect_uri'] = 'https://client.example.com/other';
1291 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1293 manager
._checkSessionMatchingRedirectUri(ctx
);
1294 assert(ctx
.session
.error
);
1296 }); // _checkSessionMatchingRedirectUri
1298 describe('_checkGrantType', function () {
1299 it('covers valid', async
function () {
1300 ctx
.parsedBody
['grant_type'] = 'authorization_code';
1302 manager
._checkGrantType(ctx
);
1303 assert(!ctx
.session
.error
);
1305 it('allows missing, because of one client', async
function () {
1306 ctx
.parsedBody
['grant_type'] = undefined;
1308 manager
._checkGrantType(ctx
);
1309 assert(!ctx
.session
.error
);
1311 it('rejects invalid', async
function () {
1312 ctx
.parsedBody
['grant_type'] = 'pigeon_dance';
1314 manager
._checkGrantType(ctx
);
1315 assert(ctx
.session
.error
);
1317 }); // _checkGrantType
1319 describe('_checkSessionMatchingCodeVerifier', function () {
1320 it('covers valid', async
function () {
1321 ctx
.parsedBody
['code_verifier'] = 'verifier';
1322 ctx
.session
.codeChallengeMethod
= 'S256';
1323 ctx
.session
.codeChallenge
= 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ';
1325 manager
._checkSessionMatchingCodeVerifier(ctx
);
1326 assert(!ctx
.session
.error
);
1328 it('requires field', async
function () {
1329 ctx
.parsedBody
['code_verifier'] = undefined;
1330 ctx
.session
.codeChallengeMethod
= 'S256';
1331 ctx
.session
.codeChallenge
= 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ';
1333 manager
._checkSessionMatchingCodeVerifier(ctx
);
1334 assert(ctx
.session
.error
);
1336 it('requires match', async
function () {
1337 ctx
.parsedBody
['code_verifier'] = 'wrongverifier';
1338 ctx
.session
.codeChallengeMethod
= 'S256';
1339 ctx
.session
.codeChallenge
= 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ';
1341 manager
._checkSessionMatchingCodeVerifier(ctx
);
1342 assert(ctx
.session
.error
);
1344 it('covers legacy non-PKCE missing fields', async
function () {
1345 ctx
.parsedBody
['code_verifier'] = undefined;
1346 ctx
.session
.codeChallengeMethod
= undefined;
1347 ctx
.session
.codeChallenge
= undefined;
1348 manager
.options
.manager
.allowLegacyNonPKCE
= true;
1350 manager
._checkSessionMatchingCodeVerifier(ctx
);
1351 assert(!ctx
.session
.error
);
1353 }); // _checkSessionMatchingCodeVerifier
1355 describe('postToken', function () {
1357 beforeEach(function () {
1358 ctx
.session
.acceptedScopes
= [];
1360 codeId: 'cffe1558-35f0-11ec-98bc-0025905f714a',
1361 codeChallengeMethod: 'S256',
1362 codeChallenge: 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ',
1363 clientId: 'https://app.example.com/',
1364 redirectUri: 'https://app.example.com/return',
1365 acceptedScopes: ['profile', 'email', 'tricks'],
1367 me: 'https://client.example.com/',
1368 identifier: 'username',
1370 name: 'Firsty McLastname',
1371 email: 'f.mclastname@example.com',
1372 url: 'https://example.com/',
1376 describe('Revocation (legacy)', function () {
1377 beforeEach(function () {
1378 sinon
.stub(manager
, '_revokeToken');
1380 it('covers revocation', async
function () {
1381 manager
._revokeToken
.resolves();
1386 await manager
.postToken(req
, res
, ctx
);
1387 assert(manager
._revokeToken
.called
);
1390 describe('Validation (legacy)', function () {
1391 beforeEach(function () {
1392 sinon
.stub(manager
, '_validateToken');
1393 req
.getHeader
.returns({ Authorization: 'Bearer XXX' });
1395 it('covers validation', async
function () {
1396 ctx
.bearer
= { isValid: true };
1397 await manager
.postToken(req
, res
, ctx
);
1398 assert(manager
._validateToken
.called
);
1401 describe('Refresh', function () {
1402 beforeEach(function () {
1403 sinon
.stub(manager
, '_refreshToken');
1405 it('covers refresh', async
function () {
1406 ctx
.parsedBody
['grant_type'] = 'refresh_token';
1407 await manager
.postToken(req
, res
, ctx
);
1408 assert(manager
._refreshToken
.called
);
1411 describe('Ticket Redemption', function () {
1412 beforeEach(function () {
1413 sinon
.stub(manager
, '_ticketAuthToken');
1415 it('covers ticket', async
function () {
1416 ctx
.parsedBody
['grant_type'] = 'ticket';
1417 await manager
.postToken(req
, res
, ctx
);
1418 assert(manager
._ticketAuthToken
.called
);
1420 it('covers no ticket queue', async
function () {
1421 delete options
.queues
.amqp
.url
;
1422 manager
= new Manager(logger
, stubDb
, options
);
1423 sinon
.stub(manager
.communication
, 'fetchProfile');
1424 sinon
.stub(manager
.communication
, 'fetchClientIdentifier');
1425 sinon
.stub(manager
.communication
, 'deliverTicket');
1427 ctx
.parsedBody
['grant_type'] = 'ticket';
1428 await assert
.rejects(() => manager
.postToken(req
, res
, ctx
), ResponseError
);
1430 }); // Ticket Redemption
1431 describe('Code Redemption', function () {
1432 beforeEach(function () {
1433 sinon
.stub(manager
.mysteryBox
, 'unpack');
1434 sinon
.spy(manager
.mysteryBox
, 'pack');
1435 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1437 'redirect_uri': 'https://app.example.com/return',
1441 it('covers invalid code', async
function () {
1442 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1444 await manager
.postToken(req
, res
, ctx
);
1445 assert
.fail(noExpectedException
);
1447 assert(e
instanceof ResponseError
);
1450 it('covers mismatched redirect', async
function () {
1451 ctx
.parsedBody
['redirect_uri'] = 'https://elsewhere.example.com/';
1453 await manager
.postToken(req
, res
, ctx
);
1454 assert
.fail(noExpectedException
);
1456 assert(e
instanceof ResponseError
);
1459 it('covers success', async
function () {
1460 manager
.db
.redeemCode
.resolves(true);
1461 await manager
.postToken(req
, res
, ctx
);
1462 assert(res
.end
.called
);
1463 assert
.strictEqual(manager
.mysteryBox
.pack
.callCount
, 1);
1465 it('covers success with refresh', async
function () {
1466 manager
.db
.redeemCode
.resolves(true);
1467 unpackedCode
.refreshLifespan
= 86400;
1468 unpackedCode
.tokenLifespan
= 86400;
1469 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1470 await manager
.postToken(req
, res
, ctx
);
1471 assert(res
.end
.called
);
1472 assert
.strictEqual(manager
.mysteryBox
.pack
.callCount
, 2);
1474 it('covers redemption failure', async
function () {
1475 manager
.db
.redeemCode
.resolves(false);
1477 await manager
.postToken(req
, res
, ctx
);
1478 assert
.fail(noExpectedException
);
1480 assert(e
instanceof ResponseError
);
1483 it('removes email from profile if not in scope', async
function () {
1484 manager
.db
.redeemCode
.resolves(true);
1485 unpackedCode
.acceptedScopes
= ['profile', 'tricks'];
1486 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1487 await manager
.postToken(req
, res
, ctx
);
1488 assert(res
.end
.called
);
1489 const response
= JSON
.parse(res
.end
.args
[0][0]);
1490 assert(!('email' in response
.profile
));
1493 }); // Code Redemption
1494 describe('Invalid grant_type', function () {
1495 it('throws response error', async
function () {
1496 ctx
.parsedBody
['grant_type'] = 'bad';
1498 await manager
.postToken(req
, res
, ctx
);
1499 assert
.fail(noExpectedException
);
1501 assert(e
instanceof ResponseError
);
1504 }); // Invalid grant_type
1507 describe('_validateToken', function () {
1509 beforeEach(function () {
1511 sinon
.stub(manager
, '_checkTokenValidationRequest');
1513 it('covers valid token', async
function () {
1519 await manager
._validateToken(dbCtx
, req
, res
, ctx
);
1520 assert(res
.end
.called
);
1522 it('covers invalid token', async
function () {
1526 await assert
.rejects(manager
._validateToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1528 it('covers errors', async
function () {
1532 ctx
.session
.error
= 'error';
1533 ctx
.session
.errorDescriptions
= ['error_description'];
1534 await assert
.rejects(manager
._validateToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1536 }); // _validateToken
1538 describe('_checkTokenValidationRequest', function () {
1540 beforeEach(function () {
1542 sinon
.stub(manager
.mysteryBox
, 'unpack');
1544 it('does nothing with no auth header', async
function () {
1545 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1547 it('does nothing with unknown auth header', async
function () {
1548 req
.getHeader
.returns('flarp authy woo');
1549 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1551 it('requires a valid auth token', async
function () {
1552 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1553 req
.getHeader
.returns('Bearer XXX');
1554 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1555 assert(ctx
.session
.error
);
1557 it('requires valid auth token fields', async
function () {
1558 manager
.mysteryBox
.unpack
.resolves({});
1559 req
.getHeader
.returns('Bearer XXX');
1560 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1561 assert(ctx
.session
.error
)
1563 it('covers no token', async
function () {
1564 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1565 req
.getHeader
.returns('Bearer XXX');
1566 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1567 assert(ctx
.session
.error
)
1569 it('covers db error', async
function () {
1570 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1571 manager
.db
.tokenGetByCodeId
.rejects(expectedException
);
1572 req
.getHeader
.returns('Bearer XXX');
1573 await assert
.rejects(manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
), expectedException
);
1575 it('valid token', async
function () {
1576 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1577 manager
.db
.tokenGetByCodeId
.resolves({
1579 expires: new Date(Date
.now() + 86400000),
1581 req
.getHeader
.returns('Bearer XXX');
1582 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1583 assert
.strictEqual(ctx
.bearer
.isValid
, true);
1585 it('revoked token', async
function () {
1586 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1587 manager
.db
.tokenGetByCodeId
.resolves({
1589 expires: new Date(Date
.now() + 86400000),
1591 req
.getHeader
.returns('Bearer XXX');
1592 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1593 assert
.strictEqual(ctx
.bearer
.isValid
, false);
1595 it('expired token', async
function () {
1596 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1597 manager
.db
.tokenGetByCodeId
.resolves({
1599 expires: new Date(Date
.now() - 86400000),
1601 req
.getHeader
.returns('Bearer XXX');
1602 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1603 assert
.strictEqual(ctx
.bearer
.isValid
, false);
1605 }); // _checkTokenValidationRequest
1607 describe('postIntrospection', function () {
1608 let inactiveToken
, activeToken
, dbResponse
;
1609 beforeEach(function () {
1611 profile: 'https://profile.example.com/',
1612 clientId: 'https://client.example.com/',
1613 scopes: ['scope1', 'scope2'],
1614 created: new Date(),
1617 inactiveToken
= JSON
.stringify({
1620 activeToken
= JSON
.stringify({
1622 me: dbResponse
.profile
,
1623 'client_id': dbResponse
.clientId
,
1624 scope: dbResponse
.scopes
.join(' '),
1625 iat: Math
.ceil(dbResponse
.created
.getTime() / 1000),
1627 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '7e9991dc-9cd5-11ec-85c4-0025905f714a' });
1628 manager
.db
.tokenGetByCodeId
.resolves(dbResponse
);
1630 it('covers bad token', async
function () {
1631 manager
.mysteryBox
.unpack
.rejects();
1632 await manager
.postIntrospection(res
, ctx
);
1633 assert(res
.end
.called
);
1634 assert
.strictEqual(res
.end
.args
[0][0], inactiveToken
);
1636 it('covers token not in db', async
function () {
1637 manager
.db
.tokenGetByCodeId
.resolves();
1638 await manager
.postIntrospection(res
, ctx
);
1639 assert(res
.end
.called
);
1640 assert
.strictEqual(res
.end
.args
[0][0], inactiveToken
);
1642 it('covers valid token', async
function () {
1643 await manager
.postIntrospection(res
, ctx
);
1644 assert(res
.end
.called
);
1645 assert
.strictEqual(res
.end
.args
[0][0], activeToken
);
1647 it('covers expired token', async
function () {
1648 dbResponse
.expires
= new Date((new Date()).getTime() - 86400000);
1649 await manager
.postIntrospection(res
, ctx
);
1650 assert(res
.end
.called
);
1651 assert
.strictEqual(res
.end
.args
[0][0], inactiveToken
);
1653 it('covers expiring token', async
function () {
1654 dbResponse
.expires
= new Date((new Date()).getTime() + 86400000);
1655 activeToken
= JSON
.stringify({
1657 me: dbResponse
.profile
,
1658 'client_id': dbResponse
.clientId
,
1659 scope: dbResponse
.scopes
.join(' '),
1660 iat: Math
.ceil(dbResponse
.created
.getTime() / 1000),
1661 exp: Math
.ceil(dbResponse
.expires
/ 1000),
1663 await manager
.postIntrospection(res
, ctx
);
1664 assert(res
.end
.called
);
1665 assert
.strictEqual(res
.end
.args
[0][0], activeToken
);
1667 it('covers ticket', async
function () {
1668 ctx
.parsedBody
['token_hint_type'] = 'ticket';
1669 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
1670 manager
.mysteryBox
.unpack
.resolves({
1671 c: '515172ae-5b0b-11ed-a6af-0025905f714a',
1672 iss: nowEpoch
- 86400,
1673 exp: nowEpoch
+ 86400,
1674 sub: 'https://subject.exmaple.com/',
1675 res: 'https://profile.example.com/feed',
1676 scope: ['read', 'role:private'],
1678 profile: 'https://profile.example.com/',
1680 await manager
.postIntrospection(res
, ctx
);
1681 assert(res
.end
.called
);
1683 }); // postIntrospection
1685 describe('_revokeToken', function () {
1687 beforeEach(function () {
1690 it('requires token field', async
function () {
1691 await manager
._revokeToken(dbCtx
, res
, ctx
);
1692 assert(res
.end
.called
);
1693 assert
.strictEqual(res
.statusCode
, 400);
1695 it('requires parsable token', async
function () {
1696 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ notC: 'foop' });
1697 ctx
.parsedBody
['token'] = 'invalid token';
1698 ctx
.parsedBody
['token_type_hint'] = 'access_token';
1699 await manager
._revokeToken(dbCtx
, res
, ctx
);
1700 assert(res
.end
.called
);
1701 assert
.strictEqual(res
.statusCode
, 400);
1703 it('requires parsable token', async
function () {
1704 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves();
1705 ctx
.parsedBody
['token'] = 'invalid token';
1706 ctx
.parsedBody
['token_type_hint'] = 'refresh_token';
1707 await manager
._revokeToken(dbCtx
, res
, ctx
);
1708 assert(res
.end
.called
);
1709 assert
.strictEqual(res
.statusCode
, 400);
1711 it('succeeds', async
function () {
1712 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1713 ctx
.parsedBody
['token'] = 'valid token';
1714 await manager
._revokeToken(dbCtx
, res
, ctx
);
1715 assert(manager
.db
.tokenRevokeByCodeId
.called
);
1716 assert(res
.end
.called
);
1718 it('succeeds for refresh token', async
function () {
1719 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ rc: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1720 ctx
.parsedBody
['token'] = 'valid token';
1721 await manager
._revokeToken(dbCtx
, res
, ctx
);
1722 assert(manager
.db
.tokenRefreshRevokeByCodeId
.called
);
1723 assert(res
.end
.called
);
1725 it('covers non-revokable token', async
function () {
1726 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1727 manager
.db
.tokenRevokeByCodeId
.rejects(new UnexpectedResult());
1728 ctx
.parsedBody
['token'] = 'valid token';
1729 await manager
._revokeToken(dbCtx
, res
, ctx
);
1730 assert
.strictEqual(res
.statusCode
, 404);
1732 it('covers failure', async
function () {
1733 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1734 manager
.db
.tokenRevokeByCodeId
.rejects(expectedException
);
1735 ctx
.parsedBody
['token'] = 'valid token';
1736 ctx
.parsedBody
['token_type_hint'] = 'ignores_bad_hint';
1737 await assert
.rejects(manager
._revokeToken(dbCtx
, res
, ctx
), expectedException
, noExpectedException
);
1741 describe('_scopeDifference', function () {
1742 let previousScopes
, requestedScopes
;
1743 beforeEach(function () {
1744 previousScopes
= ['a', 'b', 'c'];
1745 requestedScopes
= ['b', 'c', 'd'];
1747 it('covers', function () {
1748 const expected
= ['a'];
1749 const result
= Manager
._scopeDifference(previousScopes
, requestedScopes
);
1750 assert
.deepStrictEqual(result
, expected
);
1752 }); // _scopeDifference
1754 describe('_refreshToken', function () {
1756 beforeEach(function () {
1758 ctx
.parsedBody
['client_id'] = 'https://client.example.com/';
1759 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
1760 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
1761 rc: '03bb8ab0-1dc7-11ed-99f2-0025905f714a',
1762 ts: nowEpoch
- 86400,
1763 exp: nowEpoch
+ 86400,
1765 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('newToken');
1766 const futureDate
= new Date(Date
.now() + 86400000);
1767 manager
.db
.tokenGetByCodeId
.resolves({
1768 refreshExpires: futureDate
,
1770 clientId: 'https://client.example.com/',
1771 scopes: ['profile', 'create'],
1773 manager
.db
.refreshCode
.resolves({
1774 expires: futureDate
,
1775 refreshExpires: futureDate
,
1778 it('requires a token', async
function () {
1779 manager
.mysteryBox
.unpack
.rejects();
1780 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1782 it('requires token to have refresh field', async
function () {
1783 manager
.mysteryBox
.unpack
.resolves();
1784 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1786 it('requires token to exist in db', async
function () {
1787 manager
.db
.tokenGetByCodeId
.resolves();
1788 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1790 it('requires token be refreshable', async
function () {
1791 manager
.db
.tokenGetByCodeId
.resolves({
1792 refreshExpires: undefined,
1794 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1796 it('requires refresh of token not be expired', async
function () {
1797 manager
.db
.tokenGetByCodeId
.resolves({
1798 refreshExpires: 1000,
1800 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1802 it('requires token not to have been already refreshed', async
function () {
1803 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
1804 manager
.mysteryBox
.unpack
.resolves({
1805 rc: '03bb8ab0-1dc7-11ed-99f2-0025905f714a',
1806 ts: nowEpoch
- 864000,
1807 exp: nowEpoch
- 86400,
1809 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1811 it('requires client_id requesting refresh match', async
function () {
1812 ctx
.parsedBody
['client_id'] = 'https://wrong.example.com/';
1813 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1815 it('succeeds', async
function () {
1816 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1817 assert(res
.end
.called
);
1819 it('covers non-expiring', async
function () {
1820 manager
.db
.tokenGetByCodeId
.resolves({
1821 refreshExpires: new Date(Date
.now() + 86400000),
1823 clientId: 'https://client.example.com/',
1824 scopes: ['profile', 'create'],
1826 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1827 assert(res
.end
.called
);
1829 it('covers profile and email', async
function () {
1830 manager
.db
.tokenGetByCodeId
.resolves({
1831 refreshExpires: new Date(Date
.now() + 86400000),
1833 clientId: 'https://client.example.com/',
1834 scopes: ['profile', 'email', 'create'],
1836 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1837 assert(res
.end
.called
);
1839 it('succeeds with scope reduction', async
function () {
1840 ctx
.parsedBody
['scope'] = 'profile fancy';
1841 manager
.db
.tokenGetByCodeId
.resolves({
1842 refreshExpires: new Date(Date
.now() + 86400000),
1843 clientId: 'https://client.example.com/',
1844 scopes: ['profile', 'create'],
1846 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1847 assert(res
.end
.called
);
1849 it('covers refresh failed', async
function () {
1850 manager
.db
.refreshCode
.resolves();
1851 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1853 }); // _refreshToken
1855 describe('_mintTicket', function () {
1857 beforeEach(function () {
1860 subject: 'https://third-party.example.com/',
1861 resource: 'https://private.example.com/feed',
1863 identifier: 'account',
1864 profile: 'https://profile.example.com/',
1865 ticketLifespanSeconds: 86400,
1868 it('covers', async
function () {
1869 const expected
= 'xxx';
1870 sinon
.stub(manager
.mysteryBox
, 'pack').resolves(expected
);
1871 const result
= await manager
._mintTicket(dbCtx
, payload
);
1872 assert
.strictEqual(result
, expected
);
1876 describe('_ticketAuthToken', function () {
1877 let dbCtx
, ticketPayload
, nowEpoch
;
1878 beforeEach(function () {
1880 nowEpoch
= Math
.ceil(Date
.now() / 1000);
1882 c: '5ec17f78-5576-11ed-b444-0025905f714a',
1883 iss: nowEpoch
- 86400,
1884 exp: nowEpoch
+ 86400,
1885 sub: 'https://third-party.example.com/',
1886 res: 'https://private.example.com/feed',
1887 scope: ['read', 'flap'],
1889 profile: 'https://profile.example.com/',
1891 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves(ticketPayload
);
1892 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('ticket');
1894 it('covers invalid ticket', async
function () {
1895 manager
.mysteryBox
.unpack
.resolves();
1896 await assert
.rejects(() => manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1898 it('covers expired ticket', async
function () {
1899 manager
.mysteryBox
.unpack
.resolves({
1900 c: '5ec17f78-5576-11ed-b444-0025905f714a',
1901 iss: nowEpoch
- 172800,
1902 exp: nowEpoch
- 86400,
1903 sub: 'https://third-party.example.com/',
1904 res: 'https://private.example.com/feed',
1905 scope: ['read', 'flap'],
1907 profile: 'https://profile.example.com/',
1909 await assert
.rejects(() => manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1911 it('covers success', async
function () {
1912 manager
.db
.redeemCode
.resolves(true);
1913 await manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
);
1914 assert(res
.end
.called
);
1916 it('covers invalid redeem', async
function () {
1917 manager
.db
.redeemCode
.resolves(false);
1918 await assert
.rejects(() => manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1920 }); // _ticketAuthToken
1922 describe('postRevocation', function () {
1923 beforeEach(function () {
1924 sinon
.stub(manager
, '_revokeToken');
1926 it('covers success', async
function () {
1927 manager
._revokeToken
.resolves();
1928 await manager
.postRevocation(res
, ctx
);
1929 assert(manager
._revokeToken
.called
);
1931 it('covers failure', async
function () {
1932 manager
._revokeToken
.rejects(expectedException
);
1933 await assert
.rejects(manager
.postRevocation(res
, ctx
));
1935 }); // postRevocation
1937 describe('postUserInfo', function () {
1938 beforeEach(function () {
1939 ctx
.parsedBody
['token'] = 'XXX';
1940 sinon
.stub(manager
.mysteryBox
, 'unpack');
1942 it('requires a token', async
function () {
1943 delete ctx
.parsedBody
.token
;
1944 await manager
.postUserInfo(res
, ctx
);
1945 assert(res
.end
.called
);
1946 assert
.strictEqual(res
.statusCode
, 400);
1948 it('requires a valid token', async
function () {
1949 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1950 await manager
.postUserInfo(res
, ctx
);
1951 assert(res
.end
.called
);
1952 assert
.strictEqual(res
.statusCode
, 401);
1954 it('requires token to have profile scope', async
function () {
1955 manager
.mysteryBox
.unpack
.resolves({});
1956 manager
.db
.tokenGetByCodeId
.resolves({
1959 await manager
.postUserInfo(res
, ctx
);
1960 assert(res
.end
.called
);
1961 assert
.strictEqual(res
.statusCode
, 403);
1963 it('succeeds', async
function () {
1964 manager
.mysteryBox
.unpack
.resolves({});
1965 manager
.db
.tokenGetByCodeId
.resolves({
1966 scopes: ['profile', 'email'],
1968 url: 'https://example.com/',
1969 email: 'user@example.com',
1972 await manager
.postUserInfo(res
, ctx
);
1973 assert(res
.end
.called
);
1974 assert
.strictEqual(res
.statusCode
, 200);
1976 it('succeeds, and does not include email without scope', async
function () {
1977 manager
.mysteryBox
.unpack
.resolves({});
1978 manager
.db
.tokenGetByCodeId
.resolves({
1979 scopes: ['profile'],
1981 url: 'https://example.com/',
1982 email: 'user@example.com',
1985 await manager
.postUserInfo(res
, ctx
);
1986 assert(res
.end
.called
);
1987 assert
.strictEqual(res
.statusCode
, 200);
1988 const response
= JSON
.parse(res
.end
.args
[0][0]);
1989 assert(!('email' in response
));
1993 describe('getAdmin', function () {
1994 beforeEach(function () {
1995 manager
.db
.profilesScopesByIdentifier
.resolves({
1997 'https://profile.example.com/': {
2000 description: 'a scope',
2001 profiles: ['https://profile.example.com/'],
2008 description: 'a scope',
2009 profiles: ['https://profile.example.com/'],
2012 profiles: ['https://profile.example.com/'],
2014 manager
.db
.tokensGetByIdentifier
.resolves();
2016 it('covers', async
function () {
2017 await manager
.getAdmin(res
, ctx
);
2021 describe('postAdmin', function () {
2022 beforeEach(function () {
2023 manager
.db
.profilesScopesByIdentifier
.resolves({
2025 'https://profile.example.com/': {
2028 description: 'a scope',
2029 profiles: ['https://profile.example.com/'],
2036 description: 'a scope',
2037 profiles: ['https://profile.example.com/'],
2040 profiles: ['https://profile.example.com/'],
2042 manager
.db
.tokensGetByIdentifier
.resolves([]);
2043 manager
.db
.tokenRevokeByCodeId
.resolves();
2044 manager
.db
.profileIdentifierInsert
.resolves();
2045 manager
.db
.profileScopesSetAll
.resolves();
2046 manager
.communication
.fetchProfile
.resolves({
2048 authorizationEndpoint: manager
.selfAuthorizationEndpoint
,
2052 describe('save-scopes action', function () {
2053 beforeEach(function () {
2054 ctx
.parsedBody
['action'] = 'save-scopes';
2055 ctx
.parsedBody
['scopes-https://profile/example.com/[]'] = ['scope1', 'scope2'];
2057 it('covers saving scopes', async
function () {
2058 await manager
.postAdmin(res
, ctx
);
2059 assert(ctx
.notifications
.length
);
2060 assert(manager
.db
.profileScopesSetAll
.called
);
2062 it('covers saving scopes error', async
function () {
2063 manager
.db
.profileScopesSetAll
.rejects();
2064 await manager
.postAdmin(res
, ctx
);
2065 assert(ctx
.errors
.length
);
2067 }); // save-scopes action
2068 describe('new-profile action', function () {
2069 beforeEach(function () {
2070 ctx
.parsedBody
['action'] = 'new-profile';
2072 it('covers new profile', async
function () {
2073 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2074 await manager
.postAdmin(res
, ctx
);
2075 assert(ctx
.notifications
.length
);
2076 assert(manager
.db
.profileIdentifierInsert
.called
);
2077 assert(manager
.db
.profileScopesSetAll
.called
);
2079 it('covers invalid profile', async
function () {
2080 ctx
.parsedBody
['action'] = 'new-profile';
2081 ctx
.parsedBody
['profile'] = 'not a url';
2082 await manager
.postAdmin(res
, ctx
);
2083 assert(ctx
.errors
.length
);
2085 it('covers other validation failure', async
function () {
2086 sinon
.stub(manager
.communication
, 'validateProfile').rejects(expectedException
);
2087 ctx
.parsedBody
['action'] = 'new-profile';
2088 ctx
.parsedBody
['profile'] = 'not a url';
2089 await manager
.postAdmin(res
, ctx
);
2090 assert(ctx
.errors
.length
);
2092 it('covers mismatched profile', async
function () {
2093 ctx
.parsedBody
['action'] = 'new-profile';
2094 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2095 manager
.communication
.fetchProfile
.resolves({
2097 authorizationEndpoint: 'https://other.example.com/auth',
2100 await manager
.postAdmin(res
, ctx
);
2101 assert(ctx
.errors
.length
);
2103 it('covers new profile error', async
function () {
2104 ctx
.parsedBody
['action'] = 'new-profile';
2105 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2106 manager
.db
.profileIdentifierInsert
.rejects();
2107 await manager
.postAdmin(res
, ctx
);
2108 assert(ctx
.errors
.length
);
2110 }); // new-profile action
2111 describe('new-scope action', function () {
2112 beforeEach(function () {
2113 ctx
.parsedBody
['action'] = 'new-scope';
2115 it('covers new scope', async
function () {
2116 ctx
.parsedBody
['scope'] = 'newscope';
2117 await manager
.postAdmin(res
, ctx
);
2118 assert(ctx
.notifications
.length
);
2119 assert(manager
.db
.scopeUpsert
.called
);
2121 it('covers bad scope', async
function () {
2122 ctx
.parsedBody
['scope'] = 'bad scope';
2123 await manager
.postAdmin(res
, ctx
);
2124 assert(ctx
.errors
.length
);
2126 it('covers new scope error', async
function () {
2127 ctx
.parsedBody
['scope'] = 'newscope';
2128 manager
.db
.scopeUpsert
.rejects();
2129 await manager
.postAdmin(res
, ctx
);
2130 assert(ctx
.errors
.length
);
2132 it('covers empty scope', async
function () {
2133 delete ctx
.parsedBody
.scope
;
2134 await manager
.postAdmin(res
, ctx
);
2135 assert(!ctx
.errors
.length
);
2137 }); // new-scope action
2138 describe('delete-scope-* action', function () {
2139 beforeEach(function () {
2140 ctx
.parsedBody
['action'] = 'delete-scope-food%3Ayum';
2142 it('covers delete', async
function () {
2143 manager
.db
.scopeDelete
.resolves(true);
2144 await manager
.postAdmin(res
, ctx
);
2145 assert(ctx
.notifications
.length
);
2146 assert(manager
.db
.scopeDelete
.called
);
2148 it('covers no delete', async
function () {
2149 manager
.db
.scopeDelete
.resolves(false);
2150 await manager
.postAdmin(res
, ctx
);
2151 assert(ctx
.notifications
.length
);
2152 assert(manager
.db
.scopeDelete
.called
);
2154 it('covers delete error', async
function () {
2155 manager
.db
.scopeDelete
.rejects();
2156 await manager
.postAdmin(res
, ctx
);
2157 assert(ctx
.errors
.length
);
2158 assert(manager
.db
.scopeDelete
.called
);
2160 it('ignores empty scope', async
function () {
2161 ctx
.parsedBody
['action'] = 'delete-scope-';
2162 await manager
.postAdmin(res
, ctx
);
2163 assert(manager
.db
.scopeDelete
.notCalled
);
2164 assert(!ctx
.notifications
.length
);
2165 assert(!ctx
.errors
.length
);
2167 }); // delete-scope-* action
2168 describe('revoke-* action', function () {
2169 beforeEach(function () {
2170 ctx
.parsedBody
['action'] = 'revoke-b1591c00-9cb7-11ec-a05c-0025905f714a';
2172 it('covers revocation', async
function () {
2173 await manager
.postAdmin(res
, ctx
);
2174 assert(ctx
.notifications
.length
);
2175 assert(manager
.db
.tokenRevokeByCodeId
.called
);
2177 it('covers revocation error', async
function () {
2178 manager
.db
.tokenRevokeByCodeId
.rejects();
2179 await manager
.postAdmin(res
, ctx
);
2180 assert(ctx
.errors
.length
);
2182 it('covers no code', async
function () {
2183 ctx
.parsedBody
['action'] = 'revoke-';
2184 await manager
.postAdmin(res
, ctx
);
2185 assert(!ctx
.notifications
.length
);
2186 assert(!ctx
.errors
.length
);
2187 assert(manager
.db
.tokenRevokeByCodeId
.notCalled
);
2189 }); // revoke-* action
2190 it('covers empty action', async
function () {
2191 delete ctx
.parsedBody
.action
;
2192 await manager
.postAdmin(res
, ctx
);
2193 assert(!ctx
.errors
.length
);
2195 it('covers unknown action', async
function () {
2196 ctx
.parsedBody
['action'] = 'unsupported-action';
2197 await manager
.postAdmin(res
, ctx
);
2198 assert(ctx
.errors
.length
);
2202 describe('getAdminTicket', function () {
2203 it('covers', async
function () {
2204 manager
.db
.profilesScopesByIdentifier
.resolves({ scopeIndex: {} });
2205 await manager
.getAdminTicket(res
, ctx
);
2206 assert(res
.end
.called
);
2208 }); // getAdminTicket
2210 describe('postAdminTicket', function () {
2211 beforeEach(function () {
2212 ctx
.parsedBody
['action'] = 'proffer-ticket';
2213 ctx
.parsedBody
['scopes[]'] = ['read', 'role:private'];
2214 ctx
.parsedBody
['adhoc'] = 'adhoc_scope';
2215 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2216 ctx
.parsedBody
['resource'] = 'https://profile.example.com/feed';
2217 ctx
.parsedBody
['subject'] = 'https://subject.example.com/';
2218 manager
.db
.profilesScopesByIdentifier
.resolves({ scopeIndex: {} });
2219 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('ticket');
2220 manager
.communication
.fetchProfile
.resolves({
2222 ticketEndpoint: 'https://example.com/ticket',
2226 it('covers success', async
function () {
2227 await manager
.postAdminTicket(res
, ctx
);
2228 assert(res
.end
.called
);
2229 assert
.strictEqual(ctx
.errors
.length
, 0);
2230 assert
.strictEqual(ctx
.notifications
.length
, 1);
2232 it('requires params', async
function () {
2233 delete ctx
.parsedBody
['adhoc'];
2234 ctx
.parsedBody
['profile'] = 'bad url';
2235 ctx
.parsedBody
['resource'] = 'bad url';
2236 ctx
.parsedBody
['subject'] = 'bad url';
2237 ctx
.parsedBody
['scopes[]'] = ['fl"hrgl', 'email'];
2238 await manager
.postAdminTicket(res
, ctx
);
2239 assert(res
.end
.called
);
2240 assert
.strictEqual(ctx
.errors
.length
, 5);
2241 assert
.strictEqual(ctx
.notifications
.length
, 0);
2243 it('ignores unknown action', async
function () {
2244 ctx
.parsedBody
['action'] = 'prove-dough';
2245 await manager
.postAdminTicket(res
, ctx
);
2246 assert(res
.end
.called
);
2248 it('covers delivery failure', async
function () {
2249 manager
.communication
.deliverTicket
.rejects(expectedException
);
2250 await manager
.postAdminTicket(res
, ctx
);
2251 assert(res
.end
.called
);
2252 assert
.strictEqual(ctx
.errors
.length
, 1);
2253 assert
.strictEqual(ctx
.notifications
.length
, 0);
2255 it('covers no ticket endpoint', async
function () {
2256 manager
.communication
.fetchProfile
.resolves({
2260 await manager
.postAdminTicket(res
, ctx
);
2261 assert(res
.end
.called
);
2262 assert
.strictEqual(ctx
.errors
.length
, 1);
2263 assert
.strictEqual(ctx
.notifications
.length
, 0);
2265 it('covers bad ticket endpoint', async
function () {
2266 manager
.communication
.fetchProfile
.resolves({
2268 ticketEndpoint: 'not a url',
2271 await manager
.postAdminTicket(res
, ctx
);
2272 assert(res
.end
.called
);
2273 assert
.strictEqual(ctx
.errors
.length
, 1);
2274 assert
.strictEqual(ctx
.notifications
.length
, 0);
2276 }); // postAdminTicket
2278 describe('postTicket', function () {
2279 beforeEach(function () {
2281 ticket: 'ticket123',
2282 resource: 'https://blog.example.com/',
2283 subject: 'https://otheruser.example.com/',
2284 iss: 'https://ia.example.com/',
2287 it('accepts a ticket for a known profile', async
function () {
2288 manager
.db
.profileIsValid
.resolves(true);
2289 await manager
.postTicket(req
, res
, ctx
);
2290 assert(res
.end
.called
);
2291 assert
.strictEqual(res
.statusCode
, 202);
2293 it('rejects invalid resource', async
function () {
2294 ctx
.parsedBody
.resource
= 'invalid url';
2295 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), ResponseError
);
2297 it('rejects invalid subject', async
function () {
2298 manager
.db
.profileIsValid(false);
2299 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), ResponseError
);
2301 it('covers queue publish failure', async
function () {
2302 manager
.db
.profileIsValid
.resolves(true);
2303 manager
.queuePublisher
.publish
.rejects(expectedException
);
2304 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), expectedException
);
2306 it('covers no ticket queue', async
function () {
2307 delete options
.queues
.amqp
.url
;
2308 manager
= new Manager(logger
, stubDb
, options
);
2309 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), ResponseError
);
2311 it('covers no issuer', async
function () {
2312 delete ctx
.parsedBody
.iss
;
2313 manager
.db
.profileIsValid
.resolves(true);
2314 await manager
.postTicket(req
, res
, ctx
);
2315 assert(res
.end
.called
);
2316 assert
.strictEqual(res
.statusCode
, 202);
2318 it('covers bad issuer', async
function () {
2319 ctx
.parsedBody
.iss
= 'not a url';
2320 manager
.db
.profileIsValid
.resolves(true);
2321 await manager
.postTicket(req
, res
, ctx
);
2322 assert(res
.end
.called
);
2323 assert
.strictEqual(res
.statusCode
, 202);
2328 describe('queuedTicketProcessor', function () {
2329 let channel
, content
;
2330 const message
= () => ({
2331 content: Buffer
.from(JSON
.stringify(content
)),
2333 beforeEach(function () {
2338 ticket: 'XXXticketXXX',
2339 resource: 'https://blog.example.com/',
2340 subject: 'https://otheruser.exmaple.com/',
2341 iss: 'https://ia.example.com/',
2342 epochMs: Date
.now(),
2345 it('redeems a ticket', async
function () {
2346 await manager
.queuedTicketProcessor(channel
, message());
2347 assert(manager
.queuePublisher
.publish
.called
);
2348 assert(channel
.ack
.called
);
2350 it('redeems a ticket, missing issuer', async
function () {
2352 await manager
.queuedTicketProcessor(channel
, message());
2353 assert(manager
.queuePublisher
.publish
.called
);
2354 assert(channel
.ack
.called
);
2356 it('covers bad message', async
function () {
2357 await manager
.queuedTicketProcessor(channel
, { content: 'diddly' });
2358 assert(channel
.ack
.called
);
2360 it('covers bad issuer', async
function () {
2361 content
.iss
= 'not a url';
2362 await manager
.queuedTicketProcessor(channel
, message());
2363 assert(manager
.queuePublisher
.publish
.called
);
2365 it('covers bad resource', async
function () {
2366 content
.resource
= 'not a url';
2367 await manager
.queuedTicketProcessor(channel
, message());
2368 assert(manager
.communication
.redeemTicket
.notCalled
);
2369 assert(manager
.queuePublisher
.publish
.notCalled
);
2370 assert(channel
.ack
.called
);
2372 it('covers failed redemption', async
function () {
2373 const expectedException
= new Error('oh no');
2374 manager
.communication
.redeemTicket
.rejects(expectedException
);
2375 assert
.rejects(() => manager
.queuedTicketProcessor(channel
, message()), expectedException
);
2377 it('covers failed publish', async
function () {
2378 const expectedException
= new Error('oh no');
2379 manager
.queuePublisher
.publish
.rejects(expectedException
);
2380 assert
.rejects(() => manager
.queuedTicketProcessor(channel
, message()), expectedException
);
2382 }); // queuedTicketProcessor
2384 describe('getAdminMaintenance', function () {
2385 it('covers information', async
function () {
2386 await manager
.getAdminMaintenance(res
, ctx
);
2387 assert(res
.end
.called
);
2389 it('covers tasks', async
function () {
2391 [Enum
.Chore
.CleanTokens
]: '',
2393 await manager
.getAdminMaintenance(res
, ctx
);
2394 assert(res
.end
.called
);
2396 }); // getAdminMaintenance