n8n/packages/cli/test/integration/mfa/mfa.api.test.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

330 lines
10 KiB
TypeScript
Raw Normal View History

import Container from 'typedi';
import { randomInt, randomString } from 'n8n-workflow';
import { AuthService } from '@/auth/auth.service';
import config from '@/config';
import type { User } from '@/databases/entities/user';
import { AuthUserRepository } from '@/databases/repositories/auth-user.repository';
import { TOTPService } from '@/mfa/totp.service';
import * as testDb from '../shared/test-db';
import * as utils from '../shared/utils';
import { randomValidPassword, uniqueId } from '../shared/random';
import { createUser, createUserWithMfaEnabled } from '../shared/db/users';
jest.mock('@/telemetry');
let owner: User;
const testServer = utils.setupTestServer({
endpointGroups: ['mfa', 'auth', 'me', 'passwordReset'],
});
beforeEach(async () => {
await testDb.truncate(['User']);
owner = await createUser({ role: 'global:owner' });
config.set('userManagement.disabled', false);
});
afterAll(async () => {
await testDb.terminate();
});
describe('Enable MFA setup', () => {
describe('Step one', () => {
test('GET /qr should fail due to unauthenticated user', async () => {
await testServer.authlessAgent.get('/mfa/qr').expect(401);
});
test('GET /qr should reuse secret and recovery codes until setup is complete', async () => {
const firstCall = await testServer.authAgentFor(owner).get('/mfa/qr').expect(200);
const secondCall = await testServer.authAgentFor(owner).get('/mfa/qr').expect(200);
expect(firstCall.body.data.secret).toBe(secondCall.body.data.secret);
expect(firstCall.body.data.recoveryCodes.join('')).toBe(
secondCall.body.data.recoveryCodes.join(''),
);
const token = new TOTPService().generateTOTP(firstCall.body.data.secret);
await testServer.authAgentFor(owner).post('/mfa/disable').send({ token }).expect(200);
const thirdCall = await testServer.authAgentFor(owner).get('/mfa/qr').expect(200);
expect(firstCall.body.data.secret).not.toBe(thirdCall.body.data.secret);
expect(firstCall.body.data.recoveryCodes.join('')).not.toBe(
thirdCall.body.data.recoveryCodes.join(''),
);
});
test('GET /qr should return qr, secret and recovery codes', async () => {
const response = await testServer.authAgentFor(owner).get('/mfa/qr').expect(200);
const { data } = response.body;
expect(data.secret).toBeDefined();
expect(data.qrCode).toBeDefined();
expect(data.recoveryCodes).toBeDefined();
expect(data.recoveryCodes).not.toBeEmptyArray();
expect(data.recoveryCodes.length).toBe(10);
});
});
describe('Step two', () => {
test('POST /verify should fail due to unauthenticated user', async () => {
await testServer.authlessAgent.post('/mfa/verify').expect(401);
});
test('POST /verify should fail due to invalid MFA token', async () => {
await testServer.authAgentFor(owner).post('/mfa/verify').send({ token: '123' }).expect(400);
});
test('POST /verify should fail due to missing token parameter', async () => {
await testServer.authAgentFor(owner).get('/mfa/qr').expect(200);
await testServer.authAgentFor(owner).post('/mfa/verify').send({ token: '' }).expect(400);
});
test('POST /verify should validate MFA token', async () => {
const response = await testServer.authAgentFor(owner).get('/mfa/qr').expect(200);
const { secret } = response.body.data;
const token = new TOTPService().generateTOTP(secret);
await testServer.authAgentFor(owner).post('/mfa/verify').send({ token }).expect(200);
});
});
describe('Step three', () => {
test('POST /enable should fail due to unauthenticated user', async () => {
await testServer.authlessAgent.post('/mfa/enable').expect(401);
});
test('POST /verify should fail due to missing token parameter', async () => {
await testServer.authAgentFor(owner).post('/mfa/verify').send({ token: '' }).expect(400);
});
test('POST /enable should fail due to invalid MFA token', async () => {
await testServer.authAgentFor(owner).get('/mfa/qr').expect(200);
await testServer.authAgentFor(owner).post('/mfa/enable').send({ token: '123' }).expect(400);
});
test('POST /enable should fail due to empty secret and recovery codes', async () => {
await testServer.authAgentFor(owner).post('/mfa/enable').expect(400);
});
test('POST /enable should enable MFA in account', async () => {
const response = await testServer.authAgentFor(owner).get('/mfa/qr').expect(200);
const { secret } = response.body.data;
const token = new TOTPService().generateTOTP(secret);
await testServer.authAgentFor(owner).post('/mfa/verify').send({ token }).expect(200);
await testServer.authAgentFor(owner).post('/mfa/enable').send({ token }).expect(200);
const user = await Container.get(AuthUserRepository).findOneOrFail({
where: {},
});
expect(user.mfaEnabled).toBe(true);
expect(user.mfaRecoveryCodes).toBeDefined();
expect(user.mfaSecret).toBeDefined();
});
});
});
describe('Disable MFA setup', () => {
test('POST /disable should disable login with MFA', async () => {
const { user, rawSecret } = await createUserWithMfaEnabled();
const token = new TOTPService().generateTOTP(rawSecret);
await testServer
.authAgentFor(user)
.post('/mfa/disable')
.send({
token,
})
.expect(200);
const dbUser = await Container.get(AuthUserRepository).findOneOrFail({
where: { id: user.id },
});
expect(dbUser.mfaEnabled).toBe(false);
expect(dbUser.mfaSecret).toBe(null);
expect(dbUser.mfaRecoveryCodes.length).toBe(0);
});
test('POST /disable should fail if invalid token is given', async () => {
const { user } = await createUserWithMfaEnabled();
await testServer
.authAgentFor(user)
.post('/mfa/disable')
.send({
token: 'invalid token',
})
.expect(403);
});
});
describe('Change password with MFA enabled', () => {
test('POST /change-password should fail due to missing MFA token', async () => {
await createUserWithMfaEnabled();
const newPassword = randomValidPassword();
const resetPasswordToken = uniqueId();
await testServer.authlessAgent
.post('/change-password')
.send({ password: newPassword, token: resetPasswordToken })
.expect(404);
});
test('POST /change-password should fail due to invalid MFA token', async () => {
await createUserWithMfaEnabled();
const newPassword = randomValidPassword();
const resetPasswordToken = uniqueId();
await testServer.authlessAgent
.post('/change-password')
.send({
password: newPassword,
token: resetPasswordToken,
mfaToken: randomInt(10),
})
.expect(404);
});
test('POST /change-password should update password', async () => {
const { user, rawSecret } = await createUserWithMfaEnabled();
const newPassword = randomValidPassword();
config.set('userManagement.jwtSecret', randomString(5, 10));
const resetPasswordToken = Container.get(AuthService).generatePasswordResetToken(user);
const mfaToken = new TOTPService().generateTOTP(rawSecret);
await testServer.authlessAgent
.post('/change-password')
.send({
password: newPassword,
token: resetPasswordToken,
mfaToken,
})
.expect(200);
const loginResponse = await testServer
.authAgentFor(user)
.post('/login')
.send({
email: user.email,
password: newPassword,
mfaToken: new TOTPService().generateTOTP(rawSecret),
})
.expect(200);
expect(loginResponse.body).toHaveProperty('data');
});
});
describe('Login', () => {
test('POST /login with email/password should succeed when mfa is disabled', async () => {
const password = randomString(8);
const user = await createUser({ password });
await testServer.authlessAgent.post('/login').send({ email: user.email, password }).expect(200);
});
test('GET /login should not include mfaSecret and mfaRecoveryCodes property in response', async () => {
const response = await testServer.authAgentFor(owner).get('/login').expect(200);
const { data } = response.body;
expect(data.recoveryCodes).not.toBeDefined();
expect(data.mfaSecret).not.toBeDefined();
});
test('POST /login with email/password should fail when mfa is enabled', async () => {
const { user, rawPassword } = await createUserWithMfaEnabled();
await testServer.authlessAgent
.post('/login')
.send({ email: user.email, password: rawPassword })
.expect(401);
});
describe('Login with MFA token', () => {
test('POST /login should fail due to invalid MFA token', async () => {
const { user, rawPassword } = await createUserWithMfaEnabled();
await testServer.authlessAgent
.post('/login')
.send({ email: user.email, password: rawPassword, mfaToken: 'wrongvalue' })
.expect(401);
});
test('POST /login should fail due two MFA step needed', async () => {
const { user, rawPassword } = await createUserWithMfaEnabled();
const response = await testServer.authlessAgent
.post('/login')
.send({ email: user.email, password: rawPassword })
.expect(401);
expect(response.body.code).toBe(998);
});
test('POST /login should succeed with MFA token', async () => {
const { user, rawSecret, rawPassword } = await createUserWithMfaEnabled();
const token = new TOTPService().generateTOTP(rawSecret);
const response = await testServer.authlessAgent
.post('/login')
.send({ email: user.email, password: rawPassword, mfaToken: token })
.expect(200);
const data = response.body.data;
expect(data.mfaEnabled).toBe(true);
});
});
describe('Login with recovery code', () => {
test('POST /login should fail due to invalid MFA recovery code', async () => {
const { user, rawPassword } = await createUserWithMfaEnabled();
await testServer.authlessAgent
.post('/login')
.send({ email: user.email, password: rawPassword, mfaRecoveryCode: 'wrongvalue' })
.expect(401);
});
test('POST /login should succeed with MFA recovery code', async () => {
const { user, rawPassword, rawRecoveryCodes } = await createUserWithMfaEnabled();
const response = await testServer.authlessAgent
.post('/login')
.send({ email: user.email, password: rawPassword, mfaRecoveryCode: rawRecoveryCodes[0] })
.expect(200);
const data = response.body.data;
expect(data.mfaEnabled).toBe(true);
const dbUser = await Container.get(AuthUserRepository).findOneOrFail({
where: { id: user.id },
});
// Make sure the recovery code used was removed
expect(dbUser.mfaRecoveryCodes.length).toBe(rawRecoveryCodes.length - 1);
expect(dbUser.mfaRecoveryCodes.includes(rawRecoveryCodes[0])).toBe(false);
});
});
});