UNPKG

apostrophe

Version:
680 lines (580 loc) • 16.5 kB
const t = require('../test-lib/test.js'); const assert = require('assert'); describe('Login Requirements', function() { let apos; const extraSecretErr = 'extra secret incorrect'; const captchaErr = 'captcha code incorrect'; const uponSubmitErr = 'uponSubmit incorrect'; this.timeout(20000); this.beforeEach(async function() { if (apos && apos.modules && apos.modules['@apostrophecms/login']) { const loginModule = apos.modules['@apostrophecms/login']; await loginModule.clearLoginAttempts('HarryPutter'); } }); after(function() { return t.destroy(apos); }); // EXISTENCE it('should initialize', async function() { apos = await t.create({ root: module, modules: { '@apostrophecms/login': { requirements(self) { return { add: { WeakCaptcha: { phase: 'beforeSubmit', async props(req) { return { hint: 'xyz' }; }, async verify(req, data) { if (data !== 'xyz') { throw self.apos.error('invalid', captchaErr); } } }, UponSubmit: { phase: 'uponSubmit', async props(req) { return { hint: 'abc' }; }, async verify(req, data) { if (data !== 'abc') { throw self.apos.error('invalid', uponSubmitErr); } } }, ExtraSecret: { phase: 'afterPasswordVerified', async props(req, user) { return { // Verify we had access to the user here hint: user.username }; }, async verify(req, data, user) { if (data !== user.extraSecret) { throw self.apos.error('invalid', extraSecretErr); } } } } }; } } } }); assert(apos.modules['@apostrophecms/login']); }); it('should be able to insert test user', async function() { assert(apos.user.newInstance); const user = apos.user.newInstance(); assert(user); user.title = 'Harry Putter'; user.username = 'HarryPutter'; user.password = 'crookshanks'; user.email = 'hputter@aol.com'; user.role = 'admin'; user.extraSecret = 'roll-on'; assert(user.type === '@apostrophecms/user'); assert(apos.user.insert); const doc = await apos.user.insert(apos.task.getReq(), user); assert(doc._id); }); it('should not be able to login a user without meeting a beforeSubmit requirement', async function() { const jar = apos.http.jar(); // establish session let page = await apos.http.get( '/', { jar } ); assert(page.match(/logged out/)); const context = await apos.http.post( '/api/v1/@apostrophecms/login/context', { method: 'POST', body: {}, jar } ); assert(context.requirementProps); assert(context.requirementProps.WeakCaptcha); assert.strictEqual(context.requirementProps.WeakCaptcha.hint, 'xyz'); try { await apos.http.post( '/api/v1/@apostrophecms/login/login', { method: 'POST', body: { username: 'HarryPutter', password: 'crookshanks', session: true, requirements: { UponSubmit: 'abc' } }, jar } ); assert(false); } catch (e) { assert(e.status === 400); assert.strictEqual(e.body.message, captchaErr); assert.strictEqual(e.body.data.requirement, 'WeakCaptcha'); } // Make sure it really didn't work page = await apos.http.get( '/', { jar } ); assert(page.match(/logged out/)); }); it('should not be able to login a user with the wrong value for a beforeSubmit requirement', async function() { const jar = apos.http.jar(); // establish session let page = await apos.http.get( '/', { jar } ); assert(page.match(/logged out/)); try { await apos.http.post( '/api/v1/@apostrophecms/login/login', { method: 'POST', body: { username: 'HarryPutter', password: 'crookshanks', session: true, requirements: { WeakCaptcha: 'abc', UponSubmit: 'abc' } }, jar } ); assert(false); } catch (e) { assert(e.status === 400); assert.strictEqual(e.body.message, captchaErr); assert.strictEqual(e.body.data.requirement, 'WeakCaptcha'); } // Make sure it really didn't work page = await apos.http.get( '/', { jar } ); assert(page.match(/logged out/)); }); it('should not be able to login a user without meeting an uponSubmit requirement', async function() { const jar = apos.http.jar(); // establish session let page = await apos.http.get( '/', { jar } ); assert(page.match(/logged out/)); const context = await apos.http.post( '/api/v1/@apostrophecms/login/context', { method: 'POST', body: {}, jar } ); assert(context.requirementProps); assert(context.requirementProps.UponSubmit); assert(context.requirementProps.WeakCaptcha); assert.strictEqual(context.requirementProps.UponSubmit.hint, 'abc'); try { await apos.http.post( '/api/v1/@apostrophecms/login/login', { method: 'POST', body: { username: 'HarryPutter', password: 'crookshanks', session: true, requirements: { WeakCaptcha: 'xyz' } }, jar } ); assert(false); } catch (e) { assert(e.status === 400); assert.strictEqual(e.body.message, uponSubmitErr); assert.strictEqual(e.body.data.requirement, 'UponSubmit'); } // Make sure it really didn't work page = await apos.http.get( '/', { jar } ); assert(page.match(/logged out/)); }); it('should not be able to login a user with the wrong value for a uponSubmit requirement', async function() { const jar = apos.http.jar(); // establish session let page = await apos.http.get( '/', { jar } ); assert(page.match(/logged out/)); try { await apos.http.post( '/api/v1/@apostrophecms/login/login', { method: 'POST', body: { username: 'HarryPutter', password: 'crookshanks', session: true, requirements: { WeakCaptcha: 'xyz', UponSubmit: 'xyz' } }, jar } ); assert(false); } catch (e) { assert(e.status === 400); assert.strictEqual(e.body.message, uponSubmitErr); assert.strictEqual(e.body.data.requirement, 'UponSubmit'); } // Make sure it really didn't work page = await apos.http.get( '/', { jar } ); assert(page.match(/logged out/)); }); it('should throttle requirements verify attemps and show a proper error when the limit is reached', async function () { const loginModule = apos.modules['@apostrophecms/login']; const { allowedAttempts } = loginModule.options.throttle; const namespace = '@apostrophecms/loginAttempt/ExtraSecret'; const jar = apos.http.jar(); // establish session let page = await apos.http.get( '/', { jar } ); assert(page.match(/logged out/)); const result = await apos.http.post( '/api/v1/@apostrophecms/login/login', { method: 'POST', body: { username: 'HarryPutter', password: 'crookshanks', session: true, requirements: { WeakCaptcha: 'xyz', UponSubmit: 'abc' } }, jar } ); assert(result.incompleteToken); // Make sure it did not create a login session prematurely page = await apos.http.get( '/', { jar } ); assert(page.match(/logged out/)); const token = result.incompleteToken; for (let index = 0; index <= allowedAttempts; index++) { try { await apos.http.post('/api/v1/@apostrophecms/login/requirement-verify', { body: { incompleteToken: token, session: true, name: 'ExtraSecret', value: 'roll-off' }, jar }); } catch ({ status, body }) { if (index < allowedAttempts) { assert(body.message === extraSecretErr); } else { assert(body.message === 'Too many attempts. You may try again in a minute.'); } } } await loginModule.clearLoginAttempts('HarryPutter', namespace); }); it('initial login should produce an incompleteToken, convertible with the afterPasswordVerified requirements', async function() { const jar = apos.http.jar(); // establish session let page = await apos.http.get( '/', { jar } ); const result = await apos.http.post( '/api/v1/@apostrophecms/login/login', { method: 'POST', body: { username: 'HarryPutter', password: 'crookshanks', session: true, requirements: { WeakCaptcha: 'xyz', UponSubmit: 'abc' } }, jar } ); assert(result.incompleteToken); // Make sure it did not create a login session prematurely page = await apos.http.get( '/', { jar } ); assert(page.match(/logged out/)); const token = result.incompleteToken; // Make sure we can't use an incomplete token as a bearer token await assert.rejects(tryAsBearerToken); // Make sure it won't convert with an incorrect ExtraSecret try { await apos.http.post('/api/v1/@apostrophecms/login/requirement-verify', { body: { incompleteToken: token, session: true, name: 'ExtraSecret', value: 'roll-off' }, jar }); // Getting here is bad assert(false); } catch ({ status, body }) { assert(status === 400); assert.strictEqual(body.message, extraSecretErr); assert.strictEqual(body.data.requirement, 'ExtraSecret'); } // Make sure a bad conversion attempt doesn't unlock it as a bearer token either await assert.rejects(tryAsBearerToken); // If we try the final login without // having successfully verified all requirements we get an error try { await apos.http.post( '/api/v1/@apostrophecms/login/login', { method: 'POST', body: { incompleteToken: token, session: true }, jar } ); } catch ({ status, body }) { assert(status === 403); assert.strictEqual(body.message, 'All requirements must be verified'); } // Make sure it did not create a login session prematurely page = await apos.http.get( '/', { jar } ); assert(page.match(/logged out/)); // Fetch props for afterPasswordVerified component const props = await apos.http.post( '/api/v1/@apostrophecms/login/requirement-props', { method: 'POST', body: { incompleteToken: token, name: 'ExtraSecret' }, jar } ); assert.strictEqual(props.hint, 'HarryPutter'); // Now convert token to an actual login session // by providing the post-password-verification requirements, // correctly await apos.http.post('/api/v1/@apostrophecms/login/requirement-verify', { body: { incompleteToken: token, session: true, name: 'ExtraSecret', value: 'roll-on' }, jar }); // Only now should we be able to use it as a bearer token await tryAsBearerToken(); // Complete the cookie-based session login process await apos.http.post( '/api/v1/@apostrophecms/login/login', { method: 'POST', body: { incompleteToken: token, session: true }, jar } ); page = await apos.http.get( '/', { jar } ); assert(page.match(/logged in/)); async function tryAsBearerToken() { await apos.http.get('/api/v1/@apostrophecms/page', { headers: { authorization: `Bearer ${token}` } }); } }); }); describe('Expired Token Deletion', function() { let apos; const extraSecretErr = 'extra secret incorrect'; this.timeout(20000); this.beforeEach(async function() { if (apos && apos.modules && apos.modules['@apostrophecms/login']) { const loginModule = apos.modules['@apostrophecms/login']; await loginModule.clearLoginAttempts('HarryPutter'); } }); after(function() { return t.destroy(apos); }); // EXISTENCE it('should initialize', async function() { apos = await t.create({ root: module, modules: { '@apostrophecms/login': { options: { incompleteLifetime: 5000 }, requirements(self) { return { add: { // Need an extra requirement so that the token will die // after incompleteLifetime ExtraSecret: { phase: 'afterPasswordVerified', async props(req, user) { return { // Verify we had access to the user here hint: user.username }; }, async verify(req, data, user) { if (data !== user.extraSecret) { throw self.apos.error('invalid', extraSecretErr); } } } } }; } } } }); assert(apos.modules['@apostrophecms/login']); }); it('should be able to insert test user', async function() { assert(apos.user.newInstance); const user = apos.user.newInstance(); assert(user); user.title = 'Harry Putter'; user.username = 'HarryPutter'; user.password = 'crookshanks'; user.email = 'hputter@aol.com'; user.role = 'admin'; user.extraSecret = 'roll-on'; assert(user.type === '@apostrophecms/user'); assert(apos.user.insert); const doc = await apos.user.insert(apos.task.getReq(), user); assert(doc._id); }); it('initial login should produce an incompleteToken, convertible with the afterPasswordVerified requirements', async function() { const jar = apos.http.jar(); // establish session let page = await apos.http.get( '/', { jar } ); const result = await apos.http.post( '/api/v1/@apostrophecms/login/login', { method: 'POST', body: { username: 'HarryPutter', password: 'crookshanks', session: true, requirements: { WeakCaptcha: 'xyz', UponSubmit: 'abc' } }, jar } ); const token = result.incompleteToken; assert(token); // Verify it initially exists assert(await apos.login.bearerTokens.findOne({ _id: token })); // Wait until well over 5 seconds have passed to allow the cleanup interval to run await delay(10000); // Verify it is gone from the db assert(!(await apos.login.bearerTokens.findOne({ _id: token }))); }); }); function delay(ms) { return new Promise((resolve, reject) => { setTimeout(() => resolve(), ms); }); }