2 /* eslint-disable capitalized-comments, sonarjs/no-duplicate-string, sonarjs/no-identical-functions */
6 const assert
= require('assert');
7 const sinon
= require('sinon'); // eslint-disable-line node/no-unpublished-require
9 const SessionManager
= require('../../lib/session-manager');
10 const Enum
= require('../../lib/enum');
11 const Config
= require('../stub-config');
12 const stubLogger
= require('../stub-logger');
14 describe('SessionManager', function () {
15 let manager
, options
, stubAuthenticator
;
18 beforeEach(function () {
19 options
= new Config('test');
22 setHeader: sinon
.stub(),
32 isValidIdentifierCredential: sinon
.stub(),
33 checkOTP: sinon
.stub(),
35 manager
= new SessionManager(stubLogger
, stubAuthenticator
, options
);
36 sinon
.stub(manager
.indieAuthCommunication
);
39 afterEach(function () {
43 describe('_sessionCookieSet', function () {
45 beforeEach(function () {
49 it('covers', async
function () {
50 await manager
._sessionCookieSet(res
, session
, maxAge
);
51 assert(res
.setHeader
.called
);
53 it('covers reset', async
function () {
56 await manager
._sessionCookieSet(res
, session
, maxAge
);
57 assert(res
.setHeader
.called
);
59 it('covers options', async
function() {
60 options
.authenticator
.secureAuthOnly
= false;
61 await manager
._sessionCookieSet(res
, session
, undefined, '');
62 assert(res
.setHeader
.called
);
64 }); // _sessionCookieSet
66 describe('getAdminLogin', function () {
67 it('covers no session', async
function () {
68 await manager
.getAdminLogin(res
, ctx
);
70 it('covers established session', async
function () {
71 ctx
.authenticationId
= 'identifier';
72 ctx
.queryParams
['r'] = '/admin';
73 await manager
.getAdminLogin(res
, ctx
);
74 assert
.strictEqual(res
.statusCode
, 302);
75 assert(res
.setHeader
.called
);
79 describe('postAdminLogin', function () {
80 beforeEach(function () {
81 sinon
.stub(manager
, '_otpSubmission').resolves(false);
83 it('covers otp submission', async
function () {
84 manager
._otpSubmission
.resolves(true);
85 await manager
.postAdminLogin(res
, ctx
);
86 assert(res
.end
.notCalled
);
88 it('covers valid local', async
function () {
89 ctx
.parsedBody
.identifier
= 'user';
90 ctx
.parsedBody
.credential
= 'password';
91 manager
.authenticator
.isValidIdentifierCredential
.resolves(true);
92 await manager
.postAdminLogin(res
, ctx
);
93 assert
.strictEqual(res
.statusCode
, 302);
95 it('covers invalid local', async
function () {
96 ctx
.parsedBody
.identifier
= 'user';
97 ctx
.parsedBody
.credential
= 'password';
98 manager
.authenticator
.isValidIdentifierCredential
.resolves(false);
99 await manager
.postAdminLogin(res
, ctx
);
100 assert(!res
.setHeader
.called
);
102 it('covers valid profile', async
function () {
103 ctx
.parsedBody
.me
= 'https://example.com/profile';
104 manager
.indieAuthCommunication
.fetchProfile
.resolves({
106 authorizationEndpoint: 'https://example.com/auth',
109 await manager
.postAdminLogin(res
, ctx
);
110 assert
.strictEqual(res
.statusCode
, 302);
112 it('covers invalid profile', async
function () {
113 ctx
.parsedBody
.me
= 'not a profile';
114 manager
.indieAuthCommunication
.fetchProfile
.resolves();
115 await manager
.postAdminLogin(res
, ctx
);
116 assert(!res
.setHeader
.called
);
118 it('covers invalid profile response', async
function () {
119 ctx
.parsedBody
.me
= 'https://example.com/profile';
120 manager
.indieAuthCommunication
.fetchProfile
.resolves();
121 await manager
.postAdminLogin(res
, ctx
);
122 assert(!res
.setHeader
.called
);
124 it('covers invalid profile response endpoint', async
function () {
125 ctx
.parsedBody
.me
= 'https://example.com/profile';
126 manager
.indieAuthCommunication
.fetchProfile
.resolves({
128 authorizationEndpoint: 'not an auth endpoint',
131 await manager
.postAdminLogin(res
, ctx
);
132 assert(!res
.setHeader
.called
);
134 it('covers profile scheme fallback', async
function () {
135 ctx
.parsedBody
.me
= 'https://example.com/profile';
136 ctx
.parsedBody
['me_auto_scheme'] = '1';
137 manager
.indieAuthCommunication
.fetchProfile
138 .onCall(0).resolves()
139 .onCall(1).resolves({
141 issuer: 'https://example.com/',
142 authorizationEndpoint: 'https://example.com/auth',
145 await manager
.postAdminLogin(res
, ctx
);
146 assert
.strictEqual(res
.statusCode
, 302);
149 describe('living-standard-20220212', function () {
150 it('covers valid profile', async
function () {
151 ctx
.parsedBody
.me
= 'https://example.com/profile';
152 manager
.indieAuthCommunication
.fetchProfile
.resolves({
154 issuer: 'https://example.com/',
155 authorizationEndpoint: 'https://example.com/auth',
158 await manager
.postAdminLogin(res
, ctx
);
159 assert
.strictEqual(res
.statusCode
, 302);
161 it('covers bad issuer url', async
function () {
162 ctx
.parsedBody
.me
= 'https://example.com/profile';
163 manager
.indieAuthCommunication
.fetchProfile
.resolves({
165 issuer: 'http://example.com/?bah#foo',
166 authorizationEndpoint: 'https://example.com/auth',
169 await manager
.postAdminLogin(res
, ctx
);
170 assert(!res
.setHeader
.called
);
172 it('covers unparsable issuer url', async
function () {
173 ctx
.parsedBody
.me
= 'https://example.com/profile';
174 manager
.indieAuthCommunication
.fetchProfile
.resolves({
177 authorizationEndpoint: 'https://example.com/auth',
180 await manager
.postAdminLogin(res
, ctx
);
181 assert(!res
.setHeader
.called
);
183 }); // living-standard-20220212
184 }); // postAdminLogin
186 describe('_otpSubmission', function () {
187 beforeEach(function () {
188 sinon
.useFakeTimers(new Date());
189 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
190 authenticatedIdentifier: 'identifier',
194 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.Valid
);
195 ctx
.parsedBody
.state
= 'state_data';
196 ctx
.parsedBody
.otp
= '123456';
198 it('returns false if no otp state', async
function () {
199 delete ctx
.parsedBody
.state
;
200 const result
= await manager
._otpSubmission(res
, ctx
);
201 assert(manager
.mysteryBox
.unpack
.notCalled
);
202 assert
.strictEqual(result
, false);
204 it('returns false when presented with invalid otp state', async
function () {
205 manager
.mysteryBox
.unpack
.rejects();
206 const result
= await manager
._otpSubmission(res
, ctx
);
207 assert(manager
.mysteryBox
.unpack
.called
);
208 assert
.strictEqual(result
, false);
210 it('returns true when submitted otp is invalid, but allowed to retry', async
function () {
211 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.InvalidSoftFail
);
212 const result
= await manager
._otpSubmission(res
, ctx
);
213 assert(manager
.mysteryBox
.unpack
.called
);
214 assert
.strictEqual(result
, true);
215 assert(res
.end
.called
);
217 it('returns false when submitted otp is invalid and too many attempts', async
function () {
218 manager
.mysteryBox
.unpack
.resolves({
219 authenticatedIdentifier: 'identifier',
223 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.InvalidHardFail
);
224 const result
= await manager
._otpSubmission(res
, ctx
);
225 assert(manager
.mysteryBox
.unpack
.called
);
226 assert
.strictEqual(result
, false);
228 it('returns false when submitted otp is invalid and too much time has passed', async
function () {
229 manager
.mysteryBox
.unpack
.resolves({
230 authenticatedIdentifier: 'identifier',
232 epochMs: Date
.now() - 99999999,
234 manager
.authenticator
.checkOTP
.resolves(Enum
.OTPResult
.InvalidHardFail
);
235 const result
= await manager
._otpSubmission(res
, ctx
);
236 assert(manager
.mysteryBox
.unpack
.called
);
237 assert
.strictEqual(result
, false);
239 it('returns true when submitted otp is valid', async
function () {
240 const result
= await manager
._otpSubmission(res
, ctx
);
241 assert(res
.end
.called
);
242 assert
.strictEqual(result
, true);
244 it('covers unexpected otp response', async
function () {
245 manager
.authenticator
.checkOTP
.resolves('wrong');
246 assert
.rejects(() => manager
._otpSubmission(res
, ctx
), RangeError
);
248 }); // _otpSubmission
250 describe('_localUserAuth', function () {
251 beforeEach(function () {
252 ctx
.parsedBody
.identifier
= 'identifier';
253 ctx
.parsedBody
.credential
= 'credential';
254 manager
.authenticator
.isValidIdentifierCredential
.resolves(true);
255 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('box');
257 it('returns false if indieauth available', async
function () {
258 ctx
.parsedBody
.me
= 'https://example.com/';
259 const result
= await manager
._localUserAuth(res
, ctx
);
260 assert
.strictEqual(result
, false);
262 it('returns true if identifier is invalid', async
function () {
263 manager
.authenticator
.isValidIdentifierCredential
.resolves(false);
264 const result
= await manager
._localUserAuth(res
, ctx
);
265 assert
.strictEqual(result
, true);
266 assert(manager
.authenticator
.isValidIdentifierCredential
.called
);
267 assert(res
.end
.called
);
269 it('returns true if valid identifier', async
function () {
270 const result
= await manager
._localUserAuth(res
, ctx
);
271 assert
.strictEqual(result
, true);
272 assert(res
.end
.called
);
274 it('returns true if valid identifier requires otp entry', async
function () {
275 ctx
.otpNeeded
= true;
276 const result
= await manager
._localUserAuth(res
, ctx
);
277 assert
.strictEqual(result
, true);
278 assert(manager
.mysteryBox
.pack
.called
);
279 assert(res
.end
.called
);
281 }); // _localUserAuth
283 describe('getAdminLogout', function () {
284 it('covers', async
function () {
285 await manager
.getAdminLogout(res
, ctx
);
287 }); // getAdminLogout
289 describe('getAdminIA', function () {
290 let state
, me
, authorizationEndpoint
;
291 beforeEach(function () {
292 state
= '4ea7e936-3427-11ec-9f4b-0025905f714a';
293 me
= 'https://example.com/profile';
294 authorizationEndpoint
= 'https://example.com/auth'
295 ctx
.cookie
= 'squeepSession=sessionCookie';
296 manager
.indieAuthCommunication
.redeemProfileCode
.resolves({
299 manager
.indieAuthCommunication
.fetchProfile
.resolves({
301 authorizationEndpoint
,
304 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
305 authorizationEndpoint
,
310 it('covers valid', async
function () {
311 ctx
.queryParams
['state'] = state
;
312 ctx
.queryParams
['code'] = 'codeCodeCode';
314 await manager
.getAdminIA(res
, ctx
);
316 assert
.strictEqual(res
.statusCode
, 302);
318 it('covers missing cookie', async
function () {
321 await manager
.getAdminIA(res
, ctx
);
323 assert(ctx
.errors
.length
);
325 it('covers invalid cookie', async
function () {
326 manager
.mysteryBox
.unpack
.restore();
327 sinon
.stub(manager
.mysteryBox
, 'unpack').rejects();
329 await manager
.getAdminIA(res
, ctx
);
331 assert(ctx
.errors
.length
);
333 it('covers mis-matched state', async
function () {
334 ctx
.queryParams
['state'] = 'incorrect-state';
335 ctx
.queryParams
['code'] = 'codeCodeCode';
337 await manager
.getAdminIA(res
, ctx
);
339 assert(ctx
.errors
.length
);
341 it('relays auth endpoint errors', async
function () {
342 ctx
.queryParams
['state'] = state
;
343 ctx
.queryParams
['code'] = 'codeCodeCode';
344 ctx
.queryParams
['error'] = 'error_code';
345 ctx
.queryParams
['error_description'] = 'something went wrong';
347 await manager
.getAdminIA(res
, ctx
);
349 assert(ctx
.errors
.length
);
351 it('covers empty error_description', async
function () {
352 ctx
.queryParams
['state'] = state
;
353 ctx
.queryParams
['code'] = 'codeCodeCode';
354 ctx
.queryParams
['error'] = 'error_code';
356 await manager
.getAdminIA(res
, ctx
);
358 assert(ctx
.errors
.length
);
360 it('covers invalid restored session', async
function () {
361 manager
.mysteryBox
.unpack
.restore();
362 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
363 authorizationEndpoint: 'not a url',
367 ctx
.queryParams
['state'] = state
;
368 ctx
.queryParams
['code'] = 'codeCodeCode';
370 await manager
.getAdminIA(res
, ctx
);
372 assert(ctx
.errors
.length
);
374 it('covers empty profile redemption response', async
function () {
375 ctx
.queryParams
['state'] = state
;
376 ctx
.queryParams
['code'] = 'codeCodeCode';
377 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
378 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves();
380 await manager
.getAdminIA(res
, ctx
);
382 assert(ctx
.errors
.length
);
384 it('covers missing profile in redemption response', async
function () {
385 ctx
.queryParams
['state'] = state
;
386 ctx
.queryParams
['code'] = 'codeCodeCode';
387 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
388 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves({
391 await manager
.getAdminIA(res
, ctx
);
393 assert(ctx
.errors
.length
);
395 it('covers different canonical profile response', async
function () {
396 ctx
.queryParams
['state'] = state
;
397 ctx
.queryParams
['code'] = 'codeCodeCode';
398 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
399 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves({
400 me: 'https://different.example.com/profile',
403 await manager
.getAdminIA(res
, ctx
);
405 assert
.strictEqual(res
.statusCode
, 302);
407 it('covers different canonical profile response mis-matched endpoint', async
function () {
408 ctx
.queryParams
['state'] = state
;
409 ctx
.queryParams
['code'] = 'codeCodeCode';
410 manager
.indieAuthCommunication
.redeemProfileCode
.restore();
411 sinon
.stub(manager
.indieAuthCommunication
, 'redeemProfileCode').resolves({
412 me: 'https://different.example.com/profile',
414 manager
.indieAuthCommunication
.fetchProfile
.restore();
415 sinon
.stub(manager
.indieAuthCommunication
, 'fetchProfile').resolves({
417 authorizationEndpoint: 'https://elsewhere.example.com/auth',
421 await manager
.getAdminIA(res
, ctx
);
423 assert(ctx
.errors
.length
);
425 describe('living-standard-20220212', function () {
426 beforeEach(function () {
427 manager
.indieAuthCommunication
.fetchProfile
.resolves({
429 authorizationEndpoint
,
430 issuer: 'https://example.com/',
433 manager
.mysteryBox
.unpack
.resolves({
434 authorizationEndpoint
,
435 issuer: 'https://example.com/',
440 it('covers valid', async
function () {
441 ctx
.queryParams
['state'] = state
;
442 ctx
.queryParams
['code'] = 'codeCodeCode';
443 ctx
.queryParams
['iss'] = 'https://example.com/';
445 await manager
.getAdminIA(res
, ctx
);
447 assert
.strictEqual(res
.statusCode
, 302);
449 it('covers mis-matched issuer', async
function () {
450 ctx
.queryParams
['state'] = state
;
451 ctx
.queryParams
['code'] = 'codeCodeCode';
453 await manager
.getAdminIA(res
, ctx
);
455 assert(ctx
.errors
.length
);
457 }); // living-standard-20220212
460 }); // SessionManager