Skip to content

Commit 34bb5d3

Browse files
added tests for origin config changes
1 parent feff385 commit 34bb5d3

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
@@ -1459,6 +1459,216 @@ describe('Authenticator', () => {
14591459
);
14601460
});
14611461
});
1462+
1463+
describe('with origin config', () => {
1464+
const headersWithOrigin = { authorization: 'Basic .....', origin: 'http://localhost:5601' };
1465+
1466+
const request = httpServerMock.createKibanaRequest({ headers: headersWithOrigin });
1467+
const user = mockAuthenticatedUser();
1468+
1469+
beforeEach(() => {
1470+
mockOptions.session.create.mockResolvedValue(mockSessVal);
1471+
1472+
mockBasicAuthenticationProvider.login.mockResolvedValue(
1473+
AuthenticationResult.succeeded(user, {
1474+
authHeaders: headersWithOrigin,
1475+
state: {}, // to ensure a new session is created
1476+
})
1477+
);
1478+
});
1479+
1480+
it('allows requests with matching origin header', async () => {
1481+
jest
1482+
.requireMock('./providers/basic')
1483+
.BasicAuthenticationProvider.mockImplementation(() => ({
1484+
type: 'basic',
1485+
origin: 'http://localhost:5601',
1486+
...mockBasicAuthenticationProvider,
1487+
}));
1488+
1489+
authenticator = new Authenticator(
1490+
getMockOptions({
1491+
providers: {
1492+
basic: { basic1: { order: 0 } },
1493+
},
1494+
})
1495+
);
1496+
1497+
await expect(
1498+
authenticator.login(request, {
1499+
provider: { type: 'basic', name: 'basic1' },
1500+
value: {},
1501+
})
1502+
).resolves.toEqual(
1503+
AuthenticationResult.succeeded(user, {
1504+
authHeaders: headersWithOrigin,
1505+
state: {},
1506+
})
1507+
);
1508+
expectAuditEvents({ action: 'user_login', outcome: 'success' });
1509+
expect(mockBasicAuthenticationProvider.login).toHaveBeenCalled();
1510+
});
1511+
1512+
it('allows requests without an origin header', async () => {
1513+
jest
1514+
.requireMock('./providers/basic')
1515+
.BasicAuthenticationProvider.mockImplementation(() => ({
1516+
type: 'basic',
1517+
origin: 'http://localhost:5601',
1518+
...mockBasicAuthenticationProvider,
1519+
}));
1520+
1521+
authenticator = new Authenticator(
1522+
getMockOptions({
1523+
providers: {
1524+
basic: { basic1: { order: 0 } },
1525+
},
1526+
})
1527+
);
1528+
1529+
await expect(
1530+
authenticator.login(httpServerMock.createKibanaRequest(), {
1531+
provider: { type: 'basic', name: 'basic1' },
1532+
value: {},
1533+
})
1534+
).resolves.toEqual(
1535+
AuthenticationResult.succeeded(user, {
1536+
authHeaders: headersWithOrigin,
1537+
state: {},
1538+
})
1539+
);
1540+
expectAuditEvents({ action: 'user_login', outcome: 'success' });
1541+
expect(mockBasicAuthenticationProvider.login).toHaveBeenCalled();
1542+
});
1543+
1544+
it('does not attempt to login for requests with non-matching origin header', async () => {
1545+
jest
1546+
.requireMock('./providers/basic')
1547+
.BasicAuthenticationProvider.mockImplementation(() => ({
1548+
type: 'basic',
1549+
origin: 'http://127.0.0.1:5601',
1550+
...mockBasicAuthenticationProvider,
1551+
}));
1552+
1553+
jest.requireMock('./providers/http').HTTPAuthenticationProvider.mockImplementation(() => ({
1554+
type: 'http',
1555+
origin: 'http://127.0.0.1:5601',
1556+
...mockHTTPAuthenticationProvider,
1557+
}));
1558+
1559+
const mockSamlAuthenticationProvider = jest
1560+
.requireMock('./providers/saml')
1561+
.SAMLAuthenticationProvider.mockImplementation(() => ({
1562+
type: 'saml',
1563+
origin: 'http://127.0.0.1:5601',
1564+
login: jest.fn(),
1565+
authenticate: jest.fn(),
1566+
logout: jest.fn(),
1567+
getHTTPAuthenticationScheme: jest.fn(),
1568+
}));
1569+
1570+
authenticator = new Authenticator(
1571+
getMockOptions({
1572+
providers: {
1573+
basic: { basic1: { order: 0 } },
1574+
saml: { saml1: { order: 1, realm: 'saml1' } },
1575+
},
1576+
})
1577+
);
1578+
1579+
await expect(
1580+
authenticator.login(request, {
1581+
provider: { type: 'basic', name: 'basic1' },
1582+
value: {},
1583+
})
1584+
).resolves.toEqual(AuthenticationResult.notHandled());
1585+
1586+
await expect(
1587+
authenticator.login(request, {
1588+
provider: { type: 'http' },
1589+
value: {},
1590+
})
1591+
).resolves.toEqual(AuthenticationResult.notHandled());
1592+
1593+
await expect(
1594+
authenticator.login(request, {
1595+
provider: { type: 'saml', name: 'saml1' },
1596+
value: {},
1597+
})
1598+
).resolves.toEqual(AuthenticationResult.notHandled());
1599+
1600+
expect(auditLogger.log).not.toHaveBeenCalled();
1601+
expect(mockBasicAuthenticationProvider.login).not.toHaveBeenCalled();
1602+
expect(mockHTTPAuthenticationProvider.login).not.toHaveBeenCalled();
1603+
expect(mockSamlAuthenticationProvider.login).not.toHaveBeenCalled();
1604+
});
1605+
1606+
it('skips over providers that do not match the origin config', async () => {
1607+
const mockSAMLAuthenticationProvider1: jest.Mocked<
1608+
PublicMethodsOf<SAMLAuthenticationProvider>
1609+
> = {
1610+
login: jest.fn(),
1611+
authenticate: jest.fn(),
1612+
logout: jest.fn(),
1613+
getHTTPAuthenticationScheme: jest.fn(),
1614+
};
1615+
1616+
const mockSAMLAuthenticationProvider2: jest.Mocked<
1617+
PublicMethodsOf<SAMLAuthenticationProvider>
1618+
> = {
1619+
login: jest.fn().mockResolvedValue(
1620+
AuthenticationResult.succeeded(user, {
1621+
authHeaders: headersWithOrigin,
1622+
state: {}, // to ensure a new session is created
1623+
})
1624+
),
1625+
authenticate: jest.fn(),
1626+
logout: jest.fn(),
1627+
getHTTPAuthenticationScheme: jest.fn(),
1628+
};
1629+
1630+
jest
1631+
.requireMock('./providers/saml')
1632+
.SAMLAuthenticationProvider.mockImplementationOnce(() => ({
1633+
type: 'saml',
1634+
origin: 'http://127.0.0.1:5601',
1635+
name: 'saml1',
1636+
order: 0,
1637+
...mockSAMLAuthenticationProvider1,
1638+
}))
1639+
.mockImplementationOnce(() => ({
1640+
type: 'saml',
1641+
origin: ['http://localhost:5601', 'http://127.0.0.1:5601'],
1642+
name: 'saml2',
1643+
order: 1,
1644+
...mockSAMLAuthenticationProvider2,
1645+
}));
1646+
1647+
authenticator = new Authenticator(
1648+
getMockOptions({
1649+
providers: {
1650+
saml: { saml1: { order: 0, realm: 'saml1' }, saml2: { order: 1, realm: 'saml1' } },
1651+
},
1652+
})
1653+
);
1654+
1655+
await expect(
1656+
authenticator.login(request, {
1657+
provider: { type: 'saml' },
1658+
value: {},
1659+
})
1660+
).resolves.toEqual(
1661+
AuthenticationResult.succeeded(user, {
1662+
authHeaders: headersWithOrigin,
1663+
state: {},
1664+
})
1665+
);
1666+
1667+
expectAuditEvents({ action: 'user_login', outcome: 'success' });
1668+
expect(mockSAMLAuthenticationProvider1.login).not.toHaveBeenCalled();
1669+
expect(mockSAMLAuthenticationProvider2.login).toHaveBeenCalled();
1670+
});
1671+
});
14621672
});
14631673

14641674
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
@@ -343,7 +343,7 @@ export class Authenticator {
343343
const { origin: originHeader } = request.headers;
344344

345345
const filteredProviders = providers.filter(([name, provider]) => {
346-
const providerOrigin = provider.getOrigin();
346+
const providerOrigin = provider.origin;
347347

348348
return (
349349
!originHeader ||

0 commit comments

Comments
 (0)