2023-04-12 01:59:14 -07:00
|
|
|
import { Container } from 'typedi';
|
2023-07-13 01:14:48 -07:00
|
|
|
import type { AuthenticationMethod } from 'n8n-workflow';
|
2024-05-31 00:40:03 -07:00
|
|
|
|
2023-03-17 09:24:05 -07:00
|
|
|
import type { User } from '@db/entities/User';
|
|
|
|
import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers';
|
2023-03-24 09:46:06 -07:00
|
|
|
import { getCurrentAuthenticationMethod, setCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
|
2023-04-28 09:11:33 -07:00
|
|
|
import { InternalHooks } from '@/InternalHooks';
|
|
|
|
import { SamlService } from '@/sso/saml/saml.service.ee';
|
2023-05-02 01:37:19 -07:00
|
|
|
import type { SamlUserAttributes } from '@/sso/saml/types/samlUserAttributes';
|
2023-07-13 01:14:48 -07:00
|
|
|
|
|
|
|
import { randomEmail, randomName, randomValidPassword } from '../shared/random';
|
|
|
|
import * as utils from '../shared/utils/';
|
|
|
|
import { sampleConfig } from './sampleMetadata';
|
2023-11-08 07:29:39 -08:00
|
|
|
import { createOwner, createUser } from '../shared/db/users';
|
2024-05-31 00:40:03 -07:00
|
|
|
import type { SuperAgentTest } from '../shared/types';
|
2023-03-03 01:05:30 -08:00
|
|
|
|
2023-04-28 09:11:33 -07:00
|
|
|
let someUser: User;
|
2023-03-17 09:24:05 -07:00
|
|
|
let owner: User;
|
2023-04-28 09:11:33 -07:00
|
|
|
let authMemberAgent: SuperAgentTest;
|
2023-03-17 09:24:05 -07:00
|
|
|
let authOwnerAgent: SuperAgentTest;
|
2023-03-03 01:05:30 -08:00
|
|
|
|
2023-03-23 08:54:35 -07:00
|
|
|
async function enableSaml(enable: boolean) {
|
|
|
|
await setSamlLoginEnabled(enable);
|
2023-03-03 01:05:30 -08:00
|
|
|
}
|
|
|
|
|
2023-07-13 01:14:48 -07:00
|
|
|
const testServer = utils.setupTestServer({
|
|
|
|
endpointGroups: ['me', 'saml'],
|
|
|
|
enabledFeatures: ['feat:saml'],
|
|
|
|
});
|
|
|
|
|
2023-03-03 01:05:30 -08:00
|
|
|
beforeAll(async () => {
|
2023-11-08 07:29:39 -08:00
|
|
|
owner = await createOwner();
|
|
|
|
someUser = await createUser();
|
2023-07-13 01:14:48 -07:00
|
|
|
authOwnerAgent = testServer.authAgentFor(owner);
|
|
|
|
authMemberAgent = testServer.authAgentFor(someUser);
|
2023-03-03 01:05:30 -08:00
|
|
|
});
|
|
|
|
|
2023-03-16 07:34:28 -07:00
|
|
|
describe('Instance owner', () => {
|
2023-03-17 09:24:05 -07:00
|
|
|
describe('PATCH /me', () => {
|
|
|
|
test('should succeed with valid inputs', async () => {
|
2023-03-23 08:54:35 -07:00
|
|
|
await enableSaml(false);
|
2023-03-17 09:24:05 -07:00
|
|
|
await authOwnerAgent
|
|
|
|
.patch('/me')
|
|
|
|
.send({
|
|
|
|
email: randomEmail(),
|
|
|
|
firstName: randomName(),
|
|
|
|
lastName: randomName(),
|
|
|
|
password: randomValidPassword(),
|
|
|
|
})
|
|
|
|
.expect(200);
|
2023-03-03 01:05:30 -08:00
|
|
|
});
|
|
|
|
|
2023-03-17 09:24:05 -07:00
|
|
|
test('should throw BadRequestError if email is changed when SAML is enabled', async () => {
|
2023-03-23 08:54:35 -07:00
|
|
|
await enableSaml(true);
|
2023-03-17 09:24:05 -07:00
|
|
|
await authOwnerAgent
|
|
|
|
.patch('/me')
|
|
|
|
.send({
|
|
|
|
email: randomEmail(),
|
|
|
|
firstName: randomName(),
|
|
|
|
lastName: randomName(),
|
|
|
|
})
|
|
|
|
.expect(400, { code: 400, message: 'SAML user may not change their email' });
|
2023-03-03 01:05:30 -08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-03-17 09:24:05 -07:00
|
|
|
describe('PATCH /password', () => {
|
|
|
|
test('should throw BadRequestError if password is changed when SAML is enabled', async () => {
|
2023-03-23 08:54:35 -07:00
|
|
|
await enableSaml(true);
|
2023-03-17 09:24:05 -07:00
|
|
|
await authOwnerAgent
|
|
|
|
.patch('/me/password')
|
|
|
|
.send({
|
|
|
|
password: randomValidPassword(),
|
|
|
|
})
|
|
|
|
.expect(400, {
|
|
|
|
code: 400,
|
|
|
|
message: 'With SAML enabled, users need to use their SAML provider to change passwords',
|
|
|
|
});
|
2023-03-03 01:05:30 -08:00
|
|
|
});
|
|
|
|
});
|
2023-03-24 09:46:06 -07:00
|
|
|
|
|
|
|
describe('POST /sso/saml/config', () => {
|
|
|
|
test('should post saml config', async () => {
|
|
|
|
await authOwnerAgent
|
|
|
|
.post('/sso/saml/config')
|
|
|
|
.send({
|
|
|
|
...sampleConfig,
|
|
|
|
loginEnabled: true,
|
|
|
|
})
|
|
|
|
.expect(200);
|
|
|
|
expect(getCurrentAuthenticationMethod()).toBe('saml');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('POST /sso/saml/config/toggle', () => {
|
|
|
|
test('should toggle saml as default authentication method', async () => {
|
|
|
|
await enableSaml(true);
|
|
|
|
expect(getCurrentAuthenticationMethod()).toBe('saml');
|
|
|
|
|
|
|
|
await authOwnerAgent
|
|
|
|
.post('/sso/saml/config/toggle')
|
|
|
|
.send({
|
|
|
|
loginEnabled: false,
|
|
|
|
})
|
|
|
|
.expect(200);
|
|
|
|
expect(getCurrentAuthenticationMethod()).toBe('email');
|
|
|
|
|
|
|
|
await authOwnerAgent
|
|
|
|
.post('/sso/saml/config/toggle')
|
|
|
|
.send({
|
|
|
|
loginEnabled: true,
|
|
|
|
})
|
|
|
|
.expect(200);
|
|
|
|
expect(getCurrentAuthenticationMethod()).toBe('saml');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('POST /sso/saml/config/toggle', () => {
|
|
|
|
test('should fail enable saml if default authentication is not email', async () => {
|
|
|
|
await enableSaml(true);
|
|
|
|
|
|
|
|
await authOwnerAgent
|
|
|
|
.post('/sso/saml/config/toggle')
|
|
|
|
.send({
|
|
|
|
loginEnabled: false,
|
|
|
|
})
|
|
|
|
.expect(200);
|
|
|
|
expect(getCurrentAuthenticationMethod()).toBe('email');
|
|
|
|
|
|
|
|
await setCurrentAuthenticationMethod('ldap');
|
|
|
|
expect(getCurrentAuthenticationMethod()).toBe('ldap');
|
|
|
|
|
|
|
|
await authOwnerAgent
|
|
|
|
.post('/sso/saml/config/toggle')
|
|
|
|
.send({
|
|
|
|
loginEnabled: true,
|
|
|
|
})
|
2023-04-04 05:28:29 -07:00
|
|
|
.expect(500);
|
2023-03-24 09:46:06 -07:00
|
|
|
|
|
|
|
expect(getCurrentAuthenticationMethod()).toBe('ldap');
|
2023-04-28 09:11:33 -07:00
|
|
|
await setCurrentAuthenticationMethod('saml');
|
2023-03-24 09:46:06 -07:00
|
|
|
});
|
|
|
|
});
|
2023-03-03 01:05:30 -08:00
|
|
|
});
|
2023-04-28 09:11:33 -07:00
|
|
|
|
|
|
|
describe('Check endpoint permissions', () => {
|
|
|
|
beforeEach(async () => {
|
|
|
|
await enableSaml(true);
|
|
|
|
});
|
2024-04-10 01:55:49 -07:00
|
|
|
|
2023-04-28 09:11:33 -07:00
|
|
|
describe('Owner', () => {
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access GET /sso/saml/metadata', async () => {
|
|
|
|
await authOwnerAgent.get('/sso/saml/metadata').expect(200);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access GET /sso/saml/config', async () => {
|
|
|
|
await authOwnerAgent.get('/sso/saml/config').expect(200);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access POST /sso/saml/config', async () => {
|
|
|
|
await authOwnerAgent.post('/sso/saml/config').expect(200);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access POST /sso/saml/config/toggle', async () => {
|
|
|
|
await authOwnerAgent.post('/sso/saml/config/toggle').expect(400);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access GET /sso/saml/acs', async () => {
|
2023-04-28 09:11:33 -07:00
|
|
|
// Note that 401 here is coming from the missing SAML object,
|
|
|
|
// not from not being able to access the endpoint, so this is expected!
|
2024-04-10 01:55:49 -07:00
|
|
|
const response = await authOwnerAgent.get('/sso/saml/acs').expect(401);
|
2023-04-28 09:11:33 -07:00
|
|
|
expect(response.text).toContain('SAML Authentication failed');
|
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access POST /sso/saml/acs', async () => {
|
2023-04-28 09:11:33 -07:00
|
|
|
// Note that 401 here is coming from the missing SAML object,
|
|
|
|
// not from not being able to access the endpoint, so this is expected!
|
2024-04-10 01:55:49 -07:00
|
|
|
const response = await authOwnerAgent.post('/sso/saml/acs').expect(401);
|
2023-04-28 09:11:33 -07:00
|
|
|
expect(response.text).toContain('SAML Authentication failed');
|
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access GET /sso/saml/initsso', async () => {
|
|
|
|
await authOwnerAgent.get('/sso/saml/initsso').expect(200);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access GET /sso/saml/config/test', async () => {
|
|
|
|
await authOwnerAgent.get('/sso/saml/config/test').expect(200);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
});
|
2024-04-10 01:55:49 -07:00
|
|
|
|
2023-04-28 09:11:33 -07:00
|
|
|
describe('Authenticated Member', () => {
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access GET /sso/saml/metadata', async () => {
|
|
|
|
await authMemberAgent.get('/sso/saml/metadata').expect(200);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access GET /sso/saml/config', async () => {
|
|
|
|
await authMemberAgent.get('/sso/saml/config').expect(200);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should NOT be able to access POST /sso/saml/config', async () => {
|
|
|
|
await authMemberAgent.post('/sso/saml/config').expect(403);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should NOT be able to access POST /sso/saml/config/toggle', async () => {
|
|
|
|
await authMemberAgent.post('/sso/saml/config/toggle').expect(403);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access GET /sso/saml/acs', async () => {
|
2023-04-28 09:11:33 -07:00
|
|
|
// Note that 401 here is coming from the missing SAML object,
|
|
|
|
// not from not being able to access the endpoint, so this is expected!
|
2024-04-10 01:55:49 -07:00
|
|
|
const response = await authMemberAgent.get('/sso/saml/acs').expect(401);
|
2023-04-28 09:11:33 -07:00
|
|
|
expect(response.text).toContain('SAML Authentication failed');
|
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access POST /sso/saml/acs', async () => {
|
2023-04-28 09:11:33 -07:00
|
|
|
// Note that 401 here is coming from the missing SAML object,
|
|
|
|
// not from not being able to access the endpoint, so this is expected!
|
2024-04-10 01:55:49 -07:00
|
|
|
const response = await authMemberAgent.post('/sso/saml/acs').expect(401);
|
2023-04-28 09:11:33 -07:00
|
|
|
expect(response.text).toContain('SAML Authentication failed');
|
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access GET /sso/saml/initsso', async () => {
|
|
|
|
await authMemberAgent.get('/sso/saml/initsso').expect(200);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should NOT be able to access GET /sso/saml/config/test', async () => {
|
|
|
|
await authMemberAgent.get('/sso/saml/config/test').expect(403);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
});
|
|
|
|
describe('Non-Authenticated User', () => {
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access /sso/saml/metadata', async () => {
|
|
|
|
await testServer.authlessAgent.get('/sso/saml/metadata').expect(200);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should NOT be able to access GET /sso/saml/config', async () => {
|
|
|
|
await testServer.authlessAgent.get('/sso/saml/config').expect(401);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should NOT be able to access POST /sso/saml/config', async () => {
|
|
|
|
await testServer.authlessAgent.post('/sso/saml/config').expect(401);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should NOT be able to access POST /sso/saml/config/toggle', async () => {
|
|
|
|
await testServer.authlessAgent.post('/sso/saml/config/toggle').expect(401);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access GET /sso/saml/acs', async () => {
|
2023-04-28 09:11:33 -07:00
|
|
|
// Note that 401 here is coming from the missing SAML object,
|
|
|
|
// not from not being able to access the endpoint, so this is expected!
|
2024-04-10 01:55:49 -07:00
|
|
|
const response = await testServer.authlessAgent.get('/sso/saml/acs').expect(401);
|
2023-04-28 09:11:33 -07:00
|
|
|
expect(response.text).toContain('SAML Authentication failed');
|
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access POST /sso/saml/acs', async () => {
|
2023-04-28 09:11:33 -07:00
|
|
|
// Note that 401 here is coming from the missing SAML object,
|
|
|
|
// not from not being able to access the endpoint, so this is expected!
|
2024-04-10 01:55:49 -07:00
|
|
|
const response = await testServer.authlessAgent.post('/sso/saml/acs').expect(401);
|
2023-04-28 09:11:33 -07:00
|
|
|
expect(response.text).toContain('SAML Authentication failed');
|
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should be able to access GET /sso/saml/initsso', async () => {
|
|
|
|
await testServer.authlessAgent.get('/sso/saml/initsso').expect(200);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
|
2024-04-10 01:55:49 -07:00
|
|
|
test('should NOT be able to access GET /sso/saml/config/test', async () => {
|
|
|
|
await testServer.authlessAgent.get('/sso/saml/config/test').expect(401);
|
2023-04-28 09:11:33 -07:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('SAML login flow', () => {
|
|
|
|
beforeEach(async () => {
|
|
|
|
await enableSaml(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('should trigger onUserLoginSuccess hook', async () => {
|
|
|
|
const mockedHandleSamlLogin = jest.spyOn(Container.get(SamlService), 'handleSamlLogin');
|
|
|
|
|
|
|
|
mockedHandleSamlLogin.mockImplementation(
|
|
|
|
async (): Promise<{
|
|
|
|
authenticatedUser: User;
|
|
|
|
attributes: SamlUserAttributes;
|
|
|
|
onboardingRequired: false;
|
|
|
|
}> => {
|
|
|
|
return {
|
|
|
|
authenticatedUser: someUser,
|
|
|
|
attributes: {
|
|
|
|
email: someUser.email,
|
|
|
|
firstName: someUser.firstName,
|
|
|
|
lastName: someUser.lastName,
|
|
|
|
userPrincipalName: someUser.email,
|
|
|
|
},
|
|
|
|
onboardingRequired: false,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
const mockedHookOnUserLoginSuccess = jest.spyOn(
|
|
|
|
Container.get(InternalHooks),
|
|
|
|
'onUserLoginSuccess',
|
|
|
|
);
|
|
|
|
mockedHookOnUserLoginSuccess.mockImplementation(
|
|
|
|
async (userLoginData: { user: User; authenticationMethod: AuthenticationMethod }) => {
|
|
|
|
expect(userLoginData.authenticationMethod).toEqual('saml');
|
|
|
|
return;
|
|
|
|
},
|
|
|
|
);
|
2024-04-10 01:55:49 -07:00
|
|
|
await authOwnerAgent.post('/sso/saml/acs').expect(302);
|
2023-04-28 09:11:33 -07:00
|
|
|
expect(mockedHookOnUserLoginSuccess).toBeCalled();
|
|
|
|
mockedHookOnUserLoginSuccess.mockRestore();
|
|
|
|
mockedHandleSamlLogin.mockRestore();
|
|
|
|
});
|
|
|
|
|
|
|
|
test('should trigger onUserLoginFailed hook', async () => {
|
|
|
|
const mockedHandleSamlLogin = jest.spyOn(Container.get(SamlService), 'handleSamlLogin');
|
|
|
|
|
|
|
|
mockedHandleSamlLogin.mockImplementation(
|
|
|
|
async (): Promise<{
|
|
|
|
authenticatedUser: User | undefined;
|
|
|
|
attributes: SamlUserAttributes;
|
|
|
|
onboardingRequired: false;
|
|
|
|
}> => {
|
|
|
|
return {
|
|
|
|
authenticatedUser: undefined,
|
|
|
|
attributes: {
|
|
|
|
email: someUser.email,
|
|
|
|
firstName: someUser.firstName,
|
|
|
|
lastName: someUser.lastName,
|
|
|
|
userPrincipalName: someUser.email,
|
|
|
|
},
|
|
|
|
onboardingRequired: false,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
const mockedHookOnUserLoginFailed = jest.spyOn(
|
|
|
|
Container.get(InternalHooks),
|
|
|
|
'onUserLoginFailed',
|
|
|
|
);
|
|
|
|
mockedHookOnUserLoginFailed.mockImplementation(
|
|
|
|
async (userLoginData: {
|
|
|
|
user: string;
|
|
|
|
authenticationMethod: AuthenticationMethod;
|
|
|
|
reason?: string;
|
|
|
|
}) => {
|
|
|
|
expect(userLoginData.authenticationMethod).toEqual('saml');
|
|
|
|
return;
|
|
|
|
},
|
|
|
|
);
|
2024-04-10 01:55:49 -07:00
|
|
|
await authOwnerAgent.post('/sso/saml/acs').expect(401);
|
2023-04-28 09:11:33 -07:00
|
|
|
expect(mockedHookOnUserLoginFailed).toBeCalled();
|
|
|
|
mockedHookOnUserLoginFailed.mockRestore();
|
|
|
|
mockedHandleSamlLogin.mockRestore();
|
|
|
|
});
|
|
|
|
});
|