set SameSite to Lax on session cookies
[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 describe('living-standard-20220212', function () {
117 it('covers valid profile', async function () {
118 ctx.parsedBody.me = 'https://example.com/profile';
119 manager.indieAuthCommunication.fetchProfile.resolves({
120 metadata: {
121 issuer: 'https://example.com/',
122 authorizationEndpoint: 'https://example.com/auth',
123 },
124 });
125 await manager.postAdminLogin(res, ctx);
126 assert.strictEqual(res.statusCode, 302);
127 });
128 it('covers bad issuer url', async function () {
129 ctx.parsedBody.me = 'https://example.com/profile';
130 manager.indieAuthCommunication.fetchProfile.resolves({
131 metadata: {
132 issuer: 'http://example.com/?bah#foo',
133 authorizationEndpoint: 'https://example.com/auth',
134 },
135 });
136 await manager.postAdminLogin(res, ctx);
137 assert(!res.setHeader.called);
138 });
139 it('covers unparsable issuer url', async function () {
140 ctx.parsedBody.me = 'https://example.com/profile';
141 manager.indieAuthCommunication.fetchProfile.resolves({
142 metadata: {
143 issuer: 'not a url',
144 authorizationEndpoint: 'https://example.com/auth',
145 },
146 });
147 await manager.postAdminLogin(res, ctx);
148 assert(!res.setHeader.called);
149 });
150 }); // living-standard-20220212
151 }); // postAdminLogin
152
153 describe('getAdminLogout', function () {
154 it('covers', async function () {
155 await manager.getAdminLogout(res, ctx);
156 });
157 }); // getAdminLogout
158
159 describe('getAdminIA', function () {
160 let state, me, authorizationEndpoint;
161 beforeEach(function () {
162 state = '4ea7e936-3427-11ec-9f4b-0025905f714a';
163 me = 'https://example.com/profile';
164 authorizationEndpoint = 'https://example.com/auth'
165 ctx.cookie = 'squeepSession=sessionCookie';
166 manager.indieAuthCommunication.redeemProfileCode.resolves({
167 me,
168 });
169 manager.indieAuthCommunication.fetchProfile.resolves({
170 metadata: {
171 authorizationEndpoint,
172 },
173 });
174 sinon.stub(manager.mysteryBox, 'unpack').resolves({
175 authorizationEndpoint,
176 state,
177 me,
178 });
179 });
180 it('covers valid', async function () {
181 ctx.queryParams['state'] = state;
182 ctx.queryParams['code'] = 'codeCodeCode';
183
184 await manager.getAdminIA(res, ctx);
185
186 assert.strictEqual(res.statusCode, 302);
187 });
188 it('covers missing cookie', async function () {
189 delete ctx.cookie;
190
191 await manager.getAdminIA(res, ctx);
192
193 assert(ctx.errors.length);
194 });
195 it('covers invalid cookie', async function () {
196 manager.mysteryBox.unpack.restore();
197 sinon.stub(manager.mysteryBox, 'unpack').rejects();
198
199 await manager.getAdminIA(res, ctx);
200
201 assert(ctx.errors.length);
202 });
203 it('covers mis-matched state', async function () {
204 ctx.queryParams['state'] = 'incorrect-state';
205 ctx.queryParams['code'] = 'codeCodeCode';
206
207 await manager.getAdminIA(res, ctx);
208
209 assert(ctx.errors.length);
210 });
211 it('relays auth endpoint errors', async function () {
212 ctx.queryParams['state'] = state;
213 ctx.queryParams['code'] = 'codeCodeCode';
214 ctx.queryParams['error'] = 'error_code';
215 ctx.queryParams['error_description'] = 'something went wrong';
216
217 await manager.getAdminIA(res, ctx);
218
219 assert(ctx.errors.length);
220 });
221 it('covers empty error_description', async function () {
222 ctx.queryParams['state'] = state;
223 ctx.queryParams['code'] = 'codeCodeCode';
224 ctx.queryParams['error'] = 'error_code';
225
226 await manager.getAdminIA(res, ctx);
227
228 assert(ctx.errors.length);
229 });
230 it('covers invalid restored session', async function () {
231 manager.mysteryBox.unpack.restore();
232 sinon.stub(manager.mysteryBox, 'unpack').resolves({
233 authorizationEndpoint: 'not a url',
234 state,
235 me,
236 });
237 ctx.queryParams['state'] = state;
238 ctx.queryParams['code'] = 'codeCodeCode';
239
240 await manager.getAdminIA(res, ctx);
241
242 assert(ctx.errors.length);
243 });
244 it('covers empty profile redemption response', async function () {
245 ctx.queryParams['state'] = state;
246 ctx.queryParams['code'] = 'codeCodeCode';
247 manager.indieAuthCommunication.redeemProfileCode.restore();
248 sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves();
249
250 await manager.getAdminIA(res, ctx);
251
252 assert(ctx.errors.length);
253 });
254 it('covers missing profile in redemption response', async function () {
255 ctx.queryParams['state'] = state;
256 ctx.queryParams['code'] = 'codeCodeCode';
257 manager.indieAuthCommunication.redeemProfileCode.restore();
258 sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
259 });
260
261 await manager.getAdminIA(res, ctx);
262
263 assert(ctx.errors.length);
264 });
265 it('covers different canonical profile response', async function () {
266 ctx.queryParams['state'] = state;
267 ctx.queryParams['code'] = 'codeCodeCode';
268 manager.indieAuthCommunication.redeemProfileCode.restore();
269 sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
270 me: 'https://different.example.com/profile',
271 });
272
273 await manager.getAdminIA(res, ctx);
274
275 assert.strictEqual(res.statusCode, 302);
276 });
277 it('covers different canonical profile response mis-matched endpoint', async function () {
278 ctx.queryParams['state'] = state;
279 ctx.queryParams['code'] = 'codeCodeCode';
280 manager.indieAuthCommunication.redeemProfileCode.restore();
281 sinon.stub(manager.indieAuthCommunication, 'redeemProfileCode').resolves({
282 me: 'https://different.example.com/profile',
283 });
284 manager.indieAuthCommunication.fetchProfile.restore();
285 sinon.stub(manager.indieAuthCommunication, 'fetchProfile').resolves({
286 metadata: {
287 authorizationEndpoint: 'https://elsewhere.example.com/auth',
288 },
289 });
290
291 await manager.getAdminIA(res, ctx);
292
293 assert(ctx.errors.length);
294 });
295 describe('living-standard-20220212', function () {
296 beforeEach(function () {
297 manager.indieAuthCommunication.fetchProfile.resolves({
298 metadata: {
299 authorizationEndpoint,
300 issuer: 'https://example.com/',
301 },
302 });
303 manager.mysteryBox.unpack.resolves({
304 authorizationEndpoint,
305 issuer: 'https://example.com/',
306 state,
307 me,
308 });
309 });
310 it('covers valid', async function () {
311 ctx.queryParams['state'] = state;
312 ctx.queryParams['code'] = 'codeCodeCode';
313 ctx.queryParams['iss'] = 'https://example.com/';
314
315 await manager.getAdminIA(res, ctx);
316
317 assert.strictEqual(res.statusCode, 302);
318 });
319 it('covers mis-matched issuer', async function () {
320 ctx.queryParams['state'] = state;
321 ctx.queryParams['code'] = 'codeCodeCode';
322
323 await manager.getAdminIA(res, ctx);
324
325 assert(ctx.errors.length);
326 });
327 }); // living-standard-20220212
328 }); // getAdminIA
329
330 }); // SessionManager