default indieauth profile entry to https if scheme not specified
[squeep-authentication-module] / test / lib / session-manager.js
1 /* eslint-env mocha */
2 /* eslint-disable capitalized-comments, sonarjs/no-duplicate-string, sonarjs/no-identical-functions */
3
4 'use strict';
5
6 const assert = require('assert');
7 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
8
9 const SessionManager = require('../../lib/session-manager');
10 const Config = require('../stub-config');
11 const stubLogger = require('../stub-logger');
12
13 describe('SessionManager', function () {
14 let manager, options, stubAuthenticator;
15 let res, ctx;
16
17 beforeEach(function () {
18 options = new Config('test');
19 res = {
20 end: sinon.stub(),
21 setHeader: sinon.stub(),
22 };
23 ctx = {
24 cookie: '',
25 params: {},
26 queryParams: {},
27 parsedBody: {},
28 };
29 stubAuthenticator = {
30 isValidIdentifierCredential: sinon.stub(),
31 };
32 manager = new SessionManager(stubLogger, stubAuthenticator, options);
33 sinon.stub(manager.indieAuthCommunication);
34 stubLogger._reset();
35 });
36 afterEach(function () {
37 sinon.restore();
38 });
39
40 describe('_sessionCookieSet', function () {
41 let session, maxAge;
42 beforeEach(function () {
43 session = {};
44 maxAge = 86400;
45 });
46 it('covers', async function () {
47 await manager._sessionCookieSet(res, session, maxAge);
48 assert(res.setHeader.called);
49 });
50 it('covers reset', async function () {
51 session = undefined;
52 maxAge = 0;
53 await manager._sessionCookieSet(res, session, maxAge);
54 assert(res.setHeader.called);
55 });
56 it('covers options', async function() {
57 options.authenticator.secureAuthOnly = false;
58 await manager._sessionCookieSet(res, session, undefined, '');
59 assert(res.setHeader.called);
60 });
61 }); // _sessionCookieSet
62
63 describe('getAdminLogin', function () {
64 it('covers', async function () {
65 await manager.getAdminLogin(res, ctx);
66 });
67 }); // getAdminLogin
68
69 describe('postAdminLogin', function () {
70 it('covers valid local', async function () {
71 ctx.parsedBody.identifier = 'user';
72 ctx.parsedBody.credential = 'password';
73 manager.authenticator.isValidIdentifierCredential.resolves(true);
74 await manager.postAdminLogin(res, ctx);
75 assert.strictEqual(res.statusCode, 302);
76 });
77 it('covers invalid local', async function () {
78 ctx.parsedBody.identifier = 'user';
79 ctx.parsedBody.credential = 'password';
80 manager.authenticator.isValidIdentifierCredential.resolves(false);
81 await manager.postAdminLogin(res, ctx);
82 assert(!res.setHeader.called);
83 });
84 it('covers valid profile', async function () {
85 ctx.parsedBody.me = 'https://example.com/profile';
86 manager.indieAuthCommunication.fetchProfile.resolves({
87 metadata: {
88 authorizationEndpoint: 'https://example.com/auth',
89 },
90 });
91 await manager.postAdminLogin(res, ctx);
92 assert.strictEqual(res.statusCode, 302);
93 });
94 it('covers invalid profile', async function () {
95 ctx.parsedBody.me = 'not a profile';
96 manager.indieAuthCommunication.fetchProfile.resolves();
97 await manager.postAdminLogin(res, ctx);
98 assert(!res.setHeader.called);
99 });
100 it('covers invalid profile response', async function () {
101 ctx.parsedBody.me = 'https://example.com/profile';
102 manager.indieAuthCommunication.fetchProfile.resolves();
103 await manager.postAdminLogin(res, ctx);
104 assert(!res.setHeader.called);
105 });
106 it('covers invalid profile response endpoint', async function () {
107 ctx.parsedBody.me = 'https://example.com/profile';
108 manager.indieAuthCommunication.fetchProfile.resolves({
109 metadata: {
110 authorizationEndpoint: 'not an auth endpoint',
111 },
112 });
113 await manager.postAdminLogin(res, ctx);
114 assert(!res.setHeader.called);
115 });
116 it('covers profile scheme fallback', async function () {
117 ctx.parsedBody.me = 'https://example.com/profile';
118 ctx.parsedBody.me_auto_scheme = '1';
119 manager.indieAuthCommunication.fetchProfile
120 .onCall(0).resolves()
121 .onCall(1).resolves({
122 metadata: {
123 issuer: 'https://example.com/',
124 authorizationEndpoint: 'https://example.com/auth',
125 },
126 });
127 await manager.postAdminLogin(res, ctx);
128 assert.strictEqual(res.statusCode, 302);
129
130 });
131 describe('living-standard-20220212', function () {
132 it('covers valid profile', async function () {
133 ctx.parsedBody.me = 'https://example.com/profile';
134 manager.indieAuthCommunication.fetchProfile.resolves({
135 metadata: {
136 issuer: 'https://example.com/',
137 authorizationEndpoint: 'https://example.com/auth',
138 },
139 });
140 await manager.postAdminLogin(res, ctx);
141 assert.strictEqual(res.statusCode, 302);
142 });
143 it('covers bad issuer url', async function () {
144 ctx.parsedBody.me = 'https://example.com/profile';
145 manager.indieAuthCommunication.fetchProfile.resolves({
146 metadata: {
147 issuer: 'http://example.com/?bah#foo',
148 authorizationEndpoint: 'https://example.com/auth',
149 },
150 });
151 await manager.postAdminLogin(res, ctx);
152 assert(!res.setHeader.called);
153 });
154 it('covers unparsable issuer url', async function () {
155 ctx.parsedBody.me = 'https://example.com/profile';
156 manager.indieAuthCommunication.fetchProfile.resolves({
157 metadata: {
158 issuer: 'not a url',
159 authorizationEndpoint: 'https://example.com/auth',
160 },
161 });
162 await manager.postAdminLogin(res, ctx);
163 assert(!res.setHeader.called);
164 });
165 }); // living-standard-20220212
166 }); // postAdminLogin
167
168 describe('getAdminLogout', function () {
169 it('covers', async function () {
170 await manager.getAdminLogout(res, ctx);
171 });
172 }); // getAdminLogout
173
174 describe('getAdminIA', function () {
175 let state, me, authorizationEndpoint;
176 beforeEach(function () {
177 state = '4ea7e936-3427-11ec-9f4b-0025905f714a';
178 me = 'https://example.com/profile';
179 authorizationEndpoint = 'https://example.com/auth'
180 ctx.cookie = 'squeepSession=sessionCookie';
181 manager.indieAuthCommunication.redeemProfileCode.resolves({
182 me,
183 });
184 manager.indieAuthCommunication.fetchProfile.resolves({
185 metadata: {
186 authorizationEndpoint,
187 },
188 });
189 sinon.stub(manager.mysteryBox, 'unpack').resolves({
190 authorizationEndpoint,
191 state,
192 me,
193 });
194 });
195 it('covers valid', async function () {
196 ctx.queryParams['state'] = state;
197 ctx.queryParams['code'] = 'codeCodeCode';
198
199 await manager.getAdminIA(res, ctx);
200
201 assert.strictEqual(res.statusCode, 302);
202 });
203 it('covers missing cookie', async function () {
204 delete ctx.cookie;
205
206 await manager.getAdminIA(res, ctx);
207
208 assert(ctx.errors.length);
209 });
210 it('covers invalid cookie', async function () {
211 manager.mysteryBox.unpack.restore();
212 sinon.stub(manager.mysteryBox, 'unpack').rejects();
213
214 await manager.getAdminIA(res, ctx);
215
216 assert(ctx.errors.length);
217 });
218 it('covers mis-matched state', async function () {
219 ctx.queryParams['state'] = 'incorrect-state';
220 ctx.queryParams['code'] = 'codeCodeCode';
221
222 await manager.getAdminIA(res, ctx);
223
224 assert(ctx.errors.length);
225 });
226 it('relays auth endpoint errors', async function () {
227 ctx.queryParams['state'] = state;
228 ctx.queryParams['code'] = 'codeCodeCode';
229 ctx.queryParams['error'] = 'error_code';
230 ctx.queryParams['error_description'] = 'something went wrong';
231
232 await manager.getAdminIA(res, ctx);
233
234 assert(ctx.errors.length);
235 });
236 it('covers empty error_description', async function () {
237 ctx.queryParams['state'] = state;
238 ctx.queryParams['code'] = 'codeCodeCode';
239 ctx.queryParams['error'] = 'error_code';
240
241 await manager.getAdminIA(res, ctx);
242
243 assert(ctx.errors.length);
244 });
245 it('covers invalid restored session', async function () {
246 manager.mysteryBox.unpack.restore();
247 sinon.stub(manager.mysteryBox, 'unpack').resolves({
248 authorizationEndpoint: 'not a url',
249 state,
250 me,
251 });
252 ctx.queryParams['state'] = state;
253 ctx.queryParams['code'] = 'codeCodeCode';
254
255 await manager.getAdminIA(res, ctx);
256
257 assert(ctx.errors.length);
258 });
259 it('covers empty profile redemption response', async function () {
260 ctx.queryParams['state'] = state;
261 ctx.queryParams['code'] = 'codeCodeCode';
262 manager.indieAuthCommunication.redeemProfileCode.restore();
263 sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves();
264
265 await manager.getAdminIA(res, ctx);
266
267 assert(ctx.errors.length);
268 });
269 it('covers missing profile in redemption response', async function () {
270 ctx.queryParams['state'] = state;
271 ctx.queryParams['code'] = 'codeCodeCode';
272 manager.indieAuthCommunication.redeemProfileCode.restore();
273 sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
274 });
275
276 await manager.getAdminIA(res, ctx);
277
278 assert(ctx.errors.length);
279 });
280 it('covers different canonical profile response', async function () {
281 ctx.queryParams['state'] = state;
282 ctx.queryParams['code'] = 'codeCodeCode';
283 manager.indieAuthCommunication.redeemProfileCode.restore();
284 sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
285 me: 'https://different.example.com/profile',
286 });
287
288 await manager.getAdminIA(res, ctx);
289
290 assert.strictEqual(res.statusCode, 302);
291 });
292 it('covers different canonical profile response mis-matched endpoint', async function () {
293 ctx.queryParams['state'] = state;
294 ctx.queryParams['code'] = 'codeCodeCode';
295 manager.indieAuthCommunication.redeemProfileCode.restore();
296 sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
297 me: 'https://different.example.com/profile',
298 });
299 manager.indieAuthCommunication.fetchProfile.restore();
300 sinon.stub(manager.indieAuthCommunication, 'fetchProfile').resolves({
301 metadata: {
302 authorizationEndpoint: 'https://elsewhere.example.com/auth',
303 },
304 });
305
306 await manager.getAdminIA(res, ctx);
307
308 assert(ctx.errors.length);
309 });
310 describe('living-standard-20220212', function () {
311 beforeEach(function () {
312 manager.indieAuthCommunication.fetchProfile.resolves({
313 metadata: {
314 authorizationEndpoint,
315 issuer: 'https://example.com/',
316 },
317 });
318 manager.mysteryBox.unpack.resolves({
319 authorizationEndpoint,
320 issuer: 'https://example.com/',
321 state,
322 me,
323 });
324 });
325 it('covers valid', async function () {
326 ctx.queryParams['state'] = state;
327 ctx.queryParams['code'] = 'codeCodeCode';
328 ctx.queryParams['iss'] = 'https://example.com/';
329
330 await manager.getAdminIA(res, ctx);
331
332 assert.strictEqual(res.statusCode, 302);
333 });
334 it('covers mis-matched issuer', async function () {
335 ctx.queryParams['state'] = state;
336 ctx.queryParams['code'] = 'codeCodeCode';
337
338 await manager.getAdminIA(res, ctx);
339
340 assert(ctx.errors.length);
341 });
342 }); // living-standard-20220212
343 }); // getAdminIA
344
345 }); // SessionManager