IndieAuth login support, allows viewing of topics related to profile
[websub-hub] / test / src / authenticator.js
1 /* eslint-env mocha */
2 'use strict';
3
4 const assert = require('assert');
5 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
6 const Authenticator = require('../../src/authenticator');
7 const stubLogger = require('../stub-logger');
8 const stubDb = require('../stub-db');
9 const Errors = require('../../src/errors');
10 const Enum = require('../../src/enum');
11 const Config = require('../../config');
12
13 const noExpectedException = 'did not receive expected exception';
14
15 describe('Authenticator', function () {
16 let authenticator, credential, ctx, identifier, password, options;
17 function _authMechanismRequired(a, m) {
18 if (!a.authn[m]) { // eslint-disable-line security/detect-object-injection
19 this.skip();
20 }
21 };
22
23 beforeEach(function () {
24 options = Config('test');
25 authenticator = new Authenticator(stubLogger, stubDb, options);
26 identifier = 'username';
27 credential = '$argon2id$v=19$m=4096,t=3,p=1$1a6zRlX4BI4$sZGcQ72BTpDOlxUI/j3DmE1PMcu+Cs5liZ/D6kk79Ew';
28 ctx = {};
29 password = 'badPassword';
30 stubDb._reset();
31 });
32 afterEach(function () {
33 sinon.restore();
34 });
35
36 it('covers no auth mechanisms', function () {
37 options.authenticator.authnEnabled = [];
38 try {
39 authenticator = new Authenticator(stubLogger, stubDb, options);
40 assert.fail(noExpectedException);
41 } catch (e) {
42 assert.strictEqual(e.message, 'no authentication mechanisms available');
43 }
44 });
45
46 describe('isValidBasic', function () {
47 it('succeeds', async function () {
48 _authMechanismRequired(authenticator, 'argon2');
49 authenticator.db.authenticationGet.resolves({
50 identifier,
51 credential,
52 });
53 const authString = `${identifier}:${password}`;
54 const result = await authenticator.isValidBasic(authString, ctx);
55 assert.strictEqual(result, true);
56 assert.strictEqual(ctx.authenticationId, identifier);
57 });
58 it('fails', async function () {
59 _authMechanismRequired(authenticator, 'argon2');
60 authenticator.db.authenticationGet.resolves({
61 identifier,
62 credential,
63 });
64 const authString = `${identifier}:wrongPassword}`;
65 const result = await authenticator.isValidBasic(authString, ctx);
66 assert.strictEqual(result, false);
67 assert.strictEqual(ctx.authenticationId, undefined);
68 });
69 it('covers no entry', async function() {
70 authenticator.db.authenticationGet.resolves();
71 const authString = `${identifier}:wrongPassword}`;
72 const result = await authenticator.isValidBasic(authString, ctx);
73 assert.strictEqual(result, false);
74 assert.strictEqual(ctx.authenticationId, undefined);
75 });
76 it('covers unknown password hash', async function () {
77 authenticator.db.authenticationGet.resolves({
78 identifier,
79 credential: '$other$kind_of_credential',
80 });
81 const authString = `${identifier}:wrongPassword}`;
82 const result = await authenticator.isValidBasic(authString, ctx);
83 assert.strictEqual(result, false);
84 assert.strictEqual(ctx.authenticationId, undefined);
85 });
86 }); // isValidBasic
87
88 describe('isValidIdentifierCredential', function () {
89 it('succeeds', async function () {
90 _authMechanismRequired(authenticator, 'argon2');
91 authenticator.db.authenticationGet.resolves({
92 identifier,
93 credential,
94 });
95 const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
96 assert.strictEqual(result, true);
97 assert.strictEqual(ctx.authenticationId, identifier);
98 });
99 it('fails', async function () {
100 _authMechanismRequired(authenticator, 'argon2');
101 authenticator.db.authenticationGet.resolves({
102 identifier,
103 credential,
104 });
105 const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
106 assert.strictEqual(result, false);
107 assert.strictEqual(ctx.authenticationId, undefined);
108 });
109 it('covers no entry', async function() {
110 authenticator.db.authenticationGet.resolves();
111 const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
112 assert.strictEqual(result, false);
113 assert.strictEqual(ctx.authenticationId, undefined);
114 });
115 it('covers unknown password hash', async function () {
116 authenticator.db.authenticationGet.resolves({
117 identifier,
118 credential: '$other$kind_of_credential',
119 });
120 const result = await authenticator.isValidIdentifierCredential(identifier, 'wrongPassword', ctx);
121 assert.strictEqual(result, false);
122 assert.strictEqual(ctx.authenticationId, undefined);
123 });
124 it('covers PAM', async function () {
125 _authMechanismRequired(authenticator, 'pam');
126 sinon.stub(authenticator, '_isValidPAMIdentifier').resolves(true);
127 authenticator.db.authenticationGet.resolves({
128 identifier,
129 credential: '$PAM$',
130 });
131 const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
132 assert.strictEqual(result, true);
133 assert.strictEqual(ctx.authenticationId, identifier);
134 });
135 it('covers debug', async function () {
136 authenticator.authnEnabled = ['DEBUG_ANY'];
137 const result = await authenticator.isValidIdentifierCredential(identifier, password, ctx);
138 assert.strictEqual(result, true);
139 assert.strictEqual(ctx.authenticationId, identifier);
140 });
141 }); // isValidIdentifierCredential
142
143 describe('_isValidPAMIdentifier', function () {
144 beforeEach(function () {
145 _authMechanismRequired(authenticator, 'pam');
146 sinon.stub(authenticator.authn.pam, 'pamAuthenticatePromise');
147 });
148 it('covers success', async function () {
149 authenticator.authn.pam.pamAuthenticatePromise.resolves(true);
150 const result = await authenticator._isValidPAMIdentifier(identifier, credential);
151 assert.strictEqual(result, true);
152 });
153 it('covers failure', async function () {
154 _authMechanismRequired(authenticator, 'pam');
155 authenticator.authn.pam.pamAuthenticatePromise.rejects(new authenticator.authn.pam.PamError());
156 const result = await authenticator._isValidPAMIdentifier(identifier, credential);
157 assert.strictEqual(result, false);
158 });
159 it('covers error', async function () {
160 _authMechanismRequired(authenticator, 'pam');
161 const expected = new Error('blah');
162 authenticator.authn.pam.pamAuthenticatePromise.rejects(expected);
163 try {
164 await authenticator._isValidPAMIdentifier(identifier, credential);
165 assert.fail(noExpectedException);
166 } catch (e) {
167 assert.deepStrictEqual(e, expected);
168 }
169 });
170 it('covers forbidden', async function () {
171 identifier = 'root';
172 const result = await authenticator._isValidPAMIdentifier(identifier, credential);
173 assert.strictEqual(result, false);
174 });
175 }); // _isValidPAMIdentifier
176
177 describe('isValidAuthorization', function () {
178 it('handles basic', async function () {
179 const expected = true;
180 const authorizationHeader = 'basic Zm9vOmJhcg==';
181 sinon.stub(authenticator, 'isValidBasic').resolves(expected);
182 const result = await authenticator.isValidAuthorization(authorizationHeader, ctx);
183 assert.strictEqual(result, expected);
184 });
185 it('handles other', async function () {
186 const expected = false;
187 const authorizationHeader = 'bearer Zm9vOmJhcg==';
188 const result = await authenticator.isValidAuthorization(authorizationHeader, ctx);
189 assert.strictEqual(result, expected);
190 });
191 }); // isValidAuthorization
192
193 describe('requestBasic', function () {
194 it('covers', function () {
195 try {
196 const res = {
197 setHeader: () => {},
198 };
199 authenticator.requestBasic(res);
200 assert.fail(noExpectedException);
201 } catch (e) {
202 assert(e instanceof Errors.ResponseError);
203 assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
204 }
205 });
206 }); // requestBasic
207
208 describe('isValidCookieAuth', function () {
209 beforeEach(function () {
210 sinon.stub(authenticator.mysteryBox, 'unpack');
211 });
212 it('covers identifier success', async function () {
213 authenticator.mysteryBox.unpack.resolves({
214 authenticatedIdentifier: 'identifier',
215 });
216 const result = await authenticator.isValidCookieAuth(ctx, 'WSHas=dummy');
217 assert.strictEqual(result, true);
218 });
219 it('covers profile success', async function () {
220 authenticator.mysteryBox.unpack.resolves({
221 authenticatedProfile: 'profile',
222 });
223 const result = await authenticator.isValidCookieAuth(ctx, 'WSHas=dummy');
224 assert.strictEqual(result, true);
225 });
226 it('covers missing cookie', async function () {
227 const result = await authenticator.isValidCookieAuth(ctx, 'wrongCookie');
228 assert.strictEqual(result, false);
229 });
230 it('covers bad cookie', async function () {
231 authenticator.mysteryBox.unpack.rejects();
232 const result = await authenticator.isValidCookieAuth(ctx, 'WSHas=dummy');
233 assert.strictEqual(result, false);
234 });
235 }); // isValidCookieAuth
236
237 describe('required', function () {
238 let req, res;
239 beforeEach(function () {
240 ctx.clientProtocol = 'https';
241 req = {
242 getHeader: sinon.stub(),
243 };
244 res = {
245 end: sinon.stub(),
246 setHeader: sinon.stub(),
247 }
248 });
249 it('succeeds', async function() {
250 req.getHeader.returns('auth header');
251 sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
252 const result = await authenticator.required(req, res, ctx);
253 assert.strictEqual(result, true);
254 });
255 it('covers valid cookie session', async function () {
256 req.getHeader.returns('WSHas=sessionCookie');
257 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
258 const result = await authenticator.required(req, res, ctx);
259 assert.strictEqual(result, true);
260 });
261 it('rejects insecure connection', async function () {
262 ctx.clientProtocol = 'http';
263 try {
264 await authenticator.required(req, res, ctx);
265 assert.fail(noExpectedException);
266 } catch (e) {
267 assert(e instanceof Errors.ResponseError);
268 assert.strictEqual(e.statusCode, Enum.ErrorResponse.Forbidden.statusCode);
269 }
270 });
271 it('rejects invalid auth', async function () {
272 try {
273 req.getHeader.returns('auth header');
274 sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
275 await authenticator.required(req, res, ctx);
276 assert.fail(noExpectedException);
277 } catch (e) {
278 assert(e instanceof Errors.ResponseError);
279 assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
280 }
281 });
282 it('redirects without any auth', async function () {
283 await authenticator.required(req, res, ctx);
284 assert(res.end.called);
285 assert(res.setHeader.called);
286 });
287 }); // required
288
289 describe('requiredLocal', function () {
290 let req, res;
291 beforeEach(function () {
292 ctx.clientProtocol = 'https';
293 req = {
294 getHeader: sinon.stub(),
295 };
296 res = {
297 end: sinon.stub(),
298 setHeader: sinon.stub(),
299 }
300 });
301 it('succeeds', async function() {
302 req.getHeader.returns('auth header');
303 sinon.stub(authenticator, 'isValidAuthorization').resolves(true);
304 const result = await authenticator.requiredLocal(req, res, ctx);
305 assert.strictEqual(result, true);
306 });
307 it('covers valid cookie session', async function () {
308 req.getHeader.returns('WSHas=sessionCookie');
309 sinon.stub(authenticator, 'isValidCookieAuth').resolves(true);
310 ctx.session = {
311 authenticatedIdentifier: identifier,
312 };
313 const result = await authenticator.requiredLocal(req, res, ctx);
314 assert.strictEqual(result, true);
315 });
316 it('rejects insecure connection', async function () {
317 ctx.clientProtocol = 'http';
318 try {
319 await authenticator.requiredLocal(req, res, ctx);
320 assert.fail(noExpectedException);
321 } catch (e) {
322 assert(e instanceof Errors.ResponseError);
323 assert.strictEqual(e.statusCode, Enum.ErrorResponse.Forbidden.statusCode);
324 }
325 });
326 it('rejects invalid auth', async function () {
327 try {
328 req.getHeader.returns('auth header');
329 sinon.stub(authenticator, 'isValidAuthorization').resolves(false);
330 await authenticator.requiredLocal(req, res, ctx);
331 assert.fail(noExpectedException);
332 } catch (e) {
333 assert(e instanceof Errors.ResponseError);
334 assert.strictEqual(e.statusCode, Enum.ErrorResponse.Unauthorized.statusCode);
335 }
336 });
337 it('redirects without any auth', async function () {
338 await authenticator.requiredLocal(req, res, ctx);
339 assert(res.end.called);
340 assert(res.setHeader.called);
341 });
342 }); // requiredLocal
343
344 }); // Authenticator