1 /* eslint-disable sonarjs/no-duplicate-string */
5 const assert
= require('assert');
6 const sinon
= require('sinon');
8 const Manager
= require('../../src/manager');
9 const Config
= require('../../config');
10 const Enum
= require('../../src/enum');
11 const { ResponseError
} = require('../../src/errors');
12 const { UnexpectedResult
} = require('../../src/db/errors');
13 const dns
= require('dns');
15 const StubDatabase
= require('../stub-db');
16 const StubLogger
= require('../stub-logger');
18 const expectedException
= new Error('oh no');
19 const noExpectedException
= 'did not get expected exception';
21 describe('Manager', function () {
22 let manager
, options
, stubDb
, logger
;
25 beforeEach(function () {
26 logger
= new StubLogger();
28 stubDb
= new StubDatabase();
30 options
= new Config('test');
32 getHeader : sinon
.stub(),
36 setHeader: sinon
.stub(),
47 manager
= new Manager(logger
, stubDb
, options
);
48 sinon
.stub(manager
.communication
, 'fetchProfile');
49 sinon
.stub(manager
.communication
, 'fetchClientIdentifier');
50 sinon
.stub(manager
.communication
, 'deliverTicket');
51 sinon
.stub(manager
.communication
, 'redeemTicket');
52 sinon
.stub(dns
.promises
, 'lookup').resolves([{ family: 4, address: '10.11.12.13' }]);
53 sinon
.stub(manager
.queuePublisher
, 'connect');
54 sinon
.stub(manager
.queuePublisher
, 'establishAMQPPlumbing');
55 sinon
.stub(manager
.queuePublisher
, 'publish');
56 sinon
.stub(manager
.queueConsumer
, 'connect');
57 sinon
.stub(manager
.queueConsumer
, 'establishAMQPPlumbing');
58 sinon
.stub(manager
.queueConsumer
, 'consume');
61 afterEach(function () {
65 describe('constructor', function () {
66 it('instantiates', function () {
69 it('covers no queuing', function () {
70 options
.queues
.amqp
.url
= undefined;
71 manager
= new Manager(logger
, stubDb
, options
);
76 describe('initialize', function () {
78 beforeEach(function () {
79 spy
= sinon
.spy(manager
, '_connectQueues');
81 it('covers', async
function () {
82 await manager
.initialize();
85 it('covers no queue', async
function () {
86 delete options
.queues
.amqp
.url
;
87 manager
= new Manager(logger
, stubDb
, options
);
88 await manager
.initialize();
89 assert(spy
.notCalled
);
93 describe('getRoot', function () {
94 it('normal response', async
function () {
95 await manager
.getRoot(res
, ctx
);
96 assert(res
.end
.called
);
100 describe('getMeta', function () {
101 it('normal response', async
function () {
102 await manager
.getMeta(res
, ctx
);
103 assert(res
.end
.called
);
104 JSON
.parse(res
.end
.args
[0][0]);
106 it('covers no ticket queue', async
function () {
107 delete options
.queues
.amqp
.url
;
108 manager
= new Manager(logger
, stubDb
, options
);
109 await manager
.getMeta(res
, ctx
);
110 assert(res
.end
.called
);
114 describe('getHealthcheck', function () {
115 it('normal response', async
function () {
116 await manager
.getHealthcheck(res
, ctx
);
117 assert(res
.end
.called
);
119 }); // getHealthcheck
121 describe('getAuthorization', function () {
122 it('covers missing redirect fields', async
function () {
123 await manager
.getAuthorization(res
, ctx
);
124 assert
.strictEqual(res
.statusCode
, 400);
126 it('requires a configured profile', async
function () {
127 manager
.db
.profilesScopesByIdentifier
.resolves({
142 manager
.communication
.fetchClientIdentifier
.resolves({
145 ctx
.authenticationId
= 'username';
146 Object
.assign(ctx
.queryParams
, {
147 'client_id': 'https://client.example.com/',
148 'redirect_uri': 'https://client.example.com/action',
149 'response_type': 'code',
151 'code_challenge_method': 'S256',
152 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
153 'scope': 'profile email',
155 await manager
.getAuthorization(res
, ctx
);
156 assert
.strictEqual(res
.statusCode
, 302);
157 assert(ctx
.session
.error
);
158 assert(res
.setHeader
.called
);
160 it('covers valid', async
function () {
161 manager
.db
.profilesScopesByIdentifier
.resolves({
163 'https://profile.example.com/': {
166 profiles: ['https://profile.example.com'],
181 profiles: ['https://profile.example.com/'],
184 profiles: ['https://profile.example.com/'],
186 manager
.communication
.fetchClientIdentifier
.resolves({
189 ctx
.authenticationId
= 'username';
190 Object
.assign(ctx
.queryParams
, {
191 'client_id': 'https://client.example.com/',
192 'redirect_uri': 'https://client.example.com/action',
193 'response_type': 'code',
195 'code_challenge_method': 'S256',
196 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
197 'scope': 'profile email',
198 'me': 'https://profile.example.com/',
200 await manager
.getAuthorization(res
, ctx
);
201 assert
.strictEqual(res
.statusCode
, 200);
202 assert
.strictEqual(ctx
.session
.error
, undefined);
203 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 0);
204 assert
.strictEqual(ctx
.notifications
.length
, 0);
206 it('succeeds with mismatched profile hint', async
function () {
207 manager
.db
.profilesScopesByIdentifier
.resolves({
209 'https://profile.example.com/': {
212 profiles: ['https://profile.example.com'],
227 profiles: ['https://profile.example.com/'],
230 profiles: ['https://profile.example.com/'],
232 manager
.communication
.fetchClientIdentifier
.resolves({
235 ctx
.authenticationId
= 'username';
236 Object
.assign(ctx
.queryParams
, {
237 'client_id': 'https://client.example.com/',
238 'redirect_uri': 'https://client.example.com/action',
239 'response_type': 'code',
241 'code_challenge_method': 'S256',
242 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
243 'scope': 'profile email',
244 'me': 'https://somethingelse.example.com/',
246 await manager
.getAuthorization(res
, ctx
);
247 assert(!('me' in ctx
.session
));
248 assert
.strictEqual(res
.statusCode
, 200);
249 assert
.strictEqual(ctx
.session
.error
, undefined);
250 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 0);
252 it('covers invalid redirect', async
function () {
253 manager
.db
.profilesScopesByIdentifier
.resolves({
255 'https://profile.example.com/': {
258 profiles: ['https://profile.example.com'],
273 profiles: ['https://profile.example.com/'],
276 profiles: ['https://profile.example.com/'],
278 manager
.communication
.fetchClientIdentifier
.resolves({
281 ctx
.authenticationId
= 'username';
282 Object
.assign(ctx
.queryParams
, {
283 'client_id': 'https://client.example.com/',
284 'redirect_uri': 'https://client.example.com/action',
285 'response_type': 'blargl',
287 'code_challenge_method': 'S256',
288 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
290 await manager
.getAuthorization(res
, ctx
);
291 assert
.strictEqual(res
.statusCode
, 302);
292 assert
.strictEqual(ctx
.session
.error
, 'invalid_request');
293 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 2);
295 it('covers legacy non-PKCE missing fields', async
function () {
296 manager
.db
.profilesScopesByIdentifier
.resolves({
298 'https://profile.example.com/': {
301 profiles: ['https://profile.example.com'],
316 profiles: ['https://profile.example.com/'],
319 profiles: ['https://profile.example.com/'],
321 manager
.communication
.fetchClientIdentifier
.resolves({
324 ctx
.authenticationId
= 'username';
325 Object
.assign(ctx
.queryParams
, {
326 'client_id': 'https://client.example.com/',
327 'redirect_uri': 'https://client.example.com/action',
328 'response_type': 'code',
330 'scope': 'profile email',
331 'me': 'https://profile.example.com/',
333 manager
.options
.manager
.allowLegacyNonPKCE
= true;
335 await manager
.getAuthorization(res
, ctx
);
336 assert
.strictEqual(res
.statusCode
, 200);
337 assert
.strictEqual(ctx
.session
.error
, undefined);
338 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 0);
340 it('rejects legacy non-PKCE not missing all fields', async
function () {
341 manager
.db
.profilesScopesByIdentifier
.resolves({
343 'https://profile.example.com/': {
346 profiles: ['https://profile.example.com'],
361 profiles: ['https://profile.example.com/'],
364 profiles: ['https://profile.example.com/'],
366 manager
.communication
.fetchClientIdentifier
.resolves({
369 ctx
.authenticationId
= 'username';
370 Object
.assign(ctx
.queryParams
, {
371 'client_id': 'https://client.example.com/',
372 'redirect_uri': 'https://client.example.com/action',
373 'response_type': 'code',
374 'code_challenge_method': 'S256',
376 'scope': 'profile email',
377 'me': 'https://profile.example.com/',
379 manager
.options
.manager
.allowLegacyNonPKCE
= true;
381 await manager
.getAuthorization(res
, ctx
);
382 assert
.strictEqual(res
.statusCode
, 302);
383 assert
.strictEqual(ctx
.session
.error
, 'invalid_request');
384 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 1);
386 it('rejects legacy non-PKCE not missing all fields', async
function () {
387 manager
.db
.profilesScopesByIdentifier
.resolves({
389 'https://profile.example.com/': {
392 profiles: ['https://profile.example.com'],
407 profiles: ['https://profile.example.com/'],
410 profiles: ['https://profile.example.com/'],
412 manager
.communication
.fetchClientIdentifier
.resolves({
415 ctx
.authenticationId
= 'username';
416 Object
.assign(ctx
.queryParams
, {
417 'client_id': 'https://client.example.com/',
418 'redirect_uri': 'https://client.example.com/action',
419 'response_type': 'code',
420 'code_challenge': 'xxx',
422 'scope': 'profile email',
423 'me': 'https://profile.example.com/',
425 manager
.options
.manager
.allowLegacyNonPKCE
= true;
427 await manager
.getAuthorization(res
, ctx
);
428 assert
.strictEqual(res
.statusCode
, 302);
429 assert
.strictEqual(ctx
.session
.error
, 'invalid_request');
430 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 1);
432 }); // getAuthorization
434 describe('_setError', function () {
435 it('covers', function () {
436 const err
= 'invalid_request';
437 const errDesc
= 'something went wrong';
438 Manager
._setError(ctx
, err
, errDesc
);
440 it('covers bad error', function () {
441 const err
= 'floopy';
442 const errDesc
= 'something went wrong';
444 Manager
._setError(ctx
, err
, errDesc
);
445 assert
.fail(noExpectedException
);
447 assert(e
instanceof RangeError
);
450 it('covers invalid error description', function () {
451 const err
= 'invalid_scope';
452 const errDesc
= 'something "went wrong"!';
454 Manager
._setError(ctx
, err
, errDesc
);
455 assert
.fail(noExpectedException
);
457 assert(e
instanceof RangeError
);
462 describe('_clientIdRequired', function () {
463 let clientIdentifier
;
464 beforeEach(function () {
468 manager
.communication
.fetchClientIdentifier
.resolves(clientIdentifier
);
470 it('covers valid', async
function () {
471 ctx
.queryParams
['client_id'] = 'https://client.example.com/';
473 await manager
._clientIdRequired(ctx
);
475 assert
.deepStrictEqual(ctx
.session
.clientIdentifier
, clientIdentifier
);
476 assert
.strictEqual(ctx
.session
.error
, undefined);
477 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
479 it('requires client_id', async
function () {
480 ctx
.queryParams
['client_id'] = undefined;
482 await manager
._clientIdRequired(ctx
);
484 assert(ctx
.session
.error
);
485 assert(ctx
.session
.errorDescriptions
.length
);
487 it('requires valid client_id', async
function () {
488 ctx
.queryParams
['client_id'] = 'not a url';
490 await manager
._clientIdRequired(ctx
);
492 assert(ctx
.session
.error
);
493 assert(ctx
.session
.errorDescriptions
.length
);
495 it('rejects strange schema', async
function () {
496 ctx
.queryParams
['client_id'] = 'file:///etc/shadow';
498 await manager
._clientIdRequired(ctx
);
500 assert(ctx
.session
.error
);
501 assert(ctx
.session
.errorDescriptions
.length
);
503 it('rejects un-allowed parts', async
function () {
504 ctx
.queryParams
['client_id'] = 'https://user:pass@client.example.com/#here';
506 await manager
._clientIdRequired(ctx
);
508 assert(ctx
.session
.error
);
509 assert(ctx
.session
.errorDescriptions
.length
);
511 it('rejects relative paths', async
function () {
512 ctx
.queryParams
['client_id'] = 'https://client.example.com/x/../y/';
514 await manager
._clientIdRequired(ctx
);
516 assert(ctx
.session
.error
);
517 assert(ctx
.session
.errorDescriptions
.length
);
519 it('rejects ipv6 hostname', async
function () {
520 ctx
.queryParams
['client_id'] = 'https://[fd12:3456:789a:1::1]/';
522 await manager
._clientIdRequired(ctx
);
524 assert(ctx
.session
.error
);
525 assert(ctx
.session
.errorDescriptions
.length
);
527 it('allows ipv6 loopback hostname', async
function () {
528 ctx
.queryParams
['client_id'] = 'https://[::1]/';
530 await manager
._clientIdRequired(ctx
);
532 assert
.deepStrictEqual(ctx
.session
.clientIdentifier
, clientIdentifier
);
533 assert
.strictEqual(ctx
.session
.error
, undefined);
534 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
536 it('rejects ipv4 hostname', async
function () {
537 ctx
.queryParams
['client_id'] = 'https://10.9.8.7/';
539 await manager
._clientIdRequired(ctx
);
541 assert(ctx
.session
.error
);
542 assert(ctx
.session
.errorDescriptions
.length
);
544 it('allows ipv4 loopback hostname', async
function () {
545 ctx
.queryParams
['client_id'] = 'https:/127.0.10.100/';
547 await manager
._clientIdRequired(ctx
);
549 assert
.deepStrictEqual(ctx
.session
.clientIdentifier
, clientIdentifier
);
550 assert
.strictEqual(ctx
.session
.error
, undefined);
551 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
553 it('requires response', async
function () {
554 manager
.communication
.fetchClientIdentifier
.restore();
555 sinon
.stub(manager
.communication
, 'fetchClientIdentifier').resolves();
556 ctx
.queryParams
['client_id'] = 'https://client.example.com/';
558 await manager
._clientIdRequired(ctx
);
560 assert(ctx
.session
.error
);
561 assert(ctx
.session
.errorDescriptions
.length
);
563 }); // _clientIdRequired
565 describe('_redirectURIRequired', function () {
566 beforeEach(function () {
567 ctx
.session
.clientId
= new URL('https://client.example.com/');
568 ctx
.session
.clientIdentifier
= {
570 'redirect_uri': ['https://alternate.example.com/', 'https://other.example.com/'],
574 it('covers valid', function () {
575 ctx
.queryParams
['redirect_uri'] = 'https://client.example.com/return';
577 Manager
._redirectURIRequired(ctx
);
579 assert
.strictEqual(ctx
.session
.error
, undefined);
580 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
582 it('requires redirect_uri', function () {
583 ctx
.queryParams
['redirect_uri'] = undefined;
585 Manager
._redirectURIRequired(ctx
);
587 assert(ctx
.session
.error
);
588 assert(ctx
.session
.errorDescriptions
.length
);
590 it('requires valid redirect_uri', function () {
591 ctx
.queryParams
['redirect_uri'] = 'not a url';
593 Manager
._redirectURIRequired(ctx
);
595 assert(ctx
.session
.error
);
596 assert(ctx
.session
.errorDescriptions
.length
);
598 it('rejects no matching alternate redirect_uri from client_id', function () {
599 ctx
.queryParams
['redirect_uri'] = 'https://unlisted.example.com/';
601 Manager
._redirectURIRequired(ctx
);
603 assert(ctx
.session
.error
);
604 assert(ctx
.session
.errorDescriptions
.length
);
606 it('allows alternate redirect_uri from client_id', function () {
607 ctx
.queryParams
['redirect_uri'] = 'https://alternate.example.com/';
609 Manager
._redirectURIRequired(ctx
);
611 assert
.strictEqual(ctx
.session
.error
, undefined);
612 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
614 }); // _redirectURIRequired
616 describe('_responseTypeRequired', function () {
617 it('covers valid', function () {
618 ctx
.queryParams
['response_type'] = 'code';
620 Manager
._responseTypeRequired(ctx
);
622 assert
.strictEqual(ctx
.session
.error
, undefined);
623 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
625 it('requires response_type', function () {
626 ctx
.queryParams
['response_type'] = undefined;
628 Manager
._responseTypeRequired(ctx
);
630 assert(ctx
.session
.error
);
631 assert(ctx
.session
.errorDescriptions
.length
);
633 it('rejects invalid', function () {
634 ctx
.queryParams
['response_type'] = 'flarp';
636 Manager
._responseTypeRequired(ctx
);
638 assert(ctx
.session
.error
);
639 assert(ctx
.session
.errorDescriptions
.length
);
641 }); // _responseTypeRequired
643 describe('_stateRequired', function () {
644 it('covers valid', function () {
645 ctx
.queryParams
['state'] = 'StateStateState';
647 Manager
._stateRequired(ctx
);
649 assert
.strictEqual(ctx
.session
.error
, undefined);
650 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
652 it('requires state', function () {
653 ctx
.queryParams
['state'] = undefined;
655 Manager
._stateRequired(ctx
);
657 assert(ctx
.session
.error
);
658 assert(ctx
.session
.errorDescriptions
.length
);
660 }); // _stateRequired
662 describe('_codeChallengeMethodRequired', function () {
663 it('covers valid', function () {
664 ctx
.queryParams
['code_challenge_method'] = 'S256';
666 manager
._codeChallengeMethodRequired(ctx
);
668 assert
.strictEqual(ctx
.session
.error
, undefined);
669 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
671 it('requires code_challenge_method', function () {
672 ctx
.queryParams
['code_challenge_method'] = undefined;
674 manager
._codeChallengeMethodRequired(ctx
);
676 assert(ctx
.session
.error
);
677 assert(ctx
.session
.errorDescriptions
.length
);
679 it('rejects invalid', function () {
680 ctx
.queryParams
['code_challenge_method'] = 'MD5';
682 manager
._codeChallengeMethodRequired(ctx
);
684 assert(ctx
.session
.error
);
685 assert(ctx
.session
.errorDescriptions
.length
);
687 it('covers legacy non-PKCE', function () {
688 ctx
.queryParams
['code_challenge_method'] = undefined;
689 manager
.options
.manager
.allowLegacyNonPKCE
= true;
691 manager
._codeChallengeMethodRequired(ctx
);
693 assert
.strictEqual(ctx
.session
.error
, undefined);
694 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
696 }); // _codeChallengeMethodRequired
698 describe('_codeChallengeRequired', function () {
699 it('covers valid', function () {
700 ctx
.queryParams
['code_challenge'] = 'NBKNqs1TfjQFqpewPNOstmQ5MJnLoeTTbjqtQ9JbZOo';
702 manager
._codeChallengeRequired(ctx
);
704 assert
.strictEqual(ctx
.session
.error
, undefined);
705 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
707 it('requires code_challenge', function () {
708 ctx
.queryParams
['code_challenge'] = undefined;
710 manager
._codeChallengeRequired(ctx
);
712 assert(ctx
.session
.error
);
713 assert(ctx
.session
.errorDescriptions
.length
);
715 it('rejects invalid', function () {
716 ctx
.queryParams
['code_challenge'] = 'not base64/url encoded';
718 manager
._codeChallengeRequired(ctx
);
720 assert(ctx
.session
.error
);
721 assert(ctx
.session
.errorDescriptions
.length
);
723 it('covers legacy non-PKCE', function () {
724 ctx
.queryParams
['code_challenge'] = undefined;
725 manager
.options
.manager
.allowLegacyNonPKCE
= true;
727 manager
._codeChallengeRequired(ctx
);
729 assert
.strictEqual(ctx
.session
.error
, undefined);
730 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
733 }); // _codeChallengeRequired
735 describe('_redirectURIRequired', function () {
736 beforeEach(function () {
737 sinon
.stub(Manager
, '_setError');
738 ctx
.queryParams
['redirect_uri'] = 'https://example.com/redirect';
739 ctx
.session
.clientId
= new URL('https://example.com/');
741 it('requires redirect_uri', function () {
742 delete ctx
.queryParams
['redirect_uri'];
743 Manager
._redirectURIRequired(ctx
);
744 assert(Manager
._setError
.called
);
746 it('requires valid redirect_uri', function () {
747 ctx
.queryParams
['redirect_uri'] = 'not a uri';
748 Manager
._redirectURIRequired(ctx
);
749 assert(Manager
._setError
.called
);
751 it('sets redirectUri if no clientId', function () {
752 delete ctx
.session
.clientId
;
753 Manager
._redirectURIRequired(ctx
);
754 assert(Manager
._setError
.notCalled
);
755 assert(ctx
.session
.redirectUri
instanceof URL
);
757 it('sets redirectUri if clientId matches', function () {
758 Manager
._redirectURIRequired(ctx
);
759 assert(Manager
._setError
.notCalled
);
760 assert(ctx
.session
.redirectUri
instanceof URL
);
762 it('rejects mis-matched', function () {
763 ctx
.queryParams
['redirect_uri'] = 'https://example.com:8080/redirect';
764 Manager
._redirectURIRequired(ctx
);
765 assert(Manager
._setError
.called
);
766 assert
.strictEqual(ctx
.session
.redirectUri
, undefined);
768 it('allows client-specified alternate redirect uri', function () {
769 ctx
.session
.clientIdentifier
= {
771 'redirect_uri': ['https://alternate.example.com/redirect'],
774 ctx
.queryParams
['redirect_uri'] = 'https://alternate.example.com/redirect';
775 Manager
._redirectURIRequired(ctx
);
776 assert(Manager
._setError
.notCalled
);
777 assert(ctx
.session
.redirectUri
instanceof URL
);
779 }); // _redirectURIRequired
781 describe('_scopeOptional', function () {
782 it('covers valid', function () {
783 ctx
.queryParams
['scope'] = 'profile email';
784 manager
._scopeOptional(ctx
);
785 assert
.strictEqual(ctx
.session
.error
, undefined);
786 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
787 assert
.strictEqual(ctx
.session
.scope
.length
, 2);
789 it('allows empty', function () {
790 ctx
.queryParams
['scope'] = undefined;
791 manager
._scopeOptional(ctx
);
792 assert
.strictEqual(ctx
.session
.error
, undefined);
793 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
794 assert
.strictEqual(ctx
.session
.scope
.length
, 0);
796 it('rejects invalid scope combination', function () {
797 ctx
.queryParams
['scope'] = 'email';
798 manager
._scopeOptional(ctx
);
799 assert(ctx
.session
.error
);
800 assert(ctx
.session
.errorDescriptions
.length
);
802 it('ignores invalid scope', function () {
803 ctx
.queryParams
['scope'] = 'profile email "funny_business"';
804 manager
._scopeOptional(ctx
);
805 assert
.strictEqual(ctx
.session
.error
, undefined);
806 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
807 assert
.strictEqual(ctx
.session
.scope
.length
, 2);
809 }); // _scopeOptional
811 describe('_meOptional', function () {
812 this.beforeEach(function () {
813 ctx
.queryParams
['me'] = 'https://profile.example.com/';
815 it('covers valid', async
function () {
816 await manager
._meOptional(ctx
);
818 assert
.strictEqual(ctx
.session
.me
.href
, ctx
.queryParams
['me']);
820 it('ignore invalid', async
function () {
821 ctx
.queryParams
['me'] = 'not a url';
823 await manager
._meOptional(ctx
);
825 assert
.strictEqual(ctx
.session
.me
, undefined);
827 it('allows empty', async
function () {
828 ctx
.queryParams
['me'] = undefined;
830 await manager
._meOptional(ctx
);
832 assert
.strictEqual(ctx
.session
.me
, undefined);
836 describe('_profileValidForIdentifier', function () {
837 beforeEach(function () {
839 profiles: ['https://profile.example.com/', 'https://example.com/profile'],
840 me: new URL('https://example.com/profile'),
843 it('covers valid', async
function () {
845 const result
= await manager
._profileValidForIdentifier(ctx
);
847 assert
.strictEqual(result
, true);
849 it('covers missing me', async
function () {
850 delete ctx
.session
.me
;
852 const result
= await manager
._profileValidForIdentifier(ctx
);
854 assert
.strictEqual(result
, false);
856 }); // _profileValidForIdentifier
858 describe('_parseLifespan', function () {
859 let field
, customField
;
860 beforeEach(function () {
862 customField
= 'lifespan-seconds';
863 ctx
.parsedBody
['lifespan'] = undefined;
864 ctx
.parsedBody
['lifespan-seconds'] = undefined;
866 it('returns nothing without fields', function () {
867 const result
= manager
._parseLifespan(ctx
, field
, customField
);
868 assert
.strictEqual(result
, undefined);
870 it('returns nothing for unrecognized field', function () {
871 ctx
.parsedBody
['lifespan'] = 'a while';
872 const result
= manager
._parseLifespan(ctx
, field
, customField
);
873 assert
.strictEqual(result
, undefined);
875 it('returns recognized preset value', function () {
876 ctx
.parsedBody
['lifespan'] = '1d';
877 const result
= manager
._parseLifespan(ctx
, field
, customField
);
878 assert
.strictEqual(result
, 86400);
880 it('returns valid custom value', function () {
881 ctx
.parsedBody
['lifespan'] = 'custom';
882 ctx
.parsedBody
['lifespan-seconds'] = '123';
883 const result
= manager
._parseLifespan(ctx
, field
, customField
);
884 assert
.strictEqual(result
, 123);
886 it('returns nothing for invalid custom value', function () {
887 ctx
.parsedBody
['lifespan'] = 'custom';
888 ctx
.parsedBody
['lifespan-seconds'] = 'Not a number';
889 const result
= manager
._parseLifespan(ctx
, field
, customField
);
890 assert
.strictEqual(result
, undefined);
892 it('returns nothing for invalid custom value', function () {
893 ctx
.parsedBody
['lifespan'] = 'custom';
894 ctx
.parsedBody
['lifespan-seconds'] = '-50';
895 const result
= manager
._parseLifespan(ctx
, field
, customField
);
896 assert
.strictEqual(result
, undefined);
898 }); // _parseLifespan
900 describe('_parseConsentScopes', function () {
901 it('covers no scopes', function () {
902 const result
= manager
._parseConsentScopes(ctx
);
903 assert
.deepStrictEqual(result
, []);
905 it('filters invalid scopes', function () {
906 ctx
.parsedBody
['accepted_scopes[]'] = ['read', 'email'];
907 ctx
.parsedBody
['ad_hoc_scopes'] = 'bad"scope create ';
908 const result
= manager
._parseConsentScopes(ctx
);
909 assert
.deepStrictEqual(result
, ['read', 'create']);
911 }); // _parseConsentScopes
913 describe('_parseConsentMe', function () {
914 beforeEach(function () {
915 ctx
.session
.profiles
= ['https://me.example.com/'];
917 it('covers valid', function () {
918 const expected
= 'https://me.example.com/';
919 ctx
.parsedBody
['me'] = expected
;
920 const result
= manager
._parseConsentMe(ctx
);
922 assert
.strictEqual(result
.href
, expected
);
924 it('rejects unsupported', function () {
925 ctx
.parsedBody
['me'] = 'https://notme.example.com/';
926 const result
= manager
._parseConsentMe(ctx
);
928 assert(ctx
.session
.error
);
930 it('rejects invalid', function () {
931 ctx
.parsedBody
['me'] = 'bagel';
932 const result
= manager
._parseConsentMe(ctx
);
934 assert(ctx
.session
.error
);
936 }); // _parseConsentMe
938 describe('_fetchConsentProfileData', function () {
940 beforeEach(function () {
942 url: 'https://profile.example.com/',
944 manager
.communication
.fetchProfile
.resolves(profileResponse
);
946 it('covers success', async
function () {
947 const expected
= profileResponse
;
948 const result
= await manager
._fetchConsentProfileData(ctx
);
949 assert
.deepStrictEqual(result
, expected
);
950 assert(!ctx
.session
.error
);
952 it('covers empty response', async
function () {
953 manager
.communication
.fetchProfile
.resolves();
954 const result
= await manager
._fetchConsentProfileData(ctx
);
955 assert
.deepStrictEqual(result
, undefined);
956 assert(ctx
.session
.error
);
958 it('covers failure', async
function () {
959 manager
.communication
.fetchProfile
.rejects();
960 const result
= await manager
._fetchConsentProfileData(ctx
);
961 assert
.deepStrictEqual(result
, undefined);
962 assert(ctx
.session
.error
);
964 }); // _fetchConsentProfileData
966 describe('postConsent', function () {
968 beforeEach(function () {
969 sinon
.stub(manager
.mysteryBox
, 'unpack');
970 sinon
.stub(manager
.mysteryBox
, 'pack');
971 manager
.communication
.fetchProfile
.resolves({
972 url: 'https://profile.example.com/',
975 clientId: 'https://example.com/',
976 redirectUri: 'https://example.com/_redirect',
977 profiles: ['https://profile.example.com/'],
979 manager
.mysteryBox
.unpack
.resolves(oldSession
);
980 ctx
.parsedBody
['me'] = 'https://profile.example.com/';
981 ctx
.parsedBody
['accept'] = 'true';
983 it('covers valid', async
function () {
984 await manager
.postConsent(res
, ctx
);
985 assert(!ctx
.session
.error
, ctx
.session
.error
);
986 assert
.strictEqual(res
.statusCode
, 302);
988 it('covers valid with expiration and refresh', async
function () {
989 ctx
.parsedBody
['expires'] = '1d';
990 ctx
.parsedBody
['refresh'] = '1w';
991 await manager
.postConsent(res
, ctx
);
992 assert(!ctx
.session
.error
, ctx
.session
.error
);
993 assert
.strictEqual(res
.statusCode
, 302);
995 it('covers denial', async
function () {
996 ctx
.parsedBody
['accept'] = 'false';
997 await manager
.postConsent(res
, ctx
);
998 assert(ctx
.session
.error
);
999 assert
.strictEqual(ctx
.session
.error
, 'access_denied');
1000 assert
.strictEqual(res
.statusCode
, 302);
1002 it('covers profile fetch failure', async
function () {
1003 manager
.communication
.fetchProfile
.resolves();
1004 await manager
.postConsent(res
, ctx
);
1005 assert
.strictEqual(res
.statusCode
, 302);
1006 assert(ctx
.session
.error
);
1008 it('covers bad code', async
function () {
1009 manager
.mysteryBox
.unpack
.rejects();
1010 await manager
.postConsent(res
, ctx
);
1011 assert
.strictEqual(res
.statusCode
, 400);
1012 assert(ctx
.session
.error
);
1014 it('removes email scope without profile', async
function () {
1015 ctx
.parsedBody
['accepted_scopes[]'] = ['email', 'create'];
1016 await manager
.postConsent(res
, ctx
);
1017 assert(!ctx
.session
.acceptedScopes
.includes('email'));
1019 it('merges valid ad-hoc scopes', async
function () {
1020 ctx
.parsedBody
['accepted_scopes[]'] = ['email', 'create'];
1021 ctx
.parsedBody
['ad_hoc_scopes'] = ' my:scope "badScope';
1022 await manager
.postConsent(res
, ctx
);
1023 assert(ctx
.session
.acceptedScopes
.includes('my:scope'));
1025 it('covers invalid selected me profile', async
function () {
1026 ctx
.parsedBody
['me'] = 'https://different.example.com/';
1027 await manager
.postConsent(res
, ctx
);
1028 assert(ctx
.session
.error
);
1030 it('covers invalid me url', async
function () {
1031 ctx
.parsedBody
['me'] = 'bagel';
1032 await manager
.postConsent(res
, ctx
);
1033 assert(ctx
.session
.error
);
1035 it('covers profile fetch error', async
function () {
1036 manager
.communication
.fetchProfile
.rejects(expectedException
);
1037 await manager
.postConsent(res
, ctx
);
1038 assert
.strictEqual(res
.statusCode
, 302);
1039 assert(ctx
.session
.error
);
1043 describe('postAuthorization', function () {
1044 let code
, parsedBody
;
1045 beforeEach(function () {
1046 sinon
.stub(manager
.mysteryBox
, 'unpack');
1048 codeId: 'cffe1558-35f0-11ec-98bc-0025905f714a',
1049 codeChallengeMethod: 'S256',
1050 codeChallenge: 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ',
1051 clientId: 'https://app.example.com/',
1052 redirectUri: 'https://app.example.com/_redirect',
1053 acceptedScopes: ['profile'],
1055 me: 'https://client.example.com/',
1056 identifier: 'username',
1058 name: 'Firsty McLastname',
1059 email: 'f.mclastname@example.com',
1063 code: 'codeCodeCode',
1064 'client_id': 'https://app.example.com/',
1065 'redirect_uri': 'https://app.example.com/_redirect',
1066 'grant_type': 'authorization_code',
1067 'code_verifier': 'verifier',
1070 it('covers valid', async
function () {
1071 manager
.db
.redeemCode
.resolves(true);
1072 manager
.mysteryBox
.unpack
.resolves(code
);
1073 Object
.assign(ctx
.parsedBody
, parsedBody
);
1075 await manager
.postAuthorization(res
, ctx
);
1076 assert(!ctx
.session
.error
, ctx
.session
.error
);
1077 assert(!res
.end
.firstCall
.args
[0].includes('email'));
1079 it('includes email if accepted in scope', async
function () {
1080 code
.acceptedScopes
= ['profile', 'email'];
1081 manager
.db
.redeemCode
.resolves(true);
1082 manager
.mysteryBox
.unpack
.resolves(code
);
1083 Object
.assign(ctx
.parsedBody
, parsedBody
);
1085 await manager
.postAuthorization(res
, ctx
);
1086 assert(!ctx
.session
.error
);
1087 assert(res
.end
.firstCall
.args
[0].includes('email'));
1089 it('fails if already redeemed', async
function () {
1090 manager
.db
.redeemCode
.resolves(false);
1091 manager
.mysteryBox
.unpack
.resolves(code
);
1092 Object
.assign(ctx
.parsedBody
, parsedBody
);
1094 await manager
.postAuthorization(res
, ctx
);
1095 assert(ctx
.session
.error
);
1097 it('covers bad request', async
function () {
1098 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1099 Object
.assign(ctx
.parsedBody
, parsedBody
);
1101 await manager
.postAuthorization(res
, ctx
);
1102 assert(ctx
.session
.error
);
1104 }); // postAuthorization
1106 describe('_ingestPostAuthorizationRequest', function () {
1107 beforeEach(function () {
1108 sinon
.stub(manager
, '_restoreSessionFromCode');
1109 sinon
.stub(manager
, '_checkSessionMatchingClientId');
1110 sinon
.stub(manager
, '_checkSessionMatchingRedirectUri');
1111 sinon
.stub(manager
, '_checkGrantType');
1112 sinon
.stub(manager
, '_checkSessionMatchingCodeVerifier');
1114 it('covers valid', async
function () {
1115 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1117 me: 'https://profile.example.com/',
1122 await manager
._ingestPostAuthorizationRequest(ctx
);
1123 assert(!ctx
.session
.error
);
1125 it('requires data', async
function () {
1126 delete ctx
.parsedBody
;
1127 await manager
._ingestPostAuthorizationRequest(ctx
);
1128 assert(ctx
.session
.error
);
1130 it('requires me field', async
function () {
1131 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1136 await manager
._ingestPostAuthorizationRequest(ctx
);
1137 assert(ctx
.session
.error
);
1139 it('requires minted field', async
function () {
1140 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1142 me: 'https://profile.example.com/',
1145 await manager
._ingestPostAuthorizationRequest(ctx
);
1146 assert(ctx
.session
.error
);
1148 it('rejects expired code', async
function () {
1149 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1151 me: 'https://profile.example.com/',
1152 minted: Date
.now() - 86400000,
1156 await manager
._ingestPostAuthorizationRequest(ctx
);
1157 assert(ctx
.session
.error
);
1159 }); // _ingestPostAuthorizationRequest
1161 describe('_restoreSessionFromCode', function () {
1163 beforeEach(function () {
1164 sinon
.stub(manager
.mysteryBox
, 'unpack');
1166 codeId: 'cffe1558-35f0-11ec-98bc-0025905f714a',
1167 codeChallengeMethod: 'S256',
1168 codeChallenge: 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ',
1169 clientId: 'https://app.example.com/',
1170 redirectUri: 'https://app.example.com/_redirect',
1171 acceptedScopes: ['profile'],
1173 me: 'https://client.example.com/',
1174 identifier: 'username',
1176 name: 'Firsty McLastname',
1177 email: 'f.mclastname@example.com',
1181 it('covers valid', async
function () {
1182 ctx
.parsedBody
['code'] = 'codeCodeCode';
1183 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1184 const expected
= Object
.assign({}, ctx
, {
1185 session: unpackedCode
,
1187 await manager
._restoreSessionFromCode(ctx
);
1188 assert
.deepStrictEqual(ctx
, expected
);
1189 assert(!ctx
.session
.error
);
1191 it('requires code', async
function () {
1192 ctx
.parsedBody
['code'] = '';
1193 manager
.mysteryBox
.unpack
.resolves({
1194 me: 'https://example.com/me',
1196 await manager
._restoreSessionFromCode(ctx
);
1197 assert(ctx
.session
.error
);
1199 it('covers invalid code', async
function () {
1200 ctx
.parsedBody
['code'] = 'codeCodeCode';
1201 manager
.mysteryBox
.unpack
.rejects();
1202 await manager
._restoreSessionFromCode(ctx
);
1203 assert(ctx
.session
.error
);
1205 it('covers missing code fields', async
function () {
1206 ctx
.parsedBody
['code'] = 'codeCodeCode';
1207 delete unpackedCode
.clientId
;
1208 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1209 await manager
._restoreSessionFromCode(ctx
);
1210 assert(ctx
.session
.error
);
1212 it('covers legacy non-PKCE missing fields', async
function () {
1213 ctx
.parsedBody
['code'] = 'codeCodeCode';
1214 delete unpackedCode
.codeChallengeMethod
;
1215 delete unpackedCode
.codeChallenge
;
1216 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1217 manager
.options
.manager
.allowLegacyNonPKCE
= true;
1218 const expected
= Object
.assign({}, ctx
, {
1219 session: unpackedCode
,
1221 await manager
._restoreSessionFromCode(ctx
);
1222 assert
.deepStrictEqual(ctx
, expected
);
1223 assert(!ctx
.session
.error
);
1225 }); // _restoreSessionFromCode
1227 describe('_checkSessionMatchingClientId', function () {
1228 it('covers valid', async
function () {
1230 clientId: 'https://client.example.com/',
1232 ctx
.parsedBody
['client_id'] = 'https://client.example.com/';
1234 manager
._checkSessionMatchingClientId(ctx
);
1235 assert(!ctx
.session
.error
);
1237 it('covers missing', async
function () {
1239 clientId: 'https://client.example.com/',
1241 ctx
.parsedBody
['client_id'] = undefined;
1243 manager
._checkSessionMatchingClientId(ctx
);
1244 assert(ctx
.session
.error
);
1246 it('covers un-parsable', async
function () {
1248 clientId: 'https://client.example.com/',
1250 ctx
.parsedBody
['client_id'] = 'not a url';
1252 manager
._checkSessionMatchingClientId(ctx
);
1253 assert(ctx
.session
.error
);
1255 it('covers mismatch', async
function () {
1257 clientId: 'https://client.example.com/',
1259 ctx
.parsedBody
['client_id'] = 'https://otherclient.example.com/';
1261 manager
._checkSessionMatchingClientId(ctx
);
1262 assert(ctx
.session
.error
);
1264 }); // _checkSessionMatchingClientId
1266 describe('_checkSessionMatchingRedirectUri', function () {
1267 it('covers valid', async
function () {
1268 ctx
.parsedBody
['redirect_uri'] = 'https://client.example.com/_redirect';
1269 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1271 manager
._checkSessionMatchingRedirectUri(ctx
);
1272 assert(!ctx
.session
.error
);
1274 it('requires field', async
function () {
1275 ctx
.parsedBody
['redirect_uri'] = undefined;
1276 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1278 manager
._checkSessionMatchingRedirectUri(ctx
);
1279 assert(ctx
.session
.error
);
1281 it('requires valid field', async
function () {
1282 ctx
.parsedBody
['redirect_uri'] = 'not a url';
1283 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1285 manager
._checkSessionMatchingRedirectUri(ctx
);
1286 assert(ctx
.session
.error
);
1288 it('requires match', async
function () {
1289 ctx
.parsedBody
['redirect_uri'] = 'https://client.example.com/other';
1290 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1292 manager
._checkSessionMatchingRedirectUri(ctx
);
1293 assert(ctx
.session
.error
);
1295 }); // _checkSessionMatchingRedirectUri
1297 describe('_checkGrantType', function () {
1298 it('covers valid', async
function () {
1299 ctx
.parsedBody
['grant_type'] = 'authorization_code';
1301 manager
._checkGrantType(ctx
);
1302 assert(!ctx
.session
.error
);
1304 it('allows missing, because of one client', async
function () {
1305 ctx
.parsedBody
['grant_type'] = undefined;
1307 manager
._checkGrantType(ctx
);
1308 assert(!ctx
.session
.error
);
1310 it('rejects invalid', async
function () {
1311 ctx
.parsedBody
['grant_type'] = 'pigeon_dance';
1313 manager
._checkGrantType(ctx
);
1314 assert(ctx
.session
.error
);
1316 }); // _checkGrantType
1318 describe('_checkSessionMatchingCodeVerifier', function () {
1319 it('covers valid', async
function () {
1320 ctx
.parsedBody
['code_verifier'] = 'verifier';
1321 ctx
.session
.codeChallengeMethod
= 'S256';
1322 ctx
.session
.codeChallenge
= 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ';
1324 manager
._checkSessionMatchingCodeVerifier(ctx
);
1325 assert(!ctx
.session
.error
);
1327 it('requires field', async
function () {
1328 ctx
.parsedBody
['code_verifier'] = undefined;
1329 ctx
.session
.codeChallengeMethod
= 'S256';
1330 ctx
.session
.codeChallenge
= 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ';
1332 manager
._checkSessionMatchingCodeVerifier(ctx
);
1333 assert(ctx
.session
.error
);
1335 it('requires match', async
function () {
1336 ctx
.parsedBody
['code_verifier'] = 'wrongverifier';
1337 ctx
.session
.codeChallengeMethod
= 'S256';
1338 ctx
.session
.codeChallenge
= 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ';
1340 manager
._checkSessionMatchingCodeVerifier(ctx
);
1341 assert(ctx
.session
.error
);
1343 it('covers legacy non-PKCE missing fields', async
function () {
1344 ctx
.parsedBody
['code_verifier'] = undefined;
1345 ctx
.session
.codeChallengeMethod
= undefined;
1346 ctx
.session
.codeChallenge
= undefined;
1347 manager
.options
.manager
.allowLegacyNonPKCE
= true;
1349 manager
._checkSessionMatchingCodeVerifier(ctx
);
1350 assert(!ctx
.session
.error
);
1352 }); // _checkSessionMatchingCodeVerifier
1354 describe('postToken', function () {
1356 beforeEach(function () {
1357 ctx
.session
.acceptedScopes
= [];
1359 codeId: 'cffe1558-35f0-11ec-98bc-0025905f714a',
1360 codeChallengeMethod: 'S256',
1361 codeChallenge: 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ',
1362 clientId: 'https://app.example.com/',
1363 redirectUri: 'https://app.example.com/return',
1364 acceptedScopes: ['profile', 'email', 'tricks'],
1366 me: 'https://client.example.com/',
1367 identifier: 'username',
1369 name: 'Firsty McLastname',
1370 email: 'f.mclastname@example.com',
1371 url: 'https://example.com/',
1375 describe('Revocation (legacy)', function () {
1376 beforeEach(function () {
1377 sinon
.stub(manager
, '_revokeToken');
1379 it('covers revocation', async
function () {
1380 manager
._revokeToken
.resolves();
1385 await manager
.postToken(req
, res
, ctx
);
1386 assert(manager
._revokeToken
.called
);
1389 describe('Validation (legacy)', function () {
1390 beforeEach(function () {
1391 sinon
.stub(manager
, '_validateToken');
1392 req
.getHeader
.returns({ Authorization: 'Bearer XXX' });
1394 it('covers validation', async
function () {
1395 ctx
.bearer
= { isValid: true };
1396 await manager
.postToken(req
, res
, ctx
);
1397 assert(manager
._validateToken
.called
);
1400 describe('Refresh', function () {
1401 beforeEach(function () {
1402 sinon
.stub(manager
, '_refreshToken');
1404 it('covers refresh', async
function () {
1405 ctx
.parsedBody
['grant_type'] = 'refresh_token';
1406 await manager
.postToken(req
, res
, ctx
);
1407 assert(manager
._refreshToken
.called
);
1410 describe('Ticket Redemption', function () {
1411 beforeEach(function () {
1412 sinon
.stub(manager
, '_ticketAuthToken');
1414 it('covers ticket', async
function () {
1415 ctx
.parsedBody
['grant_type'] = 'ticket';
1416 await manager
.postToken(req
, res
, ctx
);
1417 assert(manager
._ticketAuthToken
.called
);
1419 it('covers no ticket queue', async
function () {
1420 delete options
.queues
.amqp
.url
;
1421 manager
= new Manager(logger
, stubDb
, options
);
1422 sinon
.stub(manager
.communication
, 'fetchProfile');
1423 sinon
.stub(manager
.communication
, 'fetchClientIdentifier');
1424 sinon
.stub(manager
.communication
, 'deliverTicket');
1426 ctx
.parsedBody
['grant_type'] = 'ticket';
1427 await assert
.rejects(() => manager
.postToken(req
, res
, ctx
), ResponseError
);
1429 }); // Ticket Redemption
1430 describe('Code Redemption', function () {
1431 beforeEach(function () {
1432 sinon
.stub(manager
.mysteryBox
, 'unpack');
1433 sinon
.spy(manager
.mysteryBox
, 'pack');
1434 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1436 'redirect_uri': 'https://app.example.com/return',
1440 it('covers invalid code', async
function () {
1441 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1443 await manager
.postToken(req
, res
, ctx
);
1444 assert
.fail(noExpectedException
);
1446 assert(e
instanceof ResponseError
);
1449 it('covers mismatched redirect', async
function () {
1450 ctx
.parsedBody
['redirect_uri'] = 'https://elsewhere.example.com/';
1452 await manager
.postToken(req
, res
, ctx
);
1453 assert
.fail(noExpectedException
);
1455 assert(e
instanceof ResponseError
);
1458 it('covers success', async
function () {
1459 manager
.db
.redeemCode
.resolves(true);
1460 await manager
.postToken(req
, res
, ctx
);
1461 assert(res
.end
.called
);
1462 assert
.strictEqual(manager
.mysteryBox
.pack
.callCount
, 1);
1464 it('covers success with refresh', async
function () {
1465 manager
.db
.redeemCode
.resolves(true);
1466 unpackedCode
.refreshLifespan
= 86400;
1467 unpackedCode
.tokenLifespan
= 86400;
1468 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1469 await manager
.postToken(req
, res
, ctx
);
1470 assert(res
.end
.called
);
1471 assert
.strictEqual(manager
.mysteryBox
.pack
.callCount
, 2);
1473 it('covers redemption failure', async
function () {
1474 manager
.db
.redeemCode
.resolves(false);
1476 await manager
.postToken(req
, res
, ctx
);
1477 assert
.fail(noExpectedException
);
1479 assert(e
instanceof ResponseError
);
1482 it('removes email from profile if not in scope', async
function () {
1483 manager
.db
.redeemCode
.resolves(true);
1484 unpackedCode
.acceptedScopes
= ['profile', 'tricks'];
1485 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1486 await manager
.postToken(req
, res
, ctx
);
1487 assert(res
.end
.called
);
1488 const response
= JSON
.parse(res
.end
.args
[0][0]);
1489 assert(!('email' in response
.profile
));
1492 }); // Code Redemption
1493 describe('Invalid grant_type', function () {
1494 it('throws response error', async
function () {
1495 ctx
.parsedBody
['grant_type'] = 'bad';
1497 await manager
.postToken(req
, res
, ctx
);
1498 assert
.fail(noExpectedException
);
1500 assert(e
instanceof ResponseError
);
1503 }); // Invalid grant_type
1506 describe('_validateToken', function () {
1508 beforeEach(function () {
1510 sinon
.stub(manager
, '_checkTokenValidationRequest');
1512 it('covers valid token', async
function () {
1518 await manager
._validateToken(dbCtx
, req
, res
, ctx
);
1519 assert(res
.end
.called
);
1521 it('covers invalid token', async
function () {
1525 await assert
.rejects(manager
._validateToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1527 it('covers errors', async
function () {
1531 ctx
.session
.error
= 'error';
1532 ctx
.session
.errorDescriptions
= ['error_description'];
1533 await assert
.rejects(manager
._validateToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1535 }); // _validateToken
1537 describe('_checkTokenValidationRequest', function () {
1539 beforeEach(function () {
1541 sinon
.stub(manager
.mysteryBox
, 'unpack');
1543 it('does nothing with no auth header', async
function () {
1544 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1546 it('does nothing with unknown auth header', async
function () {
1547 req
.getHeader
.returns('flarp authy woo');
1548 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1550 it('requires a valid auth token', async
function () {
1551 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1552 req
.getHeader
.returns('Bearer XXX');
1553 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1554 assert(ctx
.session
.error
);
1556 it('requires valid auth token fields', async
function () {
1557 manager
.mysteryBox
.unpack
.resolves({});
1558 req
.getHeader
.returns('Bearer XXX');
1559 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1560 assert(ctx
.session
.error
);
1562 it('covers no token', async
function () {
1563 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1564 req
.getHeader
.returns('Bearer XXX');
1565 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1566 assert(ctx
.session
.error
);
1568 it('covers db error', async
function () {
1569 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1570 manager
.db
.tokenGetByCodeId
.rejects(expectedException
);
1571 req
.getHeader
.returns('Bearer XXX');
1572 await assert
.rejects(manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
), expectedException
);
1574 it('valid token', async
function () {
1575 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1576 manager
.db
.tokenGetByCodeId
.resolves({
1578 expires: new Date(Date
.now() + 86400000),
1580 req
.getHeader
.returns('Bearer XXX');
1581 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1582 assert
.strictEqual(ctx
.bearer
.isValid
, true);
1584 it('revoked token', async
function () {
1585 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1586 manager
.db
.tokenGetByCodeId
.resolves({
1588 expires: new Date(Date
.now() + 86400000),
1590 req
.getHeader
.returns('Bearer XXX');
1591 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1592 assert
.strictEqual(ctx
.bearer
.isValid
, false);
1594 it('expired token', async
function () {
1595 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1596 manager
.db
.tokenGetByCodeId
.resolves({
1598 expires: new Date(Date
.now() - 86400000),
1600 req
.getHeader
.returns('Bearer XXX');
1601 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1602 assert
.strictEqual(ctx
.bearer
.isValid
, false);
1604 }); // _checkTokenValidationRequest
1606 describe('postIntrospection', function () {
1607 let inactiveToken
, activeToken
, dbResponse
;
1608 beforeEach(function () {
1610 profile: 'https://profile.example.com/',
1611 clientId: 'https://client.example.com/',
1612 scopes: ['scope1', 'scope2'],
1613 created: new Date(),
1616 inactiveToken
= JSON
.stringify({
1619 activeToken
= JSON
.stringify({
1621 me: dbResponse
.profile
,
1622 'client_id': dbResponse
.clientId
,
1623 scope: dbResponse
.scopes
.join(' '),
1624 iat: Math
.ceil(dbResponse
.created
.getTime() / 1000),
1626 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '7e9991dc-9cd5-11ec-85c4-0025905f714a' });
1627 manager
.db
.tokenGetByCodeId
.resolves(dbResponse
);
1629 it('covers bad token', async
function () {
1630 manager
.mysteryBox
.unpack
.rejects();
1631 await manager
.postIntrospection(res
, ctx
);
1632 assert(res
.end
.called
);
1633 assert
.strictEqual(res
.end
.args
[0][0], inactiveToken
);
1635 it('covers token not in db', async
function () {
1636 manager
.db
.tokenGetByCodeId
.resolves();
1637 await manager
.postIntrospection(res
, ctx
);
1638 assert(res
.end
.called
);
1639 assert
.strictEqual(res
.end
.args
[0][0], inactiveToken
);
1641 it('covers valid token', async
function () {
1642 await manager
.postIntrospection(res
, ctx
);
1643 assert(res
.end
.called
);
1644 assert
.strictEqual(res
.end
.args
[0][0], activeToken
);
1646 it('covers expired token', async
function () {
1647 dbResponse
.expires
= new Date((new Date()).getTime() - 86400000);
1648 await manager
.postIntrospection(res
, ctx
);
1649 assert(res
.end
.called
);
1650 assert
.strictEqual(res
.end
.args
[0][0], inactiveToken
);
1652 it('covers expiring token', async
function () {
1653 dbResponse
.expires
= new Date((new Date()).getTime() + 86400000);
1654 activeToken
= JSON
.stringify({
1656 me: dbResponse
.profile
,
1657 'client_id': dbResponse
.clientId
,
1658 scope: dbResponse
.scopes
.join(' '),
1659 iat: Math
.ceil(dbResponse
.created
.getTime() / 1000),
1660 exp: Math
.ceil(dbResponse
.expires
/ 1000),
1662 await manager
.postIntrospection(res
, ctx
);
1663 assert(res
.end
.called
);
1664 assert
.strictEqual(res
.end
.args
[0][0], activeToken
);
1666 it('covers ticket', async
function () {
1667 ctx
.parsedBody
['token_hint_type'] = 'ticket';
1668 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
1669 manager
.mysteryBox
.unpack
.resolves({
1670 c: '515172ae-5b0b-11ed-a6af-0025905f714a',
1671 iss: nowEpoch
- 86400,
1672 exp: nowEpoch
+ 86400,
1673 sub: 'https://subject.exmaple.com/',
1674 res: 'https://profile.example.com/feed',
1675 scope: ['read', 'role:private'],
1677 profile: 'https://profile.example.com/',
1679 await manager
.postIntrospection(res
, ctx
);
1680 assert(res
.end
.called
);
1682 }); // postIntrospection
1684 describe('_revokeToken', function () {
1686 beforeEach(function () {
1689 it('requires token field', async
function () {
1690 await manager
._revokeToken(dbCtx
, res
, ctx
);
1691 assert(res
.end
.called
);
1692 assert
.strictEqual(res
.statusCode
, 400);
1694 it('requires parsable token', async
function () {
1695 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ notC: 'foop' });
1696 ctx
.parsedBody
['token'] = 'invalid token';
1697 ctx
.parsedBody
['token_type_hint'] = 'access_token';
1698 await manager
._revokeToken(dbCtx
, res
, ctx
);
1699 assert(res
.end
.called
);
1700 assert
.strictEqual(res
.statusCode
, 400);
1702 it('requires parsable token', async
function () {
1703 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves();
1704 ctx
.parsedBody
['token'] = 'invalid token';
1705 ctx
.parsedBody
['token_type_hint'] = 'refresh_token';
1706 await manager
._revokeToken(dbCtx
, res
, ctx
);
1707 assert(res
.end
.called
);
1708 assert
.strictEqual(res
.statusCode
, 400);
1710 it('succeeds', async
function () {
1711 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1712 ctx
.parsedBody
['token'] = 'valid token';
1713 await manager
._revokeToken(dbCtx
, res
, ctx
);
1714 assert(manager
.db
.tokenRevokeByCodeId
.called
);
1715 assert(res
.end
.called
);
1717 it('succeeds for refresh token', async
function () {
1718 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ rc: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1719 ctx
.parsedBody
['token'] = 'valid token';
1720 await manager
._revokeToken(dbCtx
, res
, ctx
);
1721 assert(manager
.db
.tokenRefreshRevokeByCodeId
.called
);
1722 assert(res
.end
.called
);
1724 it('covers non-revokable token', async
function () {
1725 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1726 manager
.db
.tokenRevokeByCodeId
.rejects(new UnexpectedResult());
1727 ctx
.parsedBody
['token'] = 'valid token';
1728 await manager
._revokeToken(dbCtx
, res
, ctx
);
1729 assert
.strictEqual(res
.statusCode
, 404);
1731 it('covers failure', async
function () {
1732 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1733 manager
.db
.tokenRevokeByCodeId
.rejects(expectedException
);
1734 ctx
.parsedBody
['token'] = 'valid token';
1735 ctx
.parsedBody
['token_type_hint'] = 'ignores_bad_hint';
1736 await assert
.rejects(manager
._revokeToken(dbCtx
, res
, ctx
), expectedException
, noExpectedException
);
1740 describe('_scopeDifference', function () {
1741 let previousScopes
, requestedScopes
;
1742 beforeEach(function () {
1743 previousScopes
= ['a', 'b', 'c'];
1744 requestedScopes
= ['b', 'c', 'd'];
1746 it('covers', function () {
1747 const expected
= ['a'];
1748 const result
= Manager
._scopeDifference(previousScopes
, requestedScopes
);
1749 assert
.deepStrictEqual(result
, expected
);
1751 }); // _scopeDifference
1753 describe('_refreshToken', function () {
1755 beforeEach(function () {
1757 ctx
.parsedBody
['client_id'] = 'https://client.example.com/';
1758 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
1759 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
1760 rc: '03bb8ab0-1dc7-11ed-99f2-0025905f714a',
1761 ts: nowEpoch
- 86400,
1762 exp: nowEpoch
+ 86400,
1764 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('newToken');
1765 const futureDate
= new Date(Date
.now() + 86400000);
1766 manager
.db
.tokenGetByCodeId
.resolves({
1767 refreshExpires: futureDate
,
1769 clientId: 'https://client.example.com/',
1770 scopes: ['profile', 'create'],
1772 manager
.db
.refreshCode
.resolves({
1773 expires: futureDate
,
1774 refreshExpires: futureDate
,
1777 it('requires a token', async
function () {
1778 manager
.mysteryBox
.unpack
.rejects();
1779 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1781 it('requires token to have refresh field', async
function () {
1782 manager
.mysteryBox
.unpack
.resolves();
1783 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1785 it('requires token to exist in db', async
function () {
1786 manager
.db
.tokenGetByCodeId
.resolves();
1787 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1789 it('requires token be refreshable', async
function () {
1790 manager
.db
.tokenGetByCodeId
.resolves({
1791 refreshExpires: undefined,
1793 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1795 it('requires refresh of token not be expired', async
function () {
1796 manager
.db
.tokenGetByCodeId
.resolves({
1797 refreshExpires: 1000,
1799 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1801 it('requires token not to have been already refreshed', async
function () {
1802 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
1803 manager
.mysteryBox
.unpack
.resolves({
1804 rc: '03bb8ab0-1dc7-11ed-99f2-0025905f714a',
1805 ts: nowEpoch
- 864000,
1806 exp: nowEpoch
- 86400,
1808 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1810 it('requires client_id requesting refresh match', async
function () {
1811 ctx
.parsedBody
['client_id'] = 'https://wrong.example.com/';
1812 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1814 it('succeeds', async
function () {
1815 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1816 assert(res
.end
.called
);
1818 it('covers non-expiring', async
function () {
1819 manager
.db
.tokenGetByCodeId
.resolves({
1820 refreshExpires: new Date(Date
.now() + 86400000),
1822 clientId: 'https://client.example.com/',
1823 scopes: ['profile', 'create'],
1825 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1826 assert(res
.end
.called
);
1828 it('covers profile and email', async
function () {
1829 manager
.db
.tokenGetByCodeId
.resolves({
1830 refreshExpires: new Date(Date
.now() + 86400000),
1832 clientId: 'https://client.example.com/',
1833 scopes: ['profile', 'email', 'create'],
1835 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1836 assert(res
.end
.called
);
1838 it('succeeds with scope reduction', async
function () {
1839 ctx
.parsedBody
['scope'] = 'profile fancy';
1840 manager
.db
.tokenGetByCodeId
.resolves({
1841 refreshExpires: new Date(Date
.now() + 86400000),
1842 clientId: 'https://client.example.com/',
1843 scopes: ['profile', 'create'],
1845 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1846 assert(res
.end
.called
);
1848 it('covers refresh failed', async
function () {
1849 manager
.db
.refreshCode
.resolves();
1850 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1852 }); // _refreshToken
1854 describe('_mintTicket', function () {
1856 beforeEach(function () {
1859 subject: 'https://third-party.example.com/',
1860 resource: 'https://private.example.com/feed',
1862 identifier: 'account',
1863 profile: 'https://profile.example.com/',
1864 ticketLifespanSeconds: 86400,
1867 it('covers', async
function () {
1868 const expected
= 'xxx';
1869 sinon
.stub(manager
.mysteryBox
, 'pack').resolves(expected
);
1870 const result
= await manager
._mintTicket(dbCtx
, payload
);
1871 assert
.strictEqual(result
, expected
);
1875 describe('_ticketAuthToken', function () {
1876 let dbCtx
, ticketPayload
, nowEpoch
;
1877 beforeEach(function () {
1879 nowEpoch
= Math
.ceil(Date
.now() / 1000);
1881 c: '5ec17f78-5576-11ed-b444-0025905f714a',
1882 iss: nowEpoch
- 86400,
1883 exp: nowEpoch
+ 86400,
1884 sub: 'https://third-party.example.com/',
1885 res: 'https://private.example.com/feed',
1886 scope: ['read', 'flap'],
1888 profile: 'https://profile.example.com/',
1890 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves(ticketPayload
);
1891 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('ticket');
1893 it('covers invalid ticket', async
function () {
1894 manager
.mysteryBox
.unpack
.resolves();
1895 await assert
.rejects(() => manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1897 it('covers expired ticket', async
function () {
1898 manager
.mysteryBox
.unpack
.resolves({
1899 c: '5ec17f78-5576-11ed-b444-0025905f714a',
1900 iss: nowEpoch
- 172800,
1901 exp: nowEpoch
- 86400,
1902 sub: 'https://third-party.example.com/',
1903 res: 'https://private.example.com/feed',
1904 scope: ['read', 'flap'],
1906 profile: 'https://profile.example.com/',
1908 await assert
.rejects(() => manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1910 it('covers success', async
function () {
1911 manager
.db
.redeemCode
.resolves(true);
1912 await manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
);
1913 assert(res
.end
.called
);
1915 it('covers invalid redeem', async
function () {
1916 manager
.db
.redeemCode
.resolves(false);
1917 await assert
.rejects(() => manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1919 }); // _ticketAuthToken
1921 describe('postRevocation', function () {
1922 beforeEach(function () {
1923 sinon
.stub(manager
, '_revokeToken');
1925 it('covers success', async
function () {
1926 manager
._revokeToken
.resolves();
1927 await manager
.postRevocation(res
, ctx
);
1928 assert(manager
._revokeToken
.called
);
1930 it('covers failure', async
function () {
1931 manager
._revokeToken
.rejects(expectedException
);
1932 await assert
.rejects(manager
.postRevocation(res
, ctx
));
1934 }); // postRevocation
1936 describe('postUserInfo', function () {
1937 beforeEach(function () {
1938 ctx
.parsedBody
['token'] = 'XXX';
1939 sinon
.stub(manager
.mysteryBox
, 'unpack');
1941 it('requires a token', async
function () {
1942 delete ctx
.parsedBody
.token
;
1943 await manager
.postUserInfo(res
, ctx
);
1944 assert(res
.end
.called
);
1945 assert
.strictEqual(res
.statusCode
, 400);
1947 it('requires a valid token', async
function () {
1948 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1949 await manager
.postUserInfo(res
, ctx
);
1950 assert(res
.end
.called
);
1951 assert
.strictEqual(res
.statusCode
, 401);
1953 it('requires token to have profile scope', async
function () {
1954 manager
.mysteryBox
.unpack
.resolves({});
1955 manager
.db
.tokenGetByCodeId
.resolves({
1958 await manager
.postUserInfo(res
, ctx
);
1959 assert(res
.end
.called
);
1960 assert
.strictEqual(res
.statusCode
, 403);
1962 it('succeeds', async
function () {
1963 manager
.mysteryBox
.unpack
.resolves({});
1964 manager
.db
.tokenGetByCodeId
.resolves({
1965 scopes: ['profile', 'email'],
1967 url: 'https://example.com/',
1968 email: 'user@example.com',
1971 await manager
.postUserInfo(res
, ctx
);
1972 assert(res
.end
.called
);
1973 assert
.strictEqual(res
.statusCode
, 200);
1975 it('succeeds, and does not include email without scope', async
function () {
1976 manager
.mysteryBox
.unpack
.resolves({});
1977 manager
.db
.tokenGetByCodeId
.resolves({
1978 scopes: ['profile'],
1980 url: 'https://example.com/',
1981 email: 'user@example.com',
1984 await manager
.postUserInfo(res
, ctx
);
1985 assert(res
.end
.called
);
1986 assert
.strictEqual(res
.statusCode
, 200);
1987 const response
= JSON
.parse(res
.end
.args
[0][0]);
1988 assert(!('email' in response
));
1992 describe('getAdmin', function () {
1993 beforeEach(function () {
1994 manager
.db
.profilesScopesByIdentifier
.resolves({
1996 'https://profile.example.com/': {
1999 description: 'a scope',
2000 profiles: ['https://profile.example.com/'],
2007 description: 'a scope',
2008 profiles: ['https://profile.example.com/'],
2011 profiles: ['https://profile.example.com/'],
2013 manager
.db
.tokensGetByIdentifier
.resolves();
2015 it('covers', async
function () {
2016 await manager
.getAdmin(res
, ctx
);
2020 describe('postAdmin', function () {
2021 beforeEach(function () {
2022 manager
.db
.profilesScopesByIdentifier
.resolves({
2024 'https://profile.example.com/': {
2027 description: 'a scope',
2028 profiles: ['https://profile.example.com/'],
2035 description: 'a scope',
2036 profiles: ['https://profile.example.com/'],
2039 profiles: ['https://profile.example.com/'],
2041 manager
.db
.tokensGetByIdentifier
.resolves([]);
2042 manager
.db
.tokenRevokeByCodeId
.resolves();
2043 manager
.db
.profileIdentifierInsert
.resolves();
2044 manager
.db
.profileScopesSetAll
.resolves();
2045 manager
.communication
.fetchProfile
.resolves({
2047 authorizationEndpoint: manager
.selfAuthorizationEndpoint
,
2051 describe('save-scopes action', function () {
2052 beforeEach(function () {
2053 ctx
.parsedBody
['action'] = 'save-scopes';
2054 ctx
.parsedBody
['scopes-https://profile/example.com/[]'] = ['scope1', 'scope2'];
2056 it('covers saving scopes', async
function () {
2057 await manager
.postAdmin(res
, ctx
);
2058 assert(ctx
.notifications
.length
);
2059 assert(manager
.db
.profileScopesSetAll
.called
);
2061 it('covers saving scopes error', async
function () {
2062 manager
.db
.profileScopesSetAll
.rejects();
2063 await manager
.postAdmin(res
, ctx
);
2064 assert(ctx
.errors
.length
);
2066 }); // save-scopes action
2067 describe('new-profile action', function () {
2068 beforeEach(function () {
2069 ctx
.parsedBody
['action'] = 'new-profile';
2071 it('covers new profile', async
function () {
2072 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2073 await manager
.postAdmin(res
, ctx
);
2074 assert(ctx
.notifications
.length
);
2075 assert(manager
.db
.profileIdentifierInsert
.called
);
2076 assert(manager
.db
.profileScopesSetAll
.called
);
2078 it('covers invalid profile', async
function () {
2079 ctx
.parsedBody
['action'] = 'new-profile';
2080 ctx
.parsedBody
['profile'] = 'not a url';
2081 await manager
.postAdmin(res
, ctx
);
2082 assert(ctx
.errors
.length
);
2084 it('covers other validation failure', async
function () {
2085 sinon
.stub(manager
.communication
, 'validateProfile').rejects(expectedException
);
2086 ctx
.parsedBody
['action'] = 'new-profile';
2087 ctx
.parsedBody
['profile'] = 'not a url';
2088 await manager
.postAdmin(res
, ctx
);
2089 assert(ctx
.errors
.length
);
2091 it('covers mismatched profile', async
function () {
2092 ctx
.parsedBody
['action'] = 'new-profile';
2093 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2094 manager
.communication
.fetchProfile
.resolves({
2096 authorizationEndpoint: 'https://other.example.com/auth',
2099 await manager
.postAdmin(res
, ctx
);
2100 assert(ctx
.errors
.length
);
2102 it('covers new profile error', async
function () {
2103 ctx
.parsedBody
['action'] = 'new-profile';
2104 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2105 manager
.db
.profileIdentifierInsert
.rejects();
2106 await manager
.postAdmin(res
, ctx
);
2107 assert(ctx
.errors
.length
);
2109 }); // new-profile action
2110 describe('new-scope action', function () {
2111 beforeEach(function () {
2112 ctx
.parsedBody
['action'] = 'new-scope';
2114 it('covers new scope', async
function () {
2115 ctx
.parsedBody
['scope'] = 'newscope';
2116 await manager
.postAdmin(res
, ctx
);
2117 assert(ctx
.notifications
.length
);
2118 assert(manager
.db
.scopeUpsert
.called
);
2120 it('covers bad scope', async
function () {
2121 ctx
.parsedBody
['scope'] = 'bad scope';
2122 await manager
.postAdmin(res
, ctx
);
2123 assert(ctx
.errors
.length
);
2125 it('covers new scope error', async
function () {
2126 ctx
.parsedBody
['scope'] = 'newscope';
2127 manager
.db
.scopeUpsert
.rejects();
2128 await manager
.postAdmin(res
, ctx
);
2129 assert(ctx
.errors
.length
);
2131 it('covers empty scope', async
function () {
2132 delete ctx
.parsedBody
.scope
;
2133 await manager
.postAdmin(res
, ctx
);
2134 assert(!ctx
.errors
.length
);
2136 }); // new-scope action
2137 describe('delete-scope-* action', function () {
2138 beforeEach(function () {
2139 ctx
.parsedBody
['action'] = 'delete-scope-food%3Ayum';
2141 it('covers delete', async
function () {
2142 manager
.db
.scopeDelete
.resolves(true);
2143 await manager
.postAdmin(res
, ctx
);
2144 assert(ctx
.notifications
.length
);
2145 assert(manager
.db
.scopeDelete
.called
);
2147 it('covers no delete', async
function () {
2148 manager
.db
.scopeDelete
.resolves(false);
2149 await manager
.postAdmin(res
, ctx
);
2150 assert(ctx
.notifications
.length
);
2151 assert(manager
.db
.scopeDelete
.called
);
2153 it('covers delete error', async
function () {
2154 manager
.db
.scopeDelete
.rejects();
2155 await manager
.postAdmin(res
, ctx
);
2156 assert(ctx
.errors
.length
);
2157 assert(manager
.db
.scopeDelete
.called
);
2159 it('ignores empty scope', async
function () {
2160 ctx
.parsedBody
['action'] = 'delete-scope-';
2161 await manager
.postAdmin(res
, ctx
);
2162 assert(manager
.db
.scopeDelete
.notCalled
);
2163 assert(!ctx
.notifications
.length
);
2164 assert(!ctx
.errors
.length
);
2166 }); // delete-scope-* action
2167 describe('revoke-* action', function () {
2168 beforeEach(function () {
2169 ctx
.parsedBody
['action'] = 'revoke-b1591c00-9cb7-11ec-a05c-0025905f714a';
2171 it('covers revocation', async
function () {
2172 await manager
.postAdmin(res
, ctx
);
2173 assert(ctx
.notifications
.length
);
2174 assert(manager
.db
.tokenRevokeByCodeId
.called
);
2176 it('covers revocation error', async
function () {
2177 manager
.db
.tokenRevokeByCodeId
.rejects();
2178 await manager
.postAdmin(res
, ctx
);
2179 assert(ctx
.errors
.length
);
2181 it('covers no code', async
function () {
2182 ctx
.parsedBody
['action'] = 'revoke-';
2183 await manager
.postAdmin(res
, ctx
);
2184 assert(!ctx
.notifications
.length
);
2185 assert(!ctx
.errors
.length
);
2186 assert(manager
.db
.tokenRevokeByCodeId
.notCalled
);
2188 }); // revoke-* action
2189 it('covers empty action', async
function () {
2190 delete ctx
.parsedBody
.action
;
2191 await manager
.postAdmin(res
, ctx
);
2192 assert(!ctx
.errors
.length
);
2194 it('covers unknown action', async
function () {
2195 ctx
.parsedBody
['action'] = 'unsupported-action';
2196 await manager
.postAdmin(res
, ctx
);
2197 assert(ctx
.errors
.length
);
2201 describe('getAdminTicket', function () {
2202 it('covers', async
function () {
2203 manager
.db
.profilesScopesByIdentifier
.resolves({ scopeIndex: {} });
2204 await manager
.getAdminTicket(res
, ctx
);
2205 assert(res
.end
.called
);
2207 }); // getAdminTicket
2209 describe('postAdminTicket', function () {
2210 beforeEach(function () {
2211 ctx
.parsedBody
['action'] = 'proffer-ticket';
2212 ctx
.parsedBody
['scopes[]'] = ['read', 'role:private'];
2213 ctx
.parsedBody
['adhoc'] = 'adhoc_scope';
2214 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2215 ctx
.parsedBody
['resource'] = 'https://profile.example.com/feed';
2216 ctx
.parsedBody
['subject'] = 'https://subject.example.com/';
2217 manager
.db
.profilesScopesByIdentifier
.resolves({ scopeIndex: {} });
2218 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('ticket');
2219 manager
.communication
.fetchProfile
.resolves({
2221 ticketEndpoint: 'https://example.com/ticket',
2225 it('covers success', async
function () {
2226 await manager
.postAdminTicket(res
, ctx
);
2227 assert(res
.end
.called
);
2228 assert
.strictEqual(ctx
.errors
.length
, 0);
2229 assert
.strictEqual(ctx
.notifications
.length
, 1);
2231 it('requires params', async
function () {
2232 delete ctx
.parsedBody
['adhoc'];
2233 ctx
.parsedBody
['profile'] = 'bad url';
2234 ctx
.parsedBody
['resource'] = 'bad url';
2235 ctx
.parsedBody
['subject'] = 'bad url';
2236 ctx
.parsedBody
['scopes[]'] = ['fl"hrgl', 'email'];
2237 await manager
.postAdminTicket(res
, ctx
);
2238 assert(res
.end
.called
);
2239 assert
.strictEqual(ctx
.errors
.length
, 5);
2240 assert
.strictEqual(ctx
.notifications
.length
, 0);
2242 it('ignores unknown action', async
function () {
2243 ctx
.parsedBody
['action'] = 'prove-dough';
2244 await manager
.postAdminTicket(res
, ctx
);
2245 assert(res
.end
.called
);
2247 it('covers delivery failure', async
function () {
2248 manager
.communication
.deliverTicket
.rejects(expectedException
);
2249 await manager
.postAdminTicket(res
, ctx
);
2250 assert(res
.end
.called
);
2251 assert
.strictEqual(ctx
.errors
.length
, 1);
2252 assert
.strictEqual(ctx
.notifications
.length
, 0);
2254 it('covers no ticket endpoint', async
function () {
2255 manager
.communication
.fetchProfile
.resolves({
2259 await manager
.postAdminTicket(res
, ctx
);
2260 assert(res
.end
.called
);
2261 assert
.strictEqual(ctx
.errors
.length
, 1);
2262 assert
.strictEqual(ctx
.notifications
.length
, 0);
2264 it('covers bad ticket endpoint', async
function () {
2265 manager
.communication
.fetchProfile
.resolves({
2267 ticketEndpoint: 'not a url',
2270 await manager
.postAdminTicket(res
, ctx
);
2271 assert(res
.end
.called
);
2272 assert
.strictEqual(ctx
.errors
.length
, 1);
2273 assert
.strictEqual(ctx
.notifications
.length
, 0);
2275 }); // postAdminTicket
2277 describe('postTicket', function () {
2278 beforeEach(function () {
2280 ticket: 'ticket123',
2281 resource: 'https://blog.example.com/',
2282 subject: 'https://otheruser.example.com/',
2283 iss: 'https://ia.example.com/',
2286 it('accepts a ticket for a known profile', async
function () {
2287 manager
.db
.profileIsValid
.resolves(true);
2288 await manager
.postTicket(req
, res
, ctx
);
2289 assert(res
.end
.called
);
2290 assert
.strictEqual(res
.statusCode
, 202);
2292 it('rejects invalid resource', async
function () {
2293 ctx
.parsedBody
.resource
= 'invalid url';
2294 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), ResponseError
);
2296 it('rejects invalid subject', async
function () {
2297 manager
.db
.profileIsValid(false);
2298 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), ResponseError
);
2300 it('covers queue publish failure', async
function () {
2301 manager
.db
.profileIsValid
.resolves(true);
2302 manager
.queuePublisher
.publish
.rejects(expectedException
);
2303 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), expectedException
);
2305 it('covers no ticket queue', async
function () {
2306 delete options
.queues
.amqp
.url
;
2307 manager
= new Manager(logger
, stubDb
, options
);
2308 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), ResponseError
);
2310 it('covers no issuer', async
function () {
2311 delete ctx
.parsedBody
.iss
;
2312 manager
.db
.profileIsValid
.resolves(true);
2313 await manager
.postTicket(req
, res
, ctx
);
2314 assert(res
.end
.called
);
2315 assert
.strictEqual(res
.statusCode
, 202);
2317 it('covers bad issuer', async
function () {
2318 ctx
.parsedBody
.iss
= 'not a url';
2319 manager
.db
.profileIsValid
.resolves(true);
2320 await manager
.postTicket(req
, res
, ctx
);
2321 assert(res
.end
.called
);
2322 assert
.strictEqual(res
.statusCode
, 202);
2327 describe('queuedTicketProcessor', function () {
2328 let channel
, content
;
2329 const message
= () => ({
2330 content: Buffer
.from(JSON
.stringify(content
)),
2332 beforeEach(function () {
2337 ticket: 'XXXticketXXX',
2338 resource: 'https://blog.example.com/',
2339 subject: 'https://otheruser.exmaple.com/',
2340 iss: 'https://ia.example.com/',
2341 epochMs: Date
.now(),
2344 it('redeems a ticket', async
function () {
2345 await manager
.queuedTicketProcessor(channel
, message());
2346 assert(manager
.queuePublisher
.publish
.called
);
2347 assert(channel
.ack
.called
);
2349 it('redeems a ticket, missing issuer', async
function () {
2351 await manager
.queuedTicketProcessor(channel
, message());
2352 assert(manager
.queuePublisher
.publish
.called
);
2353 assert(channel
.ack
.called
);
2355 it('covers bad message', async
function () {
2356 await manager
.queuedTicketProcessor(channel
, { content: 'diddly' });
2357 assert(channel
.ack
.called
);
2359 it('covers bad issuer', async
function () {
2360 content
.iss
= 'not a url';
2361 await manager
.queuedTicketProcessor(channel
, message());
2362 assert(manager
.queuePublisher
.publish
.called
);
2364 it('covers bad resource', async
function () {
2365 content
.resource
= 'not a url';
2366 await manager
.queuedTicketProcessor(channel
, message());
2367 assert(manager
.communication
.redeemTicket
.notCalled
);
2368 assert(manager
.queuePublisher
.publish
.notCalled
);
2369 assert(channel
.ack
.called
);
2371 it('covers failed redemption', async
function () {
2372 const expectedException
= new Error('oh no');
2373 manager
.communication
.redeemTicket
.rejects(expectedException
);
2374 assert
.rejects(() => manager
.queuedTicketProcessor(channel
, message()), expectedException
);
2376 it('covers failed publish', async
function () {
2377 const expectedException
= new Error('oh no');
2378 manager
.queuePublisher
.publish
.rejects(expectedException
);
2379 assert
.rejects(() => manager
.queuedTicketProcessor(channel
, message()), expectedException
);
2381 }); // queuedTicketProcessor
2383 describe('getAdminMaintenance', function () {
2384 it('covers information', async
function () {
2385 await manager
.getAdminMaintenance(res
, ctx
);
2386 assert(res
.end
.called
);
2388 it('covers tasks', async
function () {
2390 [Enum
.Chore
.CleanTokens
]: '',
2392 await manager
.getAdminMaintenance(res
, ctx
);
2393 assert(res
.end
.called
);
2395 }); // getAdminMaintenance