3 const assert
= require('assert');
4 const sinon
= require('sinon');
6 const Manager
= require('../../src/manager');
7 const Config
= require('../../config');
8 const Enum
= require('../../src/enum');
9 const { ResponseError
} = require('../../src/errors');
10 const { UnexpectedResult
} = require('../../src/db/errors');
11 const dns
= require('dns');
13 const StubDatabase
= require('../stub-db');
14 const StubLogger
= require('../stub-logger');
16 const expectedException
= new Error('oh no');
17 const noExpectedException
= 'did not get expected exception';
19 describe('Manager', function () {
20 let manager
, options
, stubDb
, logger
;
23 beforeEach(function () {
24 logger
= new StubLogger();
26 stubDb
= new StubDatabase();
28 options
= new Config('test');
30 getHeader : sinon
.stub(),
34 setHeader: sinon
.stub(),
45 manager
= new Manager(logger
, stubDb
, options
);
46 sinon
.stub(manager
.communication
, 'fetchProfile');
47 sinon
.stub(manager
.communication
, 'fetchClientIdentifier');
48 sinon
.stub(manager
.communication
, 'deliverTicket');
49 sinon
.stub(manager
.communication
, 'redeemTicket');
50 sinon
.stub(dns
.promises
, 'lookup').resolves([{ family: 4, address: '10.11.12.13' }]);
51 sinon
.stub(manager
.queuePublisher
, 'connect');
52 sinon
.stub(manager
.queuePublisher
, 'establishAMQPPlumbing');
53 sinon
.stub(manager
.queuePublisher
, 'publish');
54 sinon
.stub(manager
.queueConsumer
, 'connect');
55 sinon
.stub(manager
.queueConsumer
, 'establishAMQPPlumbing');
56 sinon
.stub(manager
.queueConsumer
, 'consume');
59 afterEach(function () {
63 describe('constructor', function () {
64 it('instantiates', function () {
67 it('covers no queuing', function () {
68 options
.queues
.amqp
.url
= undefined;
69 manager
= new Manager(logger
, stubDb
, options
);
74 describe('initialize', function () {
76 beforeEach(function () {
77 spy
= sinon
.spy(manager
, '_connectQueues');
79 it('covers', async
function () {
80 await manager
.initialize();
83 it('covers no queue', async
function () {
84 delete options
.queues
.amqp
.url
;
85 manager
= new Manager(logger
, stubDb
, options
);
86 await manager
.initialize();
87 assert(spy
.notCalled
);
91 describe('getRoot', function () {
92 it('normal response', async
function () {
93 await manager
.getRoot(res
, ctx
);
94 assert(res
.end
.called
);
98 describe('getMeta', function () {
99 it('normal response', async
function () {
100 await manager
.getMeta(res
, ctx
);
101 assert(res
.end
.called
);
102 JSON
.parse(res
.end
.args
[0][0]);
104 it('covers no ticket queue', async
function () {
105 delete options
.queues
.amqp
.url
;
106 manager
= new Manager(logger
, stubDb
, options
);
107 await manager
.getMeta(res
, ctx
);
108 assert(res
.end
.called
);
112 describe('getHealthcheck', function () {
113 it('normal response', async
function () {
114 await manager
.getHealthcheck(res
, ctx
);
115 assert(res
.end
.called
);
117 }); // getHealthcheck
119 describe('getAuthorization', function () {
120 it('covers missing redirect fields', async
function () {
121 await manager
.getAuthorization(res
, ctx
);
122 assert
.strictEqual(res
.statusCode
, 400);
124 it('requires a configured profile', async
function () {
125 manager
.db
.profilesScopesByIdentifier
.resolves({
140 manager
.communication
.fetchClientIdentifier
.resolves({
143 ctx
.authenticationId
= 'username';
144 Object
.assign(ctx
.queryParams
, {
145 'client_id': 'https://client.example.com/',
146 'redirect_uri': 'https://client.example.com/action',
147 'response_type': 'code',
149 'code_challenge_method': 'S256',
150 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
151 'scope': 'profile email',
153 await manager
.getAuthorization(res
, ctx
);
154 assert
.strictEqual(res
.statusCode
, 302);
155 assert(ctx
.session
.error
);
156 assert(res
.setHeader
.called
);
158 it('covers valid', async
function () {
159 manager
.db
.profilesScopesByIdentifier
.resolves({
161 'https://profile.example.com/': {
164 profiles: ['https://profile.example.com'],
179 profiles: ['https://profile.example.com/'],
182 profiles: ['https://profile.example.com/'],
184 manager
.communication
.fetchClientIdentifier
.resolves({
187 ctx
.authenticationId
= 'username';
188 Object
.assign(ctx
.queryParams
, {
189 'client_id': 'https://client.example.com/',
190 'redirect_uri': 'https://client.example.com/action',
191 'response_type': 'code',
193 'code_challenge_method': 'S256',
194 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
195 'scope': 'profile email',
196 'me': 'https://profile.example.com/',
198 await manager
.getAuthorization(res
, ctx
);
199 assert
.strictEqual(res
.statusCode
, 200);
200 assert
.strictEqual(ctx
.session
.error
, undefined);
201 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 0);
202 assert
.strictEqual(ctx
.notifications
.length
, 0);
204 it('succeeds with mismatched profile hint', async
function () {
205 manager
.db
.profilesScopesByIdentifier
.resolves({
207 'https://profile.example.com/': {
210 profiles: ['https://profile.example.com'],
225 profiles: ['https://profile.example.com/'],
228 profiles: ['https://profile.example.com/'],
230 manager
.communication
.fetchClientIdentifier
.resolves({
233 ctx
.authenticationId
= 'username';
234 Object
.assign(ctx
.queryParams
, {
235 'client_id': 'https://client.example.com/',
236 'redirect_uri': 'https://client.example.com/action',
237 'response_type': 'code',
239 'code_challenge_method': 'S256',
240 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
241 'scope': 'profile email',
242 'me': 'https://somethingelse.example.com/',
244 await manager
.getAuthorization(res
, ctx
);
245 assert(!('me' in ctx
.session
));
246 assert
.strictEqual(res
.statusCode
, 200);
247 assert
.strictEqual(ctx
.session
.error
, undefined);
248 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 0);
250 it('covers invalid redirect', async
function () {
251 manager
.db
.profilesScopesByIdentifier
.resolves({
253 'https://profile.example.com/': {
256 profiles: ['https://profile.example.com'],
271 profiles: ['https://profile.example.com/'],
274 profiles: ['https://profile.example.com/'],
276 manager
.communication
.fetchClientIdentifier
.resolves({
279 ctx
.authenticationId
= 'username';
280 Object
.assign(ctx
.queryParams
, {
281 'client_id': 'https://client.example.com/',
282 'redirect_uri': 'https://client.example.com/action',
283 'response_type': 'blargl',
285 'code_challenge_method': 'S256',
286 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
288 await manager
.getAuthorization(res
, ctx
);
289 assert
.strictEqual(res
.statusCode
, 302);
290 assert
.strictEqual(ctx
.session
.error
, 'invalid_request');
291 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 2);
293 it('covers legacy non-PKCE missing fields', async
function () {
294 manager
.db
.profilesScopesByIdentifier
.resolves({
296 'https://profile.example.com/': {
299 profiles: ['https://profile.example.com'],
314 profiles: ['https://profile.example.com/'],
317 profiles: ['https://profile.example.com/'],
319 manager
.communication
.fetchClientIdentifier
.resolves({
322 ctx
.authenticationId
= 'username';
323 Object
.assign(ctx
.queryParams
, {
324 'client_id': 'https://client.example.com/',
325 'redirect_uri': 'https://client.example.com/action',
326 'response_type': 'code',
328 'scope': 'profile email',
329 'me': 'https://profile.example.com/',
331 manager
.options
.manager
.allowLegacyNonPKCE
= true;
333 await manager
.getAuthorization(res
, ctx
);
334 assert
.strictEqual(res
.statusCode
, 200);
335 assert
.strictEqual(ctx
.session
.error
, undefined);
336 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 0);
338 it('rejects legacy non-PKCE not missing all fields', async
function () {
339 manager
.db
.profilesScopesByIdentifier
.resolves({
341 'https://profile.example.com/': {
344 profiles: ['https://profile.example.com'],
359 profiles: ['https://profile.example.com/'],
362 profiles: ['https://profile.example.com/'],
364 manager
.communication
.fetchClientIdentifier
.resolves({
367 ctx
.authenticationId
= 'username';
368 Object
.assign(ctx
.queryParams
, {
369 'client_id': 'https://client.example.com/',
370 'redirect_uri': 'https://client.example.com/action',
371 'response_type': 'code',
372 'code_challenge_method': 'S256',
374 'scope': 'profile email',
375 'me': 'https://profile.example.com/',
377 manager
.options
.manager
.allowLegacyNonPKCE
= true;
379 await manager
.getAuthorization(res
, ctx
);
380 assert
.strictEqual(res
.statusCode
, 302);
381 assert
.strictEqual(ctx
.session
.error
, 'invalid_request');
382 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 1);
384 it('rejects legacy non-PKCE not missing all fields', async
function () {
385 manager
.db
.profilesScopesByIdentifier
.resolves({
387 'https://profile.example.com/': {
390 profiles: ['https://profile.example.com'],
405 profiles: ['https://profile.example.com/'],
408 profiles: ['https://profile.example.com/'],
410 manager
.communication
.fetchClientIdentifier
.resolves({
413 ctx
.authenticationId
= 'username';
414 Object
.assign(ctx
.queryParams
, {
415 'client_id': 'https://client.example.com/',
416 'redirect_uri': 'https://client.example.com/action',
417 'response_type': 'code',
418 'code_challenge': 'xxx',
420 'scope': 'profile email',
421 'me': 'https://profile.example.com/',
423 manager
.options
.manager
.allowLegacyNonPKCE
= true;
425 await manager
.getAuthorization(res
, ctx
);
426 assert
.strictEqual(res
.statusCode
, 302);
427 assert
.strictEqual(ctx
.session
.error
, 'invalid_request');
428 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 1);
430 }); // getAuthorization
432 describe('_setError', function () {
433 it('covers', function () {
434 const err
= 'invalid_request';
435 const errDesc
= 'something went wrong';
436 Manager
._setError(ctx
, err
, errDesc
);
438 it('covers bad error', function () {
439 const err
= 'floopy';
440 const errDesc
= 'something went wrong';
442 Manager
._setError(ctx
, err
, errDesc
);
443 assert
.fail(noExpectedException
);
445 assert(e
instanceof RangeError
);
448 it('covers invalid error description', function () {
449 const err
= 'invalid_scope';
450 const errDesc
= 'something "went wrong"!';
452 Manager
._setError(ctx
, err
, errDesc
);
453 assert
.fail(noExpectedException
);
455 assert(e
instanceof RangeError
);
460 describe('_clientIdRequired', function () {
461 let clientIdentifier
;
462 beforeEach(function () {
466 manager
.communication
.fetchClientIdentifier
.resolves(clientIdentifier
);
468 it('covers valid', async
function () {
469 ctx
.queryParams
['client_id'] = 'https://client.example.com/';
471 await manager
._clientIdRequired(ctx
);
473 assert
.deepStrictEqual(ctx
.session
.clientIdentifier
, clientIdentifier
);
474 assert
.strictEqual(ctx
.session
.error
, undefined);
475 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
477 it('requires client_id', async
function () {
478 ctx
.queryParams
['client_id'] = undefined;
480 await manager
._clientIdRequired(ctx
);
482 assert(ctx
.session
.error
);
483 assert(ctx
.session
.errorDescriptions
.length
);
485 it('requires valid client_id', async
function () {
486 ctx
.queryParams
['client_id'] = 'not a url';
488 await manager
._clientIdRequired(ctx
);
490 assert(ctx
.session
.error
);
491 assert(ctx
.session
.errorDescriptions
.length
);
493 it('rejects strange schema', async
function () {
494 ctx
.queryParams
['client_id'] = 'file:///etc/shadow';
496 await manager
._clientIdRequired(ctx
);
498 assert(ctx
.session
.error
);
499 assert(ctx
.session
.errorDescriptions
.length
);
501 it('rejects un-allowed parts', async
function () {
502 ctx
.queryParams
['client_id'] = 'https://user:pass@client.example.com/#here';
504 await manager
._clientIdRequired(ctx
);
506 assert(ctx
.session
.error
);
507 assert(ctx
.session
.errorDescriptions
.length
);
509 it('rejects relative paths', async
function () {
510 ctx
.queryParams
['client_id'] = 'https://client.example.com/x/../y/';
512 await manager
._clientIdRequired(ctx
);
514 assert(ctx
.session
.error
);
515 assert(ctx
.session
.errorDescriptions
.length
);
517 it('rejects ipv6 hostname', async
function () {
518 ctx
.queryParams
['client_id'] = 'https://[fd12:3456:789a:1::1]/';
520 await manager
._clientIdRequired(ctx
);
522 assert(ctx
.session
.error
);
523 assert(ctx
.session
.errorDescriptions
.length
);
525 it('allows ipv6 loopback hostname', async
function () {
526 ctx
.queryParams
['client_id'] = 'https://[::1]/';
528 await manager
._clientIdRequired(ctx
);
530 assert
.deepStrictEqual(ctx
.session
.clientIdentifier
, clientIdentifier
);
531 assert
.strictEqual(ctx
.session
.error
, undefined);
532 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
534 it('rejects ipv4 hostname', async
function () {
535 ctx
.queryParams
['client_id'] = 'https://10.9.8.7/';
537 await manager
._clientIdRequired(ctx
);
539 assert(ctx
.session
.error
);
540 assert(ctx
.session
.errorDescriptions
.length
);
542 it('allows ipv4 loopback hostname', async
function () {
543 ctx
.queryParams
['client_id'] = 'https:/127.0.10.100/';
545 await manager
._clientIdRequired(ctx
);
547 assert
.deepStrictEqual(ctx
.session
.clientIdentifier
, clientIdentifier
);
548 assert
.strictEqual(ctx
.session
.error
, undefined);
549 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
551 it('requires response', async
function () {
552 manager
.communication
.fetchClientIdentifier
.restore();
553 sinon
.stub(manager
.communication
, 'fetchClientIdentifier').resolves();
554 ctx
.queryParams
['client_id'] = 'https://client.example.com/';
556 await manager
._clientIdRequired(ctx
);
558 assert(ctx
.session
.error
);
559 assert(ctx
.session
.errorDescriptions
.length
);
561 }); // _clientIdRequired
563 describe('_redirectURIRequired', function () {
564 beforeEach(function () {
565 ctx
.session
.clientId
= new URL('https://client.example.com/');
566 ctx
.session
.clientIdentifier
= {
568 'redirect_uri': ['https://alternate.example.com/', 'https://other.example.com/'],
572 it('covers valid', function () {
573 ctx
.queryParams
['redirect_uri'] = 'https://client.example.com/return';
575 Manager
._redirectURIRequired(ctx
);
577 assert
.strictEqual(ctx
.session
.error
, undefined);
578 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
580 it('requires redirect_uri', function () {
581 ctx
.queryParams
['redirect_uri'] = undefined;
583 Manager
._redirectURIRequired(ctx
);
585 assert(ctx
.session
.error
);
586 assert(ctx
.session
.errorDescriptions
.length
);
588 it('requires valid redirect_uri', function () {
589 ctx
.queryParams
['redirect_uri'] = 'not a url';
591 Manager
._redirectURIRequired(ctx
);
593 assert(ctx
.session
.error
);
594 assert(ctx
.session
.errorDescriptions
.length
);
596 it('rejects no matching alternate redirect_uri from client_id', function () {
597 ctx
.queryParams
['redirect_uri'] = 'https://unlisted.example.com/';
599 Manager
._redirectURIRequired(ctx
);
601 assert(ctx
.session
.error
);
602 assert(ctx
.session
.errorDescriptions
.length
);
604 it('allows alternate redirect_uri from client_id', function () {
605 ctx
.queryParams
['redirect_uri'] = 'https://alternate.example.com/';
607 Manager
._redirectURIRequired(ctx
);
609 assert
.strictEqual(ctx
.session
.error
, undefined);
610 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
612 }); // _redirectURIRequired
614 describe('_responseTypeRequired', function () {
615 it('covers valid', function () {
616 ctx
.queryParams
['response_type'] = 'code';
618 Manager
._responseTypeRequired(ctx
);
620 assert
.strictEqual(ctx
.session
.error
, undefined);
621 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
623 it('requires response_type', function () {
624 ctx
.queryParams
['response_type'] = undefined;
626 Manager
._responseTypeRequired(ctx
);
628 assert(ctx
.session
.error
);
629 assert(ctx
.session
.errorDescriptions
.length
);
631 it('rejects invalid', function () {
632 ctx
.queryParams
['response_type'] = 'flarp';
634 Manager
._responseTypeRequired(ctx
);
636 assert(ctx
.session
.error
);
637 assert(ctx
.session
.errorDescriptions
.length
);
639 }); // _responseTypeRequired
641 describe('_stateRequired', function () {
642 it('covers valid', function () {
643 ctx
.queryParams
['state'] = 'StateStateState';
645 Manager
._stateRequired(ctx
);
647 assert
.strictEqual(ctx
.session
.error
, undefined);
648 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
650 it('requires state', function () {
651 ctx
.queryParams
['state'] = undefined;
653 Manager
._stateRequired(ctx
);
655 assert(ctx
.session
.error
);
656 assert(ctx
.session
.errorDescriptions
.length
);
658 }); // _stateRequired
660 describe('_codeChallengeMethodRequired', function () {
661 it('covers valid', function () {
662 ctx
.queryParams
['code_challenge_method'] = 'S256';
664 manager
._codeChallengeMethodRequired(ctx
);
666 assert
.strictEqual(ctx
.session
.error
, undefined);
667 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
669 it('requires code_challenge_method', function () {
670 ctx
.queryParams
['code_challenge_method'] = undefined;
672 manager
._codeChallengeMethodRequired(ctx
);
674 assert(ctx
.session
.error
);
675 assert(ctx
.session
.errorDescriptions
.length
);
677 it('rejects invalid', function () {
678 ctx
.queryParams
['code_challenge_method'] = 'MD5';
680 manager
._codeChallengeMethodRequired(ctx
);
682 assert(ctx
.session
.error
);
683 assert(ctx
.session
.errorDescriptions
.length
);
685 it('covers legacy non-PKCE', function () {
686 ctx
.queryParams
['code_challenge_method'] = undefined;
687 manager
.options
.manager
.allowLegacyNonPKCE
= true;
689 manager
._codeChallengeMethodRequired(ctx
);
691 assert
.strictEqual(ctx
.session
.error
, undefined);
692 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
694 }); // _codeChallengeMethodRequired
696 describe('_codeChallengeRequired', function () {
697 it('covers valid', function () {
698 ctx
.queryParams
['code_challenge'] = 'NBKNqs1TfjQFqpewPNOstmQ5MJnLoeTTbjqtQ9JbZOo';
700 manager
._codeChallengeRequired(ctx
);
702 assert
.strictEqual(ctx
.session
.error
, undefined);
703 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
705 it('requires code_challenge', function () {
706 ctx
.queryParams
['code_challenge'] = undefined;
708 manager
._codeChallengeRequired(ctx
);
710 assert(ctx
.session
.error
);
711 assert(ctx
.session
.errorDescriptions
.length
);
713 it('rejects invalid', function () {
714 ctx
.queryParams
['code_challenge'] = 'not base64/url encoded';
716 manager
._codeChallengeRequired(ctx
);
718 assert(ctx
.session
.error
);
719 assert(ctx
.session
.errorDescriptions
.length
);
721 it('covers legacy non-PKCE', function () {
722 ctx
.queryParams
['code_challenge'] = undefined;
723 manager
.options
.manager
.allowLegacyNonPKCE
= true;
725 manager
._codeChallengeRequired(ctx
);
727 assert
.strictEqual(ctx
.session
.error
, undefined);
728 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
731 }); // _codeChallengeRequired
733 describe('_redirectURIRequired', function () {
734 beforeEach(function () {
735 sinon
.stub(Manager
, '_setError');
736 ctx
.queryParams
['redirect_uri'] = 'https://example.com/redirect';
737 ctx
.session
.clientId
= new URL('https://example.com/');
739 it('requires redirect_uri', function () {
740 delete ctx
.queryParams
['redirect_uri'];
741 Manager
._redirectURIRequired(ctx
);
742 assert(Manager
._setError
.called
);
744 it('requires valid redirect_uri', function () {
745 ctx
.queryParams
['redirect_uri'] = 'not a uri';
746 Manager
._redirectURIRequired(ctx
);
747 assert(Manager
._setError
.called
);
749 it('sets redirectUri if no clientId', function () {
750 delete ctx
.session
.clientId
;
751 Manager
._redirectURIRequired(ctx
);
752 assert(Manager
._setError
.notCalled
);
753 assert(ctx
.session
.redirectUri
instanceof URL
);
755 it('sets redirectUri if clientId matches', function () {
756 Manager
._redirectURIRequired(ctx
);
757 assert(Manager
._setError
.notCalled
);
758 assert(ctx
.session
.redirectUri
instanceof URL
);
760 it('rejects mis-matched', function () {
761 ctx
.queryParams
['redirect_uri'] = 'https://example.com:8080/redirect';
762 Manager
._redirectURIRequired(ctx
);
763 assert(Manager
._setError
.called
);
764 assert
.strictEqual(ctx
.session
.redirectUri
, undefined);
766 it('allows client-specified alternate redirect uri', function () {
767 ctx
.session
.clientIdentifier
= {
769 'redirect_uri': ['https://alternate.example.com/redirect'],
772 ctx
.queryParams
['redirect_uri'] = 'https://alternate.example.com/redirect';
773 Manager
._redirectURIRequired(ctx
);
774 assert(Manager
._setError
.notCalled
);
775 assert(ctx
.session
.redirectUri
instanceof URL
);
777 }); // _redirectURIRequired
779 describe('_scopeOptional', function () {
780 it('covers valid', function () {
781 ctx
.queryParams
['scope'] = 'profile email';
782 manager
._scopeOptional(ctx
);
783 assert
.strictEqual(ctx
.session
.error
, undefined);
784 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
785 assert
.strictEqual(ctx
.session
.scope
.length
, 2);
787 it('allows empty', function () {
788 ctx
.queryParams
['scope'] = undefined;
789 manager
._scopeOptional(ctx
);
790 assert
.strictEqual(ctx
.session
.error
, undefined);
791 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
792 assert
.strictEqual(ctx
.session
.scope
.length
, 0);
794 it('rejects invalid scope combination', function () {
795 ctx
.queryParams
['scope'] = 'email';
796 manager
._scopeOptional(ctx
);
797 assert(ctx
.session
.error
);
798 assert(ctx
.session
.errorDescriptions
.length
);
800 it('ignores invalid scope', function () {
801 ctx
.queryParams
['scope'] = 'profile email "funny_business"';
802 manager
._scopeOptional(ctx
);
803 assert
.strictEqual(ctx
.session
.error
, undefined);
804 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
805 assert
.strictEqual(ctx
.session
.scope
.length
, 2);
807 }); // _scopeOptional
809 describe('_meOptional', function () {
810 this.beforeEach(function () {
811 ctx
.queryParams
['me'] = 'https://profile.example.com/';
813 it('covers valid', async
function () {
814 await manager
._meOptional(ctx
);
816 assert
.strictEqual(ctx
.session
.me
.href
, ctx
.queryParams
['me']);
818 it('ignore invalid', async
function () {
819 ctx
.queryParams
['me'] = 'not a url';
821 await manager
._meOptional(ctx
);
823 assert
.strictEqual(ctx
.session
.me
, undefined);
825 it('allows empty', async
function () {
826 ctx
.queryParams
['me'] = undefined;
828 await manager
._meOptional(ctx
);
830 assert
.strictEqual(ctx
.session
.me
, undefined);
834 describe('_profileValidForIdentifier', function () {
835 beforeEach(function () {
837 profiles: ['https://profile.example.com/', 'https://example.com/profile'],
838 me: new URL('https://example.com/profile'),
841 it('covers valid', async
function () {
843 const result
= await manager
._profileValidForIdentifier(ctx
);
845 assert
.strictEqual(result
, true);
847 it('covers missing me', async
function () {
848 delete ctx
.session
.me
;
850 const result
= await manager
._profileValidForIdentifier(ctx
);
852 assert
.strictEqual(result
, false);
854 }); // _profileValidForIdentifier
856 describe('_parseLifespan', function () {
857 let field
, customField
;
858 beforeEach(function () {
860 customField
= 'lifespan-seconds';
861 ctx
.parsedBody
['lifespan'] = undefined;
862 ctx
.parsedBody
['lifespan-seconds'] = undefined;
864 it('returns nothing without fields', function () {
865 const result
= manager
._parseLifespan(ctx
, field
, customField
);
866 assert
.strictEqual(result
, undefined);
868 it('returns nothing for unrecognized field', function () {
869 ctx
.parsedBody
['lifespan'] = 'a while';
870 const result
= manager
._parseLifespan(ctx
, field
, customField
);
871 assert
.strictEqual(result
, undefined);
873 it('returns recognized preset value', function () {
874 ctx
.parsedBody
['lifespan'] = '1d';
875 const result
= manager
._parseLifespan(ctx
, field
, customField
);
876 assert
.strictEqual(result
, 86400);
878 it('returns valid custom value', function () {
879 ctx
.parsedBody
['lifespan'] = 'custom';
880 ctx
.parsedBody
['lifespan-seconds'] = '123';
881 const result
= manager
._parseLifespan(ctx
, field
, customField
);
882 assert
.strictEqual(result
, 123);
884 it('returns nothing for invalid custom value', function () {
885 ctx
.parsedBody
['lifespan'] = 'custom';
886 ctx
.parsedBody
['lifespan-seconds'] = 'Not a number';
887 const result
= manager
._parseLifespan(ctx
, field
, customField
);
888 assert
.strictEqual(result
, undefined);
890 it('returns nothing for invalid custom value', function () {
891 ctx
.parsedBody
['lifespan'] = 'custom';
892 ctx
.parsedBody
['lifespan-seconds'] = '-50';
893 const result
= manager
._parseLifespan(ctx
, field
, customField
);
894 assert
.strictEqual(result
, undefined);
896 }); // _parseLifespan
898 describe('_parseConsentScopes', function () {
899 it('covers no scopes', function () {
900 const result
= manager
._parseConsentScopes(ctx
);
901 assert
.deepStrictEqual(result
, []);
903 it('filters invalid scopes', function () {
904 ctx
.parsedBody
['accepted_scopes[]'] = ['read', 'email'];
905 ctx
.parsedBody
['ad_hoc_scopes'] = 'bad"scope create ';
906 const result
= manager
._parseConsentScopes(ctx
);
907 assert
.deepStrictEqual(result
, ['read', 'create']);
909 }); // _parseConsentScopes
911 describe('_parseConsentMe', function () {
912 beforeEach(function () {
913 ctx
.session
.profiles
= ['https://me.example.com/'];
915 it('covers valid', function () {
916 const expected
= 'https://me.example.com/';
917 ctx
.parsedBody
['me'] = expected
;
918 const result
= manager
._parseConsentMe(ctx
);
920 assert
.strictEqual(result
.href
, expected
);
922 it('rejects unsupported', function () {
923 ctx
.parsedBody
['me'] = 'https://notme.example.com/';
924 const result
= manager
._parseConsentMe(ctx
);
926 assert(ctx
.session
.error
);
928 it('rejects invalid', function () {
929 ctx
.parsedBody
['me'] = 'bagel';
930 const result
= manager
._parseConsentMe(ctx
);
932 assert(ctx
.session
.error
);
934 }); // _parseConsentMe
936 describe('_fetchConsentProfileData', function () {
938 beforeEach(function () {
940 url: 'https://profile.example.com/',
942 manager
.communication
.fetchProfile
.resolves(profileResponse
);
944 it('covers success', async
function () {
945 const expected
= profileResponse
;
946 const result
= await manager
._fetchConsentProfileData(ctx
);
947 assert
.deepStrictEqual(result
, expected
);
948 assert(!ctx
.session
.error
);
950 it('covers empty response', async
function () {
951 manager
.communication
.fetchProfile
.resolves();
952 const result
= await manager
._fetchConsentProfileData(ctx
);
953 assert
.deepStrictEqual(result
, undefined);
954 assert(ctx
.session
.error
);
956 it('covers failure', async
function () {
957 manager
.communication
.fetchProfile
.rejects();
958 const result
= await manager
._fetchConsentProfileData(ctx
);
959 assert
.deepStrictEqual(result
, undefined);
960 assert(ctx
.session
.error
);
962 }); // _fetchConsentProfileData
964 describe('postConsent', function () {
966 beforeEach(function () {
967 sinon
.stub(manager
.mysteryBox
, 'unpack');
968 sinon
.stub(manager
.mysteryBox
, 'pack');
969 manager
.communication
.fetchProfile
.resolves({
970 url: 'https://profile.example.com/',
973 clientId: 'https://example.com/',
974 redirectUri: 'https://example.com/_redirect',
975 profiles: ['https://profile.example.com/'],
977 manager
.mysteryBox
.unpack
.resolves(oldSession
);
978 ctx
.parsedBody
['me'] = 'https://profile.example.com/';
979 ctx
.parsedBody
['accept'] = 'true';
981 it('covers valid', async
function () {
982 await manager
.postConsent(res
, ctx
);
983 assert(!ctx
.session
.error
, ctx
.session
.error
);
984 assert
.strictEqual(res
.statusCode
, 302);
986 it('covers valid with expiration and refresh', async
function () {
987 ctx
.parsedBody
['expires'] = '1d';
988 ctx
.parsedBody
['refresh'] = '1w';
989 await manager
.postConsent(res
, ctx
);
990 assert(!ctx
.session
.error
, ctx
.session
.error
);
991 assert
.strictEqual(res
.statusCode
, 302);
993 it('covers denial', async
function () {
994 ctx
.parsedBody
['accept'] = 'false';
995 await manager
.postConsent(res
, ctx
);
996 assert(ctx
.session
.error
);
997 assert
.strictEqual(ctx
.session
.error
, 'access_denied');
998 assert
.strictEqual(res
.statusCode
, 302);
1000 it('covers profile fetch failure', async
function () {
1001 manager
.communication
.fetchProfile
.resolves();
1002 await manager
.postConsent(res
, ctx
);
1003 assert
.strictEqual(res
.statusCode
, 302);
1004 assert(ctx
.session
.error
);
1006 it('covers bad code', async
function () {
1007 manager
.mysteryBox
.unpack
.rejects();
1008 await manager
.postConsent(res
, ctx
);
1009 assert
.strictEqual(res
.statusCode
, 400);
1010 assert(ctx
.session
.error
);
1012 it('removes email scope without profile', async
function () {
1013 ctx
.parsedBody
['accepted_scopes[]'] = ['email', 'create'];
1014 await manager
.postConsent(res
, ctx
);
1015 assert(!ctx
.session
.acceptedScopes
.includes('email'));
1017 it('merges valid ad-hoc scopes', async
function () {
1018 ctx
.parsedBody
['accepted_scopes[]'] = ['email', 'create'];
1019 ctx
.parsedBody
['ad_hoc_scopes'] = ' my:scope "badScope';
1020 await manager
.postConsent(res
, ctx
);
1021 assert(ctx
.session
.acceptedScopes
.includes('my:scope'));
1023 it('covers invalid selected me profile', async
function () {
1024 ctx
.parsedBody
['me'] = 'https://different.example.com/';
1025 await manager
.postConsent(res
, ctx
);
1026 assert(ctx
.session
.error
);
1028 it('covers invalid me url', async
function () {
1029 ctx
.parsedBody
['me'] = 'bagel';
1030 await manager
.postConsent(res
, ctx
);
1031 assert(ctx
.session
.error
);
1033 it('covers profile fetch error', async
function () {
1034 manager
.communication
.fetchProfile
.rejects(expectedException
);
1035 await manager
.postConsent(res
, ctx
);
1036 assert
.strictEqual(res
.statusCode
, 302);
1037 assert(ctx
.session
.error
);
1041 describe('postAuthorization', function () {
1042 let code
, parsedBody
;
1043 beforeEach(function () {
1044 sinon
.stub(manager
.mysteryBox
, 'unpack');
1046 codeId: 'cffe1558-35f0-11ec-98bc-0025905f714a',
1047 codeChallengeMethod: 'S256',
1048 codeChallenge: 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ',
1049 clientId: 'https://app.example.com/',
1050 redirectUri: 'https://app.example.com/_redirect',
1051 acceptedScopes: ['profile'],
1053 me: 'https://client.example.com/',
1054 identifier: 'username',
1056 name: 'Firsty McLastname',
1057 email: 'f.mclastname@example.com',
1061 code: 'codeCodeCode',
1062 'client_id': 'https://app.example.com/',
1063 'redirect_uri': 'https://app.example.com/_redirect',
1064 'grant_type': 'authorization_code',
1065 'code_verifier': 'verifier',
1068 it('covers valid', async
function () {
1069 manager
.db
.redeemCode
.resolves(true);
1070 manager
.mysteryBox
.unpack
.resolves(code
);
1071 Object
.assign(ctx
.parsedBody
, parsedBody
);
1073 await manager
.postAuthorization(res
, ctx
);
1074 assert(!ctx
.session
.error
, ctx
.session
.error
);
1075 assert(!res
.end
.firstCall
.args
[0].includes('email'));
1077 it('includes email if accepted in scope', async
function () {
1078 code
.acceptedScopes
= ['profile', 'email'];
1079 manager
.db
.redeemCode
.resolves(true);
1080 manager
.mysteryBox
.unpack
.resolves(code
);
1081 Object
.assign(ctx
.parsedBody
, parsedBody
);
1083 await manager
.postAuthorization(res
, ctx
);
1084 assert(!ctx
.session
.error
);
1085 assert(res
.end
.firstCall
.args
[0].includes('email'));
1087 it('fails if already redeemed', async
function () {
1088 manager
.db
.redeemCode
.resolves(false);
1089 manager
.mysteryBox
.unpack
.resolves(code
);
1090 Object
.assign(ctx
.parsedBody
, parsedBody
);
1092 await manager
.postAuthorization(res
, ctx
);
1093 assert(ctx
.session
.error
);
1095 it('covers bad request', async
function () {
1096 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1097 Object
.assign(ctx
.parsedBody
, parsedBody
);
1099 await manager
.postAuthorization(res
, ctx
);
1100 assert(ctx
.session
.error
);
1102 }); // postAuthorization
1104 describe('_ingestPostAuthorizationRequest', function () {
1105 beforeEach(function () {
1106 sinon
.stub(manager
, '_restoreSessionFromCode');
1107 sinon
.stub(manager
, '_checkSessionMatchingClientId');
1108 sinon
.stub(manager
, '_checkSessionMatchingRedirectUri');
1109 sinon
.stub(manager
, '_checkGrantType');
1110 sinon
.stub(manager
, '_checkSessionMatchingCodeVerifier');
1112 it('covers valid', async
function () {
1113 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1115 me: 'https://profile.example.com/',
1120 await manager
._ingestPostAuthorizationRequest(ctx
);
1121 assert(!ctx
.session
.error
);
1123 it('requires data', async
function () {
1124 delete ctx
.parsedBody
;
1125 await manager
._ingestPostAuthorizationRequest(ctx
);
1126 assert(ctx
.session
.error
);
1128 it('requires me field', async
function () {
1129 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1134 await manager
._ingestPostAuthorizationRequest(ctx
);
1135 assert(ctx
.session
.error
);
1137 it('requires minted field', async
function () {
1138 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1140 me: 'https://profile.example.com/',
1143 await manager
._ingestPostAuthorizationRequest(ctx
);
1144 assert(ctx
.session
.error
);
1146 it('rejects expired code', async
function () {
1147 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1149 me: 'https://profile.example.com/',
1150 minted: Date
.now() - 86400000,
1154 await manager
._ingestPostAuthorizationRequest(ctx
);
1155 assert(ctx
.session
.error
);
1157 }); // _ingestPostAuthorizationRequest
1159 describe('_restoreSessionFromCode', function () {
1161 beforeEach(function () {
1162 sinon
.stub(manager
.mysteryBox
, 'unpack');
1164 codeId: 'cffe1558-35f0-11ec-98bc-0025905f714a',
1165 codeChallengeMethod: 'S256',
1166 codeChallenge: 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ',
1167 clientId: 'https://app.example.com/',
1168 redirectUri: 'https://app.example.com/_redirect',
1169 acceptedScopes: ['profile'],
1171 me: 'https://client.example.com/',
1172 identifier: 'username',
1174 name: 'Firsty McLastname',
1175 email: 'f.mclastname@example.com',
1179 it('covers valid', async
function () {
1180 ctx
.parsedBody
['code'] = 'codeCodeCode';
1181 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1182 const expected
= Object
.assign({}, ctx
, {
1183 session: unpackedCode
,
1185 await manager
._restoreSessionFromCode(ctx
);
1186 assert
.deepStrictEqual(ctx
, expected
);
1187 assert(!ctx
.session
.error
);
1189 it('requires code', async
function () {
1190 ctx
.parsedBody
['code'] = '';
1191 manager
.mysteryBox
.unpack
.resolves({
1192 me: 'https://example.com/me',
1194 await manager
._restoreSessionFromCode(ctx
);
1195 assert(ctx
.session
.error
);
1197 it('covers invalid code', async
function () {
1198 ctx
.parsedBody
['code'] = 'codeCodeCode';
1199 manager
.mysteryBox
.unpack
.rejects();
1200 await manager
._restoreSessionFromCode(ctx
);
1201 assert(ctx
.session
.error
);
1203 it('covers missing code fields', async
function () {
1204 ctx
.parsedBody
['code'] = 'codeCodeCode';
1205 delete unpackedCode
.clientId
;
1206 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1207 await manager
._restoreSessionFromCode(ctx
);
1208 assert(ctx
.session
.error
);
1210 it('covers legacy non-PKCE missing fields', async
function () {
1211 ctx
.parsedBody
['code'] = 'codeCodeCode';
1212 delete unpackedCode
.codeChallengeMethod
;
1213 delete unpackedCode
.codeChallenge
;
1214 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1215 manager
.options
.manager
.allowLegacyNonPKCE
= true;
1216 const expected
= Object
.assign({}, ctx
, {
1217 session: unpackedCode
,
1219 await manager
._restoreSessionFromCode(ctx
);
1220 assert
.deepStrictEqual(ctx
, expected
);
1221 assert(!ctx
.session
.error
);
1223 }); // _restoreSessionFromCode
1225 describe('_checkSessionMatchingClientId', function () {
1226 it('covers valid', async
function () {
1228 clientId: 'https://client.example.com/',
1230 ctx
.parsedBody
['client_id'] = 'https://client.example.com/';
1232 manager
._checkSessionMatchingClientId(ctx
);
1233 assert(!ctx
.session
.error
);
1235 it('covers missing', async
function () {
1237 clientId: 'https://client.example.com/',
1239 ctx
.parsedBody
['client_id'] = undefined;
1241 manager
._checkSessionMatchingClientId(ctx
);
1242 assert(ctx
.session
.error
);
1244 it('covers un-parsable', async
function () {
1246 clientId: 'https://client.example.com/',
1248 ctx
.parsedBody
['client_id'] = 'not a url';
1250 manager
._checkSessionMatchingClientId(ctx
);
1251 assert(ctx
.session
.error
);
1253 it('covers mismatch', async
function () {
1255 clientId: 'https://client.example.com/',
1257 ctx
.parsedBody
['client_id'] = 'https://otherclient.example.com/';
1259 manager
._checkSessionMatchingClientId(ctx
);
1260 assert(ctx
.session
.error
);
1262 }); // _checkSessionMatchingClientId
1264 describe('_checkSessionMatchingRedirectUri', function () {
1265 it('covers valid', async
function () {
1266 ctx
.parsedBody
['redirect_uri'] = 'https://client.example.com/_redirect';
1267 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1269 manager
._checkSessionMatchingRedirectUri(ctx
);
1270 assert(!ctx
.session
.error
);
1272 it('requires field', async
function () {
1273 ctx
.parsedBody
['redirect_uri'] = undefined;
1274 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1276 manager
._checkSessionMatchingRedirectUri(ctx
);
1277 assert(ctx
.session
.error
);
1279 it('requires valid field', async
function () {
1280 ctx
.parsedBody
['redirect_uri'] = 'not a url';
1281 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1283 manager
._checkSessionMatchingRedirectUri(ctx
);
1284 assert(ctx
.session
.error
);
1286 it('requires match', async
function () {
1287 ctx
.parsedBody
['redirect_uri'] = 'https://client.example.com/other';
1288 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1290 manager
._checkSessionMatchingRedirectUri(ctx
);
1291 assert(ctx
.session
.error
);
1293 }); // _checkSessionMatchingRedirectUri
1295 describe('_checkGrantType', function () {
1296 it('covers valid', async
function () {
1297 ctx
.parsedBody
['grant_type'] = 'authorization_code';
1299 manager
._checkGrantType(ctx
);
1300 assert(!ctx
.session
.error
);
1302 it('allows missing, because of one client', async
function () {
1303 ctx
.parsedBody
['grant_type'] = undefined;
1305 manager
._checkGrantType(ctx
);
1306 assert(!ctx
.session
.error
);
1308 it('rejects invalid', async
function () {
1309 ctx
.parsedBody
['grant_type'] = 'pigeon_dance';
1311 manager
._checkGrantType(ctx
);
1312 assert(ctx
.session
.error
);
1314 }); // _checkGrantType
1316 describe('_checkSessionMatchingCodeVerifier', function () {
1317 it('covers valid', async
function () {
1318 ctx
.parsedBody
['code_verifier'] = 'verifier';
1319 ctx
.session
.codeChallengeMethod
= 'S256';
1320 ctx
.session
.codeChallenge
= 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ';
1322 manager
._checkSessionMatchingCodeVerifier(ctx
);
1323 assert(!ctx
.session
.error
);
1325 it('requires field', async
function () {
1326 ctx
.parsedBody
['code_verifier'] = undefined;
1327 ctx
.session
.codeChallengeMethod
= 'S256';
1328 ctx
.session
.codeChallenge
= 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ';
1330 manager
._checkSessionMatchingCodeVerifier(ctx
);
1331 assert(ctx
.session
.error
);
1333 it('requires match', async
function () {
1334 ctx
.parsedBody
['code_verifier'] = 'wrongverifier';
1335 ctx
.session
.codeChallengeMethod
= 'S256';
1336 ctx
.session
.codeChallenge
= 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ';
1338 manager
._checkSessionMatchingCodeVerifier(ctx
);
1339 assert(ctx
.session
.error
);
1341 it('covers legacy non-PKCE missing fields', async
function () {
1342 ctx
.parsedBody
['code_verifier'] = undefined;
1343 ctx
.session
.codeChallengeMethod
= undefined;
1344 ctx
.session
.codeChallenge
= undefined;
1345 manager
.options
.manager
.allowLegacyNonPKCE
= true;
1347 manager
._checkSessionMatchingCodeVerifier(ctx
);
1348 assert(!ctx
.session
.error
);
1350 }); // _checkSessionMatchingCodeVerifier
1352 describe('postToken', function () {
1354 beforeEach(function () {
1355 ctx
.session
.acceptedScopes
= [];
1357 codeId: 'cffe1558-35f0-11ec-98bc-0025905f714a',
1358 codeChallengeMethod: 'S256',
1359 codeChallenge: 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ',
1360 clientId: 'https://app.example.com/',
1361 redirectUri: 'https://app.example.com/return',
1362 acceptedScopes: ['profile', 'email', 'tricks'],
1364 me: 'https://client.example.com/',
1365 identifier: 'username',
1367 name: 'Firsty McLastname',
1368 email: 'f.mclastname@example.com',
1369 url: 'https://example.com/',
1373 describe('Revocation (legacy)', function () {
1374 beforeEach(function () {
1375 sinon
.stub(manager
, '_revokeToken');
1377 it('covers revocation', async
function () {
1378 manager
._revokeToken
.resolves();
1383 await manager
.postToken(req
, res
, ctx
);
1384 assert(manager
._revokeToken
.called
);
1387 describe('Validation (legacy)', function () {
1388 beforeEach(function () {
1389 sinon
.stub(manager
, '_validateToken');
1390 req
.getHeader
.returns({ Authorization: 'Bearer XXX' });
1392 it('covers validation', async
function () {
1393 ctx
.bearer
= { isValid: true };
1394 await manager
.postToken(req
, res
, ctx
);
1395 assert(manager
._validateToken
.called
);
1398 describe('Refresh', function () {
1399 beforeEach(function () {
1400 sinon
.stub(manager
, '_refreshToken');
1402 it('covers refresh', async
function () {
1403 ctx
.parsedBody
['grant_type'] = 'refresh_token';
1404 await manager
.postToken(req
, res
, ctx
);
1405 assert(manager
._refreshToken
.called
);
1408 describe('Ticket Redemption', function () {
1409 beforeEach(function () {
1410 sinon
.stub(manager
, '_ticketAuthToken');
1412 it('covers ticket', async
function () {
1413 ctx
.parsedBody
['grant_type'] = 'ticket';
1414 await manager
.postToken(req
, res
, ctx
);
1415 assert(manager
._ticketAuthToken
.called
);
1417 it('covers no ticket queue', async
function () {
1418 delete options
.queues
.amqp
.url
;
1419 manager
= new Manager(logger
, stubDb
, options
);
1420 sinon
.stub(manager
.communication
, 'fetchProfile');
1421 sinon
.stub(manager
.communication
, 'fetchClientIdentifier');
1422 sinon
.stub(manager
.communication
, 'deliverTicket');
1424 ctx
.parsedBody
['grant_type'] = 'ticket';
1425 await assert
.rejects(() => manager
.postToken(req
, res
, ctx
), ResponseError
);
1427 }); // Ticket Redemption
1428 describe('Code Redemption', function () {
1429 beforeEach(function () {
1430 sinon
.stub(manager
.mysteryBox
, 'unpack');
1431 sinon
.spy(manager
.mysteryBox
, 'pack');
1432 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1434 'redirect_uri': 'https://app.example.com/return',
1438 it('covers invalid code', async
function () {
1439 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1441 await manager
.postToken(req
, res
, ctx
);
1442 assert
.fail(noExpectedException
);
1444 assert(e
instanceof ResponseError
);
1447 it('covers mismatched redirect', async
function () {
1448 ctx
.parsedBody
['redirect_uri'] = 'https://elsewhere.example.com/';
1450 await manager
.postToken(req
, res
, ctx
);
1451 assert
.fail(noExpectedException
);
1453 assert(e
instanceof ResponseError
);
1456 it('covers success', async
function () {
1457 manager
.db
.redeemCode
.resolves(true);
1458 await manager
.postToken(req
, res
, ctx
);
1459 assert(res
.end
.called
);
1460 assert
.strictEqual(manager
.mysteryBox
.pack
.callCount
, 1);
1462 it('covers success with refresh', async
function () {
1463 manager
.db
.redeemCode
.resolves(true);
1464 unpackedCode
.refreshLifespan
= 86400;
1465 unpackedCode
.tokenLifespan
= 86400;
1466 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1467 await manager
.postToken(req
, res
, ctx
);
1468 assert(res
.end
.called
);
1469 assert
.strictEqual(manager
.mysteryBox
.pack
.callCount
, 2);
1471 it('covers redemption failure', async
function () {
1472 manager
.db
.redeemCode
.resolves(false);
1474 await manager
.postToken(req
, res
, ctx
);
1475 assert
.fail(noExpectedException
);
1477 assert(e
instanceof ResponseError
);
1480 it('removes email from profile if not in scope', async
function () {
1481 manager
.db
.redeemCode
.resolves(true);
1482 unpackedCode
.acceptedScopes
= ['profile', 'tricks'];
1483 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1484 await manager
.postToken(req
, res
, ctx
);
1485 assert(res
.end
.called
);
1486 const response
= JSON
.parse(res
.end
.args
[0][0]);
1487 assert(!('email' in response
.profile
));
1490 }); // Code Redemption
1491 describe('Invalid grant_type', function () {
1492 it('throws response error', async
function () {
1493 ctx
.parsedBody
['grant_type'] = 'bad';
1495 await manager
.postToken(req
, res
, ctx
);
1496 assert
.fail(noExpectedException
);
1498 assert(e
instanceof ResponseError
);
1501 }); // Invalid grant_type
1504 describe('_validateToken', function () {
1506 beforeEach(function () {
1508 sinon
.stub(manager
, '_checkTokenValidationRequest');
1510 it('covers valid token', async
function () {
1516 await manager
._validateToken(dbCtx
, req
, res
, ctx
);
1517 assert(res
.end
.called
);
1519 it('covers invalid token', async
function () {
1523 await assert
.rejects(manager
._validateToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1525 it('covers errors', async
function () {
1529 ctx
.session
.error
= 'error';
1530 ctx
.session
.errorDescriptions
= ['error_description'];
1531 await assert
.rejects(manager
._validateToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1533 }); // _validateToken
1535 describe('_checkTokenValidationRequest', function () {
1537 beforeEach(function () {
1539 sinon
.stub(manager
.mysteryBox
, 'unpack');
1541 it('does nothing with no auth header', async
function () {
1542 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1544 it('does nothing with unknown auth header', async
function () {
1545 req
.getHeader
.returns('flarp authy woo');
1546 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1548 it('requires a valid auth token', async
function () {
1549 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1550 req
.getHeader
.returns('Bearer XXX');
1551 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1552 assert(ctx
.session
.error
);
1554 it('requires valid auth token fields', async
function () {
1555 manager
.mysteryBox
.unpack
.resolves({});
1556 req
.getHeader
.returns('Bearer XXX');
1557 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1558 assert(ctx
.session
.error
);
1560 it('covers no token', async
function () {
1561 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1562 req
.getHeader
.returns('Bearer XXX');
1563 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1564 assert(ctx
.session
.error
);
1566 it('covers db error', async
function () {
1567 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1568 manager
.db
.tokenGetByCodeId
.rejects(expectedException
);
1569 req
.getHeader
.returns('Bearer XXX');
1570 await assert
.rejects(manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
), expectedException
);
1572 it('valid token', async
function () {
1573 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1574 manager
.db
.tokenGetByCodeId
.resolves({
1576 expires: new Date(Date
.now() + 86400000),
1578 req
.getHeader
.returns('Bearer XXX');
1579 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1580 assert
.strictEqual(ctx
.bearer
.isValid
, true);
1582 it('revoked token', async
function () {
1583 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1584 manager
.db
.tokenGetByCodeId
.resolves({
1586 expires: new Date(Date
.now() + 86400000),
1588 req
.getHeader
.returns('Bearer XXX');
1589 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1590 assert
.strictEqual(ctx
.bearer
.isValid
, false);
1592 it('expired token', async
function () {
1593 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1594 manager
.db
.tokenGetByCodeId
.resolves({
1596 expires: new Date(Date
.now() - 86400000),
1598 req
.getHeader
.returns('Bearer XXX');
1599 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1600 assert
.strictEqual(ctx
.bearer
.isValid
, false);
1602 }); // _checkTokenValidationRequest
1604 describe('postIntrospection', function () {
1605 let inactiveToken
, activeToken
, dbResponse
;
1606 beforeEach(function () {
1608 profile: 'https://profile.example.com/',
1609 clientId: 'https://client.example.com/',
1610 scopes: ['scope1', 'scope2'],
1611 created: new Date(),
1614 inactiveToken
= JSON
.stringify({
1617 activeToken
= JSON
.stringify({
1619 me: dbResponse
.profile
,
1620 'client_id': dbResponse
.clientId
,
1621 scope: dbResponse
.scopes
.join(' '),
1622 iat: Math
.ceil(dbResponse
.created
.getTime() / 1000),
1624 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '7e9991dc-9cd5-11ec-85c4-0025905f714a' });
1625 manager
.db
.tokenGetByCodeId
.resolves(dbResponse
);
1627 it('covers bad token', async
function () {
1628 manager
.mysteryBox
.unpack
.rejects();
1629 await manager
.postIntrospection(res
, ctx
);
1630 assert(res
.end
.called
);
1631 assert
.strictEqual(res
.end
.args
[0][0], inactiveToken
);
1633 it('covers token not in db', async
function () {
1634 manager
.db
.tokenGetByCodeId
.resolves();
1635 await manager
.postIntrospection(res
, ctx
);
1636 assert(res
.end
.called
);
1637 assert
.strictEqual(res
.end
.args
[0][0], inactiveToken
);
1639 it('covers valid token', async
function () {
1640 await manager
.postIntrospection(res
, ctx
);
1641 assert(res
.end
.called
);
1642 assert
.strictEqual(res
.end
.args
[0][0], activeToken
);
1644 it('covers expired token', async
function () {
1645 dbResponse
.expires
= new Date((new Date()).getTime() - 86400000);
1646 await manager
.postIntrospection(res
, ctx
);
1647 assert(res
.end
.called
);
1648 assert
.strictEqual(res
.end
.args
[0][0], inactiveToken
);
1650 it('covers expiring token', async
function () {
1651 dbResponse
.expires
= new Date((new Date()).getTime() + 86400000);
1652 activeToken
= JSON
.stringify({
1654 me: dbResponse
.profile
,
1655 'client_id': dbResponse
.clientId
,
1656 scope: dbResponse
.scopes
.join(' '),
1657 iat: Math
.ceil(dbResponse
.created
.getTime() / 1000),
1658 exp: Math
.ceil(dbResponse
.expires
/ 1000),
1660 await manager
.postIntrospection(res
, ctx
);
1661 assert(res
.end
.called
);
1662 assert
.strictEqual(res
.end
.args
[0][0], activeToken
);
1664 it('covers ticket', async
function () {
1665 ctx
.parsedBody
['token_hint_type'] = 'ticket';
1666 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
1667 manager
.mysteryBox
.unpack
.resolves({
1668 c: '515172ae-5b0b-11ed-a6af-0025905f714a',
1669 iss: nowEpoch
- 86400,
1670 exp: nowEpoch
+ 86400,
1671 sub: 'https://subject.exmaple.com/',
1672 res: 'https://profile.example.com/feed',
1673 scope: ['read', 'role:private'],
1675 profile: 'https://profile.example.com/',
1677 await manager
.postIntrospection(res
, ctx
);
1678 assert(res
.end
.called
);
1680 }); // postIntrospection
1682 describe('_revokeToken', function () {
1684 beforeEach(function () {
1687 it('requires token field', async
function () {
1688 await manager
._revokeToken(dbCtx
, res
, ctx
);
1689 assert(res
.end
.called
);
1690 assert
.strictEqual(res
.statusCode
, 400);
1692 it('requires parsable token', async
function () {
1693 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ notC: 'foop' });
1694 ctx
.parsedBody
['token'] = 'invalid token';
1695 ctx
.parsedBody
['token_type_hint'] = 'access_token';
1696 await manager
._revokeToken(dbCtx
, res
, ctx
);
1697 assert(res
.end
.called
);
1698 assert
.strictEqual(res
.statusCode
, 400);
1700 it('requires parsable token', async
function () {
1701 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves();
1702 ctx
.parsedBody
['token'] = 'invalid token';
1703 ctx
.parsedBody
['token_type_hint'] = 'refresh_token';
1704 await manager
._revokeToken(dbCtx
, res
, ctx
);
1705 assert(res
.end
.called
);
1706 assert
.strictEqual(res
.statusCode
, 400);
1708 it('succeeds', async
function () {
1709 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1710 ctx
.parsedBody
['token'] = 'valid token';
1711 await manager
._revokeToken(dbCtx
, res
, ctx
);
1712 assert(manager
.db
.tokenRevokeByCodeId
.called
);
1713 assert(res
.end
.called
);
1715 it('succeeds for refresh token', async
function () {
1716 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ rc: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1717 ctx
.parsedBody
['token'] = 'valid token';
1718 await manager
._revokeToken(dbCtx
, res
, ctx
);
1719 assert(manager
.db
.tokenRefreshRevokeByCodeId
.called
);
1720 assert(res
.end
.called
);
1722 it('covers non-revokable token', async
function () {
1723 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1724 manager
.db
.tokenRevokeByCodeId
.rejects(new UnexpectedResult());
1725 ctx
.parsedBody
['token'] = 'valid token';
1726 await manager
._revokeToken(dbCtx
, res
, ctx
);
1727 assert
.strictEqual(res
.statusCode
, 404);
1729 it('covers failure', async
function () {
1730 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1731 manager
.db
.tokenRevokeByCodeId
.rejects(expectedException
);
1732 ctx
.parsedBody
['token'] = 'valid token';
1733 ctx
.parsedBody
['token_type_hint'] = 'ignores_bad_hint';
1734 await assert
.rejects(manager
._revokeToken(dbCtx
, res
, ctx
), expectedException
, noExpectedException
);
1738 describe('_scopeDifference', function () {
1739 let previousScopes
, requestedScopes
;
1740 beforeEach(function () {
1741 previousScopes
= ['a', 'b', 'c'];
1742 requestedScopes
= ['b', 'c', 'd'];
1744 it('covers', function () {
1745 const expected
= ['a'];
1746 const result
= Manager
._scopeDifference(previousScopes
, requestedScopes
);
1747 assert
.deepStrictEqual(result
, expected
);
1749 }); // _scopeDifference
1751 describe('_refreshToken', function () {
1753 beforeEach(function () {
1755 ctx
.parsedBody
['client_id'] = 'https://client.example.com/';
1756 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
1757 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
1758 rc: '03bb8ab0-1dc7-11ed-99f2-0025905f714a',
1759 ts: nowEpoch
- 86400,
1760 exp: nowEpoch
+ 86400,
1762 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('newToken');
1763 const futureDate
= new Date(Date
.now() + 86400000);
1764 manager
.db
.tokenGetByCodeId
.resolves({
1765 refreshExpires: futureDate
,
1767 clientId: 'https://client.example.com/',
1768 scopes: ['profile', 'create'],
1770 manager
.db
.refreshCode
.resolves({
1771 expires: futureDate
,
1772 refreshExpires: futureDate
,
1775 it('requires a token', async
function () {
1776 manager
.mysteryBox
.unpack
.rejects();
1777 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1779 it('requires token to have refresh field', async
function () {
1780 manager
.mysteryBox
.unpack
.resolves();
1781 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1783 it('requires token to exist in db', async
function () {
1784 manager
.db
.tokenGetByCodeId
.resolves();
1785 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1787 it('requires token be refreshable', async
function () {
1788 manager
.db
.tokenGetByCodeId
.resolves({
1789 refreshExpires: undefined,
1791 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1793 it('requires refresh of token not be expired', async
function () {
1794 manager
.db
.tokenGetByCodeId
.resolves({
1795 refreshExpires: 1000,
1797 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1799 it('requires token not to have been already refreshed', async
function () {
1800 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
1801 manager
.mysteryBox
.unpack
.resolves({
1802 rc: '03bb8ab0-1dc7-11ed-99f2-0025905f714a',
1803 ts: nowEpoch
- 864000,
1804 exp: nowEpoch
- 86400,
1806 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1808 it('requires client_id requesting refresh match', async
function () {
1809 ctx
.parsedBody
['client_id'] = 'https://wrong.example.com/';
1810 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1812 it('succeeds', async
function () {
1813 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1814 assert(res
.end
.called
);
1816 it('covers non-expiring', async
function () {
1817 manager
.db
.tokenGetByCodeId
.resolves({
1818 refreshExpires: new Date(Date
.now() + 86400000),
1820 clientId: 'https://client.example.com/',
1821 scopes: ['profile', 'create'],
1823 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1824 assert(res
.end
.called
);
1826 it('covers profile and email', async
function () {
1827 manager
.db
.tokenGetByCodeId
.resolves({
1828 refreshExpires: new Date(Date
.now() + 86400000),
1830 clientId: 'https://client.example.com/',
1831 scopes: ['profile', 'email', 'create'],
1833 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1834 assert(res
.end
.called
);
1836 it('succeeds with scope reduction', async
function () {
1837 ctx
.parsedBody
['scope'] = 'profile fancy';
1838 manager
.db
.tokenGetByCodeId
.resolves({
1839 refreshExpires: new Date(Date
.now() + 86400000),
1840 clientId: 'https://client.example.com/',
1841 scopes: ['profile', 'create'],
1843 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1844 assert(res
.end
.called
);
1846 it('covers refresh failed', async
function () {
1847 manager
.db
.refreshCode
.resolves();
1848 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1850 }); // _refreshToken
1852 describe('_mintTicket', function () {
1854 beforeEach(function () {
1857 subject: 'https://third-party.example.com/',
1858 resource: 'https://private.example.com/feed',
1860 identifier: 'account',
1861 profile: 'https://profile.example.com/',
1862 ticketLifespanSeconds: 86400,
1865 it('covers', async
function () {
1866 const expected
= 'xxx';
1867 sinon
.stub(manager
.mysteryBox
, 'pack').resolves(expected
);
1868 const result
= await manager
._mintTicket(dbCtx
, payload
);
1869 assert
.strictEqual(result
, expected
);
1873 describe('_ticketAuthToken', function () {
1874 let dbCtx
, ticketPayload
, nowEpoch
;
1875 beforeEach(function () {
1877 nowEpoch
= Math
.ceil(Date
.now() / 1000);
1879 c: '5ec17f78-5576-11ed-b444-0025905f714a',
1880 iss: nowEpoch
- 86400,
1881 exp: nowEpoch
+ 86400,
1882 sub: 'https://third-party.example.com/',
1883 res: 'https://private.example.com/feed',
1884 scope: ['read', 'flap'],
1886 profile: 'https://profile.example.com/',
1888 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves(ticketPayload
);
1889 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('ticket');
1891 it('covers invalid ticket', async
function () {
1892 manager
.mysteryBox
.unpack
.resolves();
1893 await assert
.rejects(() => manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1895 it('covers expired ticket', async
function () {
1896 manager
.mysteryBox
.unpack
.resolves({
1897 c: '5ec17f78-5576-11ed-b444-0025905f714a',
1898 iss: nowEpoch
- 172800,
1899 exp: nowEpoch
- 86400,
1900 sub: 'https://third-party.example.com/',
1901 res: 'https://private.example.com/feed',
1902 scope: ['read', 'flap'],
1904 profile: 'https://profile.example.com/',
1906 await assert
.rejects(() => manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1908 it('covers success', async
function () {
1909 manager
.db
.redeemCode
.resolves(true);
1910 await manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
);
1911 assert(res
.end
.called
);
1913 it('covers invalid redeem', async
function () {
1914 manager
.db
.redeemCode
.resolves(false);
1915 await assert
.rejects(() => manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1917 }); // _ticketAuthToken
1919 describe('postRevocation', function () {
1920 beforeEach(function () {
1921 sinon
.stub(manager
, '_revokeToken');
1923 it('covers success', async
function () {
1924 manager
._revokeToken
.resolves();
1925 await manager
.postRevocation(res
, ctx
);
1926 assert(manager
._revokeToken
.called
);
1928 it('covers failure', async
function () {
1929 manager
._revokeToken
.rejects(expectedException
);
1930 await assert
.rejects(manager
.postRevocation(res
, ctx
));
1932 }); // postRevocation
1934 describe('postUserInfo', function () {
1935 beforeEach(function () {
1936 ctx
.parsedBody
['token'] = 'XXX';
1937 sinon
.stub(manager
.mysteryBox
, 'unpack');
1939 it('requires a token', async
function () {
1940 delete ctx
.parsedBody
.token
;
1941 await manager
.postUserInfo(res
, ctx
);
1942 assert(res
.end
.called
);
1943 assert
.strictEqual(res
.statusCode
, 400);
1945 it('requires a valid token', async
function () {
1946 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1947 await manager
.postUserInfo(res
, ctx
);
1948 assert(res
.end
.called
);
1949 assert
.strictEqual(res
.statusCode
, 401);
1951 it('requires token to have profile scope', async
function () {
1952 manager
.mysteryBox
.unpack
.resolves({});
1953 manager
.db
.tokenGetByCodeId
.resolves({
1956 await manager
.postUserInfo(res
, ctx
);
1957 assert(res
.end
.called
);
1958 assert
.strictEqual(res
.statusCode
, 403);
1960 it('succeeds', async
function () {
1961 manager
.mysteryBox
.unpack
.resolves({});
1962 manager
.db
.tokenGetByCodeId
.resolves({
1963 scopes: ['profile', 'email'],
1965 url: 'https://example.com/',
1966 email: 'user@example.com',
1969 await manager
.postUserInfo(res
, ctx
);
1970 assert(res
.end
.called
);
1971 assert
.strictEqual(res
.statusCode
, 200);
1973 it('succeeds, and does not include email without scope', async
function () {
1974 manager
.mysteryBox
.unpack
.resolves({});
1975 manager
.db
.tokenGetByCodeId
.resolves({
1976 scopes: ['profile'],
1978 url: 'https://example.com/',
1979 email: 'user@example.com',
1982 await manager
.postUserInfo(res
, ctx
);
1983 assert(res
.end
.called
);
1984 assert
.strictEqual(res
.statusCode
, 200);
1985 const response
= JSON
.parse(res
.end
.args
[0][0]);
1986 assert(!('email' in response
));
1990 describe('getAdmin', function () {
1991 beforeEach(function () {
1992 manager
.db
.profilesScopesByIdentifier
.resolves({
1994 'https://profile.example.com/': {
1997 description: 'a scope',
1998 profiles: ['https://profile.example.com/'],
2005 description: 'a scope',
2006 profiles: ['https://profile.example.com/'],
2009 profiles: ['https://profile.example.com/'],
2011 manager
.db
.tokensGetByIdentifier
.resolves();
2013 it('covers', async
function () {
2014 await manager
.getAdmin(res
, ctx
);
2018 describe('postAdmin', function () {
2019 beforeEach(function () {
2020 manager
.db
.profilesScopesByIdentifier
.resolves({
2022 'https://profile.example.com/': {
2025 description: 'a scope',
2026 profiles: ['https://profile.example.com/'],
2033 description: 'a scope',
2034 profiles: ['https://profile.example.com/'],
2037 profiles: ['https://profile.example.com/'],
2039 manager
.db
.tokensGetByIdentifier
.resolves([]);
2040 manager
.db
.tokenRevokeByCodeId
.resolves();
2041 manager
.db
.profileIdentifierInsert
.resolves();
2042 manager
.db
.profileScopesSetAll
.resolves();
2043 manager
.communication
.fetchProfile
.resolves({
2045 authorizationEndpoint: manager
.selfAuthorizationEndpoint
,
2049 describe('save-scopes action', function () {
2050 beforeEach(function () {
2051 ctx
.parsedBody
['action'] = 'save-scopes';
2052 ctx
.parsedBody
['scopes-https://profile/example.com/[]'] = ['scope1', 'scope2'];
2054 it('covers saving scopes', async
function () {
2055 await manager
.postAdmin(res
, ctx
);
2056 assert(ctx
.notifications
.length
);
2057 assert(manager
.db
.profileScopesSetAll
.called
);
2059 it('covers saving scopes error', async
function () {
2060 manager
.db
.profileScopesSetAll
.rejects();
2061 await manager
.postAdmin(res
, ctx
);
2062 assert(ctx
.errors
.length
);
2064 }); // save-scopes action
2065 describe('new-profile action', function () {
2066 beforeEach(function () {
2067 ctx
.parsedBody
['action'] = 'new-profile';
2069 it('covers new profile', async
function () {
2070 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2071 await manager
.postAdmin(res
, ctx
);
2072 assert(ctx
.notifications
.length
);
2073 assert(manager
.db
.profileIdentifierInsert
.called
);
2074 assert(manager
.db
.profileScopesSetAll
.called
);
2076 it('covers invalid profile', async
function () {
2077 ctx
.parsedBody
['action'] = 'new-profile';
2078 ctx
.parsedBody
['profile'] = 'not a url';
2079 await manager
.postAdmin(res
, ctx
);
2080 assert(ctx
.errors
.length
);
2082 it('covers other validation failure', async
function () {
2083 sinon
.stub(manager
.communication
, 'validateProfile').rejects(expectedException
);
2084 ctx
.parsedBody
['action'] = 'new-profile';
2085 ctx
.parsedBody
['profile'] = 'not a url';
2086 await manager
.postAdmin(res
, ctx
);
2087 assert(ctx
.errors
.length
);
2089 it('covers mismatched profile', async
function () {
2090 ctx
.parsedBody
['action'] = 'new-profile';
2091 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2092 manager
.communication
.fetchProfile
.resolves({
2094 authorizationEndpoint: 'https://other.example.com/auth',
2097 await manager
.postAdmin(res
, ctx
);
2098 assert(ctx
.errors
.length
);
2100 it('covers new profile error', async
function () {
2101 ctx
.parsedBody
['action'] = 'new-profile';
2102 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2103 manager
.db
.profileIdentifierInsert
.rejects();
2104 await manager
.postAdmin(res
, ctx
);
2105 assert(ctx
.errors
.length
);
2107 }); // new-profile action
2108 describe('new-scope action', function () {
2109 beforeEach(function () {
2110 ctx
.parsedBody
['action'] = 'new-scope';
2112 it('covers new scope', async
function () {
2113 ctx
.parsedBody
['scope'] = 'newscope';
2114 await manager
.postAdmin(res
, ctx
);
2115 assert(ctx
.notifications
.length
);
2116 assert(manager
.db
.scopeUpsert
.called
);
2118 it('covers bad scope', async
function () {
2119 ctx
.parsedBody
['scope'] = 'bad scope';
2120 await manager
.postAdmin(res
, ctx
);
2121 assert(ctx
.errors
.length
);
2123 it('covers new scope error', async
function () {
2124 ctx
.parsedBody
['scope'] = 'newscope';
2125 manager
.db
.scopeUpsert
.rejects();
2126 await manager
.postAdmin(res
, ctx
);
2127 assert(ctx
.errors
.length
);
2129 it('covers empty scope', async
function () {
2130 delete ctx
.parsedBody
.scope
;
2131 await manager
.postAdmin(res
, ctx
);
2132 assert(!ctx
.errors
.length
);
2134 }); // new-scope action
2135 describe('delete-scope-* action', function () {
2136 beforeEach(function () {
2137 ctx
.parsedBody
['action'] = 'delete-scope-food%3Ayum';
2139 it('covers delete', async
function () {
2140 manager
.db
.scopeDelete
.resolves(true);
2141 await manager
.postAdmin(res
, ctx
);
2142 assert(ctx
.notifications
.length
);
2143 assert(manager
.db
.scopeDelete
.called
);
2145 it('covers no delete', async
function () {
2146 manager
.db
.scopeDelete
.resolves(false);
2147 await manager
.postAdmin(res
, ctx
);
2148 assert(ctx
.notifications
.length
);
2149 assert(manager
.db
.scopeDelete
.called
);
2151 it('covers delete error', async
function () {
2152 manager
.db
.scopeDelete
.rejects();
2153 await manager
.postAdmin(res
, ctx
);
2154 assert(ctx
.errors
.length
);
2155 assert(manager
.db
.scopeDelete
.called
);
2157 it('ignores empty scope', async
function () {
2158 ctx
.parsedBody
['action'] = 'delete-scope-';
2159 await manager
.postAdmin(res
, ctx
);
2160 assert(manager
.db
.scopeDelete
.notCalled
);
2161 assert(!ctx
.notifications
.length
);
2162 assert(!ctx
.errors
.length
);
2164 }); // delete-scope-* action
2165 describe('revoke-* action', function () {
2166 beforeEach(function () {
2167 ctx
.parsedBody
['action'] = 'revoke-b1591c00-9cb7-11ec-a05c-0025905f714a';
2169 it('covers revocation', async
function () {
2170 await manager
.postAdmin(res
, ctx
);
2171 assert(ctx
.notifications
.length
);
2172 assert(manager
.db
.tokenRevokeByCodeId
.called
);
2174 it('covers revocation error', async
function () {
2175 manager
.db
.tokenRevokeByCodeId
.rejects();
2176 await manager
.postAdmin(res
, ctx
);
2177 assert(ctx
.errors
.length
);
2179 it('covers no code', async
function () {
2180 ctx
.parsedBody
['action'] = 'revoke-';
2181 await manager
.postAdmin(res
, ctx
);
2182 assert(!ctx
.notifications
.length
);
2183 assert(!ctx
.errors
.length
);
2184 assert(manager
.db
.tokenRevokeByCodeId
.notCalled
);
2186 }); // revoke-* action
2187 it('covers empty action', async
function () {
2188 delete ctx
.parsedBody
.action
;
2189 await manager
.postAdmin(res
, ctx
);
2190 assert(!ctx
.errors
.length
);
2192 it('covers unknown action', async
function () {
2193 ctx
.parsedBody
['action'] = 'unsupported-action';
2194 await manager
.postAdmin(res
, ctx
);
2195 assert(ctx
.errors
.length
);
2199 describe('getAdminTicket', function () {
2200 it('covers', async
function () {
2201 manager
.db
.profilesScopesByIdentifier
.resolves({ scopeIndex: {} });
2202 await manager
.getAdminTicket(res
, ctx
);
2203 assert(res
.end
.called
);
2205 }); // getAdminTicket
2207 describe('postAdminTicket', function () {
2208 beforeEach(function () {
2209 ctx
.parsedBody
['action'] = 'proffer-ticket';
2210 ctx
.parsedBody
['scopes[]'] = ['read', 'role:private'];
2211 ctx
.parsedBody
['adhoc'] = 'adhoc_scope';
2212 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2213 ctx
.parsedBody
['resource'] = 'https://profile.example.com/feed';
2214 ctx
.parsedBody
['subject'] = 'https://subject.example.com/';
2215 manager
.db
.profilesScopesByIdentifier
.resolves({ scopeIndex: {} });
2216 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('ticket');
2217 manager
.communication
.fetchProfile
.resolves({
2219 ticketEndpoint: 'https://example.com/ticket',
2223 it('covers success', async
function () {
2224 await manager
.postAdminTicket(res
, ctx
);
2225 assert(res
.end
.called
);
2226 assert
.strictEqual(ctx
.errors
.length
, 0);
2227 assert
.strictEqual(ctx
.notifications
.length
, 1);
2229 it('requires params', async
function () {
2230 delete ctx
.parsedBody
['adhoc'];
2231 ctx
.parsedBody
['profile'] = 'bad url';
2232 ctx
.parsedBody
['resource'] = 'bad url';
2233 ctx
.parsedBody
['subject'] = 'bad url';
2234 ctx
.parsedBody
['scopes[]'] = ['fl"hrgl', 'email'];
2235 await manager
.postAdminTicket(res
, ctx
);
2236 assert(res
.end
.called
);
2237 assert
.strictEqual(ctx
.errors
.length
, 5);
2238 assert
.strictEqual(ctx
.notifications
.length
, 0);
2240 it('ignores unknown action', async
function () {
2241 ctx
.parsedBody
['action'] = 'prove-dough';
2242 await manager
.postAdminTicket(res
, ctx
);
2243 assert(res
.end
.called
);
2245 it('covers delivery failure', async
function () {
2246 manager
.communication
.deliverTicket
.rejects(expectedException
);
2247 await manager
.postAdminTicket(res
, ctx
);
2248 assert(res
.end
.called
);
2249 assert
.strictEqual(ctx
.errors
.length
, 1);
2250 assert
.strictEqual(ctx
.notifications
.length
, 0);
2252 it('covers no ticket endpoint', async
function () {
2253 manager
.communication
.fetchProfile
.resolves({
2257 await manager
.postAdminTicket(res
, ctx
);
2258 assert(res
.end
.called
);
2259 assert
.strictEqual(ctx
.errors
.length
, 1);
2260 assert
.strictEqual(ctx
.notifications
.length
, 0);
2262 it('covers bad ticket endpoint', async
function () {
2263 manager
.communication
.fetchProfile
.resolves({
2265 ticketEndpoint: 'not a url',
2268 await manager
.postAdminTicket(res
, ctx
);
2269 assert(res
.end
.called
);
2270 assert
.strictEqual(ctx
.errors
.length
, 1);
2271 assert
.strictEqual(ctx
.notifications
.length
, 0);
2273 }); // postAdminTicket
2275 describe('postTicket', function () {
2276 beforeEach(function () {
2278 ticket: 'ticket123',
2279 resource: 'https://blog.example.com/',
2280 subject: 'https://otheruser.example.com/',
2281 iss: 'https://ia.example.com/',
2284 it('accepts a ticket for a known profile', async
function () {
2285 manager
.db
.profileIsValid
.resolves(true);
2286 await manager
.postTicket(req
, res
, ctx
);
2287 assert(res
.end
.called
);
2288 assert
.strictEqual(res
.statusCode
, 202);
2290 it('rejects invalid resource', async
function () {
2291 ctx
.parsedBody
.resource
= 'invalid url';
2292 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), ResponseError
);
2294 it('rejects invalid subject', async
function () {
2295 manager
.db
.profileIsValid(false);
2296 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), ResponseError
);
2298 it('covers queue publish failure', async
function () {
2299 manager
.db
.profileIsValid
.resolves(true);
2300 manager
.queuePublisher
.publish
.rejects(expectedException
);
2301 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), expectedException
);
2303 it('covers no ticket queue', async
function () {
2304 delete options
.queues
.amqp
.url
;
2305 manager
= new Manager(logger
, stubDb
, options
);
2306 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), ResponseError
);
2308 it('covers no issuer', async
function () {
2309 delete ctx
.parsedBody
.iss
;
2310 manager
.db
.profileIsValid
.resolves(true);
2311 await manager
.postTicket(req
, res
, ctx
);
2312 assert(res
.end
.called
);
2313 assert
.strictEqual(res
.statusCode
, 202);
2315 it('covers bad issuer', async
function () {
2316 ctx
.parsedBody
.iss
= 'not a url';
2317 manager
.db
.profileIsValid
.resolves(true);
2318 await manager
.postTicket(req
, res
, ctx
);
2319 assert(res
.end
.called
);
2320 assert
.strictEqual(res
.statusCode
, 202);
2325 describe('queuedTicketProcessor', function () {
2326 let channel
, content
;
2327 const message
= () => ({
2328 content: Buffer
.from(JSON
.stringify(content
)),
2330 beforeEach(function () {
2335 ticket: 'XXXticketXXX',
2336 resource: 'https://blog.example.com/',
2337 subject: 'https://otheruser.exmaple.com/',
2338 iss: 'https://ia.example.com/',
2339 epochMs: Date
.now(),
2342 it('redeems a ticket', async
function () {
2343 await manager
.queuedTicketProcessor(channel
, message());
2344 assert(manager
.queuePublisher
.publish
.called
);
2345 assert(channel
.ack
.called
);
2347 it('redeems a ticket, missing issuer', async
function () {
2349 await manager
.queuedTicketProcessor(channel
, message());
2350 assert(manager
.queuePublisher
.publish
.called
);
2351 assert(channel
.ack
.called
);
2353 it('covers bad message', async
function () {
2354 await manager
.queuedTicketProcessor(channel
, { content: 'diddly' });
2355 assert(channel
.ack
.called
);
2357 it('covers bad issuer', async
function () {
2358 content
.iss
= 'not a url';
2359 await manager
.queuedTicketProcessor(channel
, message());
2360 assert(manager
.queuePublisher
.publish
.called
);
2362 it('covers bad resource', async
function () {
2363 content
.resource
= 'not a url';
2364 await manager
.queuedTicketProcessor(channel
, message());
2365 assert(manager
.communication
.redeemTicket
.notCalled
);
2366 assert(manager
.queuePublisher
.publish
.notCalled
);
2367 assert(channel
.ack
.called
);
2369 it('covers failed redemption', async
function () {
2370 const expectedException
= new Error('oh no');
2371 manager
.communication
.redeemTicket
.rejects(expectedException
);
2372 assert
.rejects(() => manager
.queuedTicketProcessor(channel
, message()), expectedException
);
2374 it('covers failed publish', async
function () {
2375 const expectedException
= new Error('oh no');
2376 manager
.queuePublisher
.publish
.rejects(expectedException
);
2377 assert
.rejects(() => manager
.queuedTicketProcessor(channel
, message()), expectedException
);
2379 }); // queuedTicketProcessor
2381 describe('getAdminMaintenance', function () {
2382 it('covers information', async
function () {
2383 await manager
.getAdminMaintenance(res
, ctx
);
2384 assert(res
.end
.called
);
2386 it('covers tasks', async
function () {
2388 [Enum
.Chore
.CleanTokens
]: '',
2390 await manager
.getAdminMaintenance(res
, ctx
);
2391 assert(res
.end
.called
);
2393 }); // getAdminMaintenance