Skip to content

Commit 37fa181

Browse files
added tests for origin config changes
1 parent 4f38d2a commit 37fa181

File tree

5 files changed

+354
-11
lines changed

5 files changed

+354
-11
lines changed

x-pack/platform/plugins/shared/security/public/authentication/login/components/login_form/login_form.test.tsx

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { coreMock } from '@kbn/core/public/mocks';
1515
import { findTestSubject, mountWithIntl, nextTick, shallowWithIntl } from '@kbn/test-jest-helpers';
1616

1717
import { LoginForm, MessageType, PageMode } from './login_form';
18+
import { i18n } from '@kbn/i18n';
1819

1920
function expectPageMode(wrapper: ReactWrapper, mode: PageMode) {
2021
const assertions: Array<[string, boolean]> =
@@ -398,6 +399,137 @@ describe('LoginForm', () => {
398399
]);
399400
});
400401

402+
it('does not render providers with origin configs that to not match current page', async () => {
403+
const currentURL = `https://some-host.com/login?next=${encodeURIComponent(
404+
'/some-base-path/app/kibana#/home?_g=()'
405+
)}`;
406+
407+
const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' });
408+
409+
window.location = { ...window.location, href: currentURL, origin: 'https://some-host.com' };
410+
const wrapper = mountWithIntl(
411+
<EuiProvider>
412+
<LoginForm
413+
http={coreStartMock.http}
414+
notifications={coreStartMock.notifications}
415+
loginAssistanceMessage=""
416+
selector={{
417+
enabled: true,
418+
providers: [
419+
{
420+
type: 'basic',
421+
name: 'basic',
422+
usesLoginForm: true,
423+
hint: 'Basic hint',
424+
icon: 'logoElastic',
425+
showInSelector: true,
426+
},
427+
{
428+
type: 'saml',
429+
name: 'saml1',
430+
description: 'Log in w/SAML',
431+
origin: ['https://some-host.com', 'https://some-other-host.com'],
432+
usesLoginForm: false,
433+
showInSelector: true,
434+
},
435+
{
436+
type: 'pki',
437+
name: 'pki1',
438+
description: 'Log in w/PKI',
439+
hint: 'PKI hint',
440+
origin: 'https://not-some-host.com',
441+
usesLoginForm: false,
442+
showInSelector: true,
443+
},
444+
],
445+
}}
446+
/>
447+
</EuiProvider>
448+
);
449+
450+
expect(window.location.origin).toBe('https://some-host.com');
451+
452+
expectPageMode(wrapper, PageMode.Selector);
453+
454+
const result = findTestSubject(wrapper, 'loginCard-', '^=').map((card) => {
455+
const hint = findTestSubject(card, 'card-hint');
456+
return {
457+
title: findTestSubject(card, 'card-title').text(),
458+
hint: hint.exists() ? hint.text() : '',
459+
icon: card.find(EuiIcon).props().type,
460+
};
461+
});
462+
463+
expect(result).toEqual([
464+
{ title: 'Log in with basic/basic', hint: 'Basic hint', icon: 'logoElastic' },
465+
{ title: 'Log in w/SAML', hint: '', icon: 'empty' },
466+
]);
467+
});
468+
469+
it('does not render any providers and shows error message if no providers match current origin', async () => {
470+
const currentURL = `https://some-host.com/login?next=${encodeURIComponent(
471+
'/some-base-path/app/kibana#/home?_g=()'
472+
)}`;
473+
474+
const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' });
475+
476+
window.location = { ...window.location, href: currentURL, origin: 'https://some-host.com' };
477+
const wrapper = mountWithIntl(
478+
<EuiProvider>
479+
<LoginForm
480+
http={coreStartMock.http}
481+
notifications={coreStartMock.notifications}
482+
loginAssistanceMessage=""
483+
selector={{
484+
enabled: true,
485+
providers: [
486+
{
487+
type: 'basic',
488+
name: 'basic',
489+
usesLoginForm: true,
490+
hint: 'Basic hint',
491+
icon: 'logoElastic',
492+
origin: 'https://not-some-host.com',
493+
showInSelector: true,
494+
},
495+
{
496+
type: 'saml',
497+
name: 'saml1',
498+
description: 'Log in w/SAML',
499+
origin: ['https://not-some-host.com', 'https://not-some-other-host.com'],
500+
usesLoginForm: false,
501+
showInSelector: true,
502+
},
503+
{
504+
type: 'pki',
505+
name: 'pki1',
506+
description: 'Log in w/PKI',
507+
hint: 'PKI hint',
508+
origin: 'https://not-some-host.com',
509+
usesLoginForm: false,
510+
showInSelector: true,
511+
},
512+
],
513+
}}
514+
/>
515+
</EuiProvider>
516+
);
517+
518+
expect(window.location.origin).toBe('https://some-host.com');
519+
520+
expect(findTestSubject(wrapper, 'loginForm').exists()).toBe(false);
521+
expect(findTestSubject(wrapper, 'loginSelector').exists()).toBe(false);
522+
expect(findTestSubject(wrapper, 'loginHelp').exists()).toBe(false);
523+
expect(findTestSubject(wrapper, 'autoLoginOverlay').exists()).toBe(false);
524+
expect(findTestSubject(wrapper, 'loginCard-', '^=').exists()).toBe(false);
525+
526+
expect(findTestSubject(wrapper, 'loginErrorMessage').text()).toEqual(
527+
i18n.translate('xpack.security.noAuthProvidersForDomain', {
528+
defaultMessage: 'No authentication providers have been configured for this domain.',
529+
})
530+
);
531+
});
532+
401533
it('properly redirects after successful login', async () => {
402534
const currentURL = `https://some-host/login?next=${encodeURIComponent(
403535
'/some-base-path/app/kibana#/home?_g=()'

x-pack/platform/plugins/shared/security/public/authentication/login/components/login_form/login_form.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ const assistanceCss = (theme: UseEuiTheme) => css`
130130
}
131131
`;
132132

133+
const noProvidersMessage = i18n.translate('xpack.security.noAuthProvidersForDomain', {
134+
defaultMessage: 'No authentication providers have been configured for this domain.',
135+
});
136+
133137
export class LoginForm extends Component<LoginFormProps, State> {
134138
private readonly validator: LoginValidator;
135139

@@ -173,9 +177,7 @@ export class LoginForm extends Component<LoginFormProps, State> {
173177
(this.availableProviders.length === 0
174178
? {
175179
type: MessageType.Danger,
176-
content: i18n.translate('xpack.security.noAuthProvidersForDomain', {
177-
defaultMessage: 'No authentication providers have been configured for this domain.',
178-
}),
180+
content: noProvidersMessage,
179181
}
180182
: { type: MessageType.None }),
181183
mode,

x-pack/platform/plugins/shared/security/server/authentication/authenticator.test.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,6 +1376,216 @@ describe('Authenticator', () => {
13761376
);
13771377
});
13781378
});
1379+
1380+
describe('with origin config', () => {
1381+
const headersWithOrigin = { authorization: 'Basic .....', origin: 'http://localhost:5601' };
1382+
1383+
const request = httpServerMock.createKibanaRequest({ headers: headersWithOrigin });
1384+
const user = mockAuthenticatedUser();
1385+
1386+
beforeEach(() => {
1387+
mockOptions.session.create.mockResolvedValue(mockSessVal);
1388+
1389+
mockBasicAuthenticationProvider.login.mockResolvedValue(
1390+
AuthenticationResult.succeeded(user, {
1391+
authHeaders: headersWithOrigin,
1392+
state: {}, // to ensure a new session is created
1393+
})
1394+
);
1395+
});
1396+
1397+
it('allows requests with matching origin header', async () => {
1398+
jest
1399+
.requireMock('./providers/basic')
1400+
.BasicAuthenticationProvider.mockImplementation(() => ({
1401+
type: 'basic',
1402+
origin: 'http://localhost:5601',
1403+
...mockBasicAuthenticationProvider,
1404+
}));
1405+
1406+
authenticator = new Authenticator(
1407+
getMockOptions({
1408+
providers: {
1409+
basic: { basic1: { order: 0 } },
1410+
},
1411+
})
1412+
);
1413+
1414+
await expect(
1415+
authenticator.login(request, {
1416+
provider: { type: 'basic', name: 'basic1' },
1417+
value: {},
1418+
})
1419+
).resolves.toEqual(
1420+
AuthenticationResult.succeeded(user, {
1421+
authHeaders: headersWithOrigin,
1422+
state: {},
1423+
})
1424+
);
1425+
expectAuditEvents({ action: 'user_login', outcome: 'success' });
1426+
expect(mockBasicAuthenticationProvider.login).toHaveBeenCalled();
1427+
});
1428+
1429+
it('allows requests without an origin header', async () => {
1430+
jest
1431+
.requireMock('./providers/basic')
1432+
.BasicAuthenticationProvider.mockImplementation(() => ({
1433+
type: 'basic',
1434+
origin: 'http://localhost:5601',
1435+
...mockBasicAuthenticationProvider,
1436+
}));
1437+
1438+
authenticator = new Authenticator(
1439+
getMockOptions({
1440+
providers: {
1441+
basic: { basic1: { order: 0 } },
1442+
},
1443+
})
1444+
);
1445+
1446+
await expect(
1447+
authenticator.login(httpServerMock.createKibanaRequest(), {
1448+
provider: { type: 'basic', name: 'basic1' },
1449+
value: {},
1450+
})
1451+
).resolves.toEqual(
1452+
AuthenticationResult.succeeded(user, {
1453+
authHeaders: headersWithOrigin,
1454+
state: {},
1455+
})
1456+
);
1457+
expectAuditEvents({ action: 'user_login', outcome: 'success' });
1458+
expect(mockBasicAuthenticationProvider.login).toHaveBeenCalled();
1459+
});
1460+
1461+
it('does not attempt to login for requests with non-matching origin header', async () => {
1462+
jest
1463+
.requireMock('./providers/basic')
1464+
.BasicAuthenticationProvider.mockImplementation(() => ({
1465+
type: 'basic',
1466+
origin: 'http://127.0.0.1:5601',
1467+
...mockBasicAuthenticationProvider,
1468+
}));
1469+
1470+
jest.requireMock('./providers/http').HTTPAuthenticationProvider.mockImplementation(() => ({
1471+
type: 'http',
1472+
origin: 'http://127.0.0.1:5601',
1473+
...mockHTTPAuthenticationProvider,
1474+
}));
1475+
1476+
const mockSamlAuthenticationProvider = jest
1477+
.requireMock('./providers/saml')
1478+
.SAMLAuthenticationProvider.mockImplementation(() => ({
1479+
type: 'saml',
1480+
origin: 'http://127.0.0.1:5601',
1481+
login: jest.fn(),
1482+
authenticate: jest.fn(),
1483+
logout: jest.fn(),
1484+
getHTTPAuthenticationScheme: jest.fn(),
1485+
}));
1486+
1487+
authenticator = new Authenticator(
1488+
getMockOptions({
1489+
providers: {
1490+
basic: { basic1: { order: 0 } },
1491+
saml: { saml1: { order: 1, realm: 'saml1' } },
1492+
},
1493+
})
1494+
);
1495+
1496+
await expect(
1497+
authenticator.login(request, {
1498+
provider: { type: 'basic', name: 'basic1' },
1499+
value: {},
1500+
})
1501+
).resolves.toEqual(AuthenticationResult.notHandled());
1502+
1503+
await expect(
1504+
authenticator.login(request, {
1505+
provider: { type: 'http' },
1506+
value: {},
1507+
})
1508+
).resolves.toEqual(AuthenticationResult.notHandled());
1509+
1510+
await expect(
1511+
authenticator.login(request, {
1512+
provider: { type: 'saml', name: 'saml1' },
1513+
value: {},
1514+
})
1515+
).resolves.toEqual(AuthenticationResult.notHandled());
1516+
1517+
expect(auditLogger.log).not.toHaveBeenCalled();
1518+
expect(mockBasicAuthenticationProvider.login).not.toHaveBeenCalled();
1519+
expect(mockHTTPAuthenticationProvider.login).not.toHaveBeenCalled();
1520+
expect(mockSamlAuthenticationProvider.login).not.toHaveBeenCalled();
1521+
});
1522+
1523+
it('skips over providers that do not match the origin config', async () => {
1524+
const mockSAMLAuthenticationProvider1: jest.Mocked<
1525+
PublicMethodsOf<SAMLAuthenticationProvider>
1526+
> = {
1527+
login: jest.fn(),
1528+
authenticate: jest.fn(),
1529+
logout: jest.fn(),
1530+
getHTTPAuthenticationScheme: jest.fn(),
1531+
};
1532+
1533+
const mockSAMLAuthenticationProvider2: jest.Mocked<
1534+
PublicMethodsOf<SAMLAuthenticationProvider>
1535+
> = {
1536+
login: jest.fn().mockResolvedValue(
1537+
AuthenticationResult.succeeded(user, {
1538+
authHeaders: headersWithOrigin,
1539+
state: {}, // to ensure a new session is created
1540+
})
1541+
),
1542+
authenticate: jest.fn(),
1543+
logout: jest.fn(),
1544+
getHTTPAuthenticationScheme: jest.fn(),
1545+
};
1546+
1547+
jest
1548+
.requireMock('./providers/saml')
1549+
.SAMLAuthenticationProvider.mockImplementationOnce(() => ({
1550+
type: 'saml',
1551+
origin: 'http://127.0.0.1:5601',
1552+
name: 'saml1',
1553+
order: 0,
1554+
...mockSAMLAuthenticationProvider1,
1555+
}))
1556+
.mockImplementationOnce(() => ({
1557+
type: 'saml',
1558+
origin: ['http://localhost:5601', 'http://127.0.0.1:5601'],
1559+
name: 'saml2',
1560+
order: 1,
1561+
...mockSAMLAuthenticationProvider2,
1562+
}));
1563+
1564+
authenticator = new Authenticator(
1565+
getMockOptions({
1566+
providers: {
1567+
saml: { saml1: { order: 0, realm: 'saml1' }, saml2: { order: 1, realm: 'saml1' } },
1568+
},
1569+
})
1570+
);
1571+
1572+
await expect(
1573+
authenticator.login(request, {
1574+
provider: { type: 'saml' },
1575+
value: {},
1576+
})
1577+
).resolves.toEqual(
1578+
AuthenticationResult.succeeded(user, {
1579+
authHeaders: headersWithOrigin,
1580+
state: {},
1581+
})
1582+
);
1583+
1584+
expectAuditEvents({ action: 'user_login', outcome: 'success' });
1585+
expect(mockSAMLAuthenticationProvider1.login).not.toHaveBeenCalled();
1586+
expect(mockSAMLAuthenticationProvider2.login).toHaveBeenCalled();
1587+
});
1588+
});
13791589
});
13801590

13811591
describe('`authenticate` method', () => {

x-pack/platform/plugins/shared/security/server/authentication/authenticator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ export class Authenticator {
340340
const { origin: originHeader } = request.headers;
341341

342342
const filteredProviders = providers.filter(([name, provider]) => {
343-
const providerOrigin = provider.getOrigin();
343+
const providerOrigin = provider.origin;
344344

345345
return (
346346
!originHeader ||

0 commit comments

Comments
 (0)