Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/reference/configuration-reference/security-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ xpack.security.authc.providers.<provider-type>.<provider-name>.hint ![logo cloud
xpack.security.authc.providers.<provider-type>.<provider-name>.icon ![logo cloud](https://doc-icons.s3.us-east-2.amazonaws.com/logo_cloud.svg "Supported on {{ech}}")
: Custom icon for the provider entry displayed on the Login Selector UI.

xpack.security.authc.providers.<provider-type>.<provider-name>.origin ![logo cloud](https://doc-icons.s3.us-east-2.amazonaws.com/logo_cloud.svg "Supported on {{ech}}")
: Specifies a list of allowed origins for authentication requests initiated from the UI. Each origin must be a valid URI and is matched against the browser’s `origin` header when fulfilling an authentication request. Providers not matching the browser's location do not appear in the UI. By default, requests are not restricted to specific origins.

For example:

```yaml
xpack.security.authc:
providers:
basic.basic1:
origin: [http://localhost:5601, http://127.0.0.1:5601]
...

saml.saml1:
origin: https://elastic.co
...
```

xpack.security.authc.providers.<provider-type>.<provider-name>.showInSelector ![logo cloud](https://doc-icons.s3.us-east-2.amazonaws.com/logo_cloud.svg "Supported on {{ech}}")
: Flag that indicates if the provider should have an entry on the Login Selector UI. Setting this to `false` doesn’t remove the provider from the authentication chain.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface LoginSelectorProvider {
description?: string;
hint?: string;
icon?: string;
origin?: string | string[];
}

export interface LoginSelector {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React from 'react';
import ReactMarkdown from 'react-markdown';

import { coreMock } from '@kbn/core/public/mocks';
import { i18n } from '@kbn/i18n';
import { findTestSubject, mountWithIntl, nextTick, shallowWithIntl } from '@kbn/test-jest-helpers';

import { LoginForm, MessageType, PageMode } from './login_form';
Expand Down Expand Up @@ -398,6 +399,137 @@ describe('LoginForm', () => {
]);
});

it('does not render providers with origin configs that to not match current page', async () => {
const currentURL = `https://some-host.com/login?next=${encodeURIComponent(
'/some-base-path/app/kibana#/home?_g=()'
)}`;

const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' });

window.location = { ...window.location, href: currentURL, origin: 'https://some-host.com' };
const wrapper = mountWithIntl(
<EuiProvider>
<LoginForm
http={coreStartMock.http}
notifications={coreStartMock.notifications}
loginAssistanceMessage=""
selector={{
enabled: true,
providers: [
{
type: 'basic',
name: 'basic',
usesLoginForm: true,
hint: 'Basic hint',
icon: 'logoElastic',
showInSelector: true,
},
{
type: 'saml',
name: 'saml1',
description: 'Log in w/SAML',
origin: ['https://some-host.com', 'https://some-other-host.com'],
usesLoginForm: false,
showInSelector: true,
},
{
type: 'pki',
name: 'pki1',
description: 'Log in w/PKI',
hint: 'PKI hint',
origin: 'https://not-some-host.com',
usesLoginForm: false,
showInSelector: true,
},
],
}}
/>
</EuiProvider>
);

expect(window.location.origin).toBe('https://some-host.com');

expectPageMode(wrapper, PageMode.Selector);

const result = findTestSubject(wrapper, 'loginCard-', '^=').map((card) => {
const hint = findTestSubject(card, 'card-hint');
return {
title: findTestSubject(card, 'card-title').text(),
hint: hint.exists() ? hint.text() : '',
icon: card.find(EuiIcon).props().type,
};
});

expect(result).toEqual([
{ title: 'Log in with basic/basic', hint: 'Basic hint', icon: 'logoElastic' },
{ title: 'Log in w/SAML', hint: '', icon: 'empty' },
]);
});

it('does not render any providers and shows error message if no providers match current origin', async () => {
const currentURL = `https://some-host.com/login?next=${encodeURIComponent(
'/some-base-path/app/kibana#/home?_g=()'
)}`;

const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' });

window.location = { ...window.location, href: currentURL, origin: 'https://some-host.com' };
const wrapper = mountWithIntl(
<EuiProvider>
<LoginForm
http={coreStartMock.http}
notifications={coreStartMock.notifications}
loginAssistanceMessage=""
selector={{
enabled: true,
providers: [
{
type: 'basic',
name: 'basic',
usesLoginForm: true,
hint: 'Basic hint',
icon: 'logoElastic',
origin: 'https://not-some-host.com',
showInSelector: true,
},
{
type: 'saml',
name: 'saml1',
description: 'Log in w/SAML',
origin: ['https://not-some-host.com', 'https://not-some-other-host.com'],
usesLoginForm: false,
showInSelector: true,
},
{
type: 'pki',
name: 'pki1',
description: 'Log in w/PKI',
hint: 'PKI hint',
origin: 'https://not-some-host.com',
usesLoginForm: false,
showInSelector: true,
},
],
}}
/>
</EuiProvider>
);

expect(window.location.origin).toBe('https://some-host.com');

expect(findTestSubject(wrapper, 'loginForm').exists()).toBe(false);
expect(findTestSubject(wrapper, 'loginSelector').exists()).toBe(false);
expect(findTestSubject(wrapper, 'loginHelp').exists()).toBe(false);
expect(findTestSubject(wrapper, 'autoLoginOverlay').exists()).toBe(false);
expect(findTestSubject(wrapper, 'loginCard-', '^=').exists()).toBe(false);

expect(findTestSubject(wrapper, 'loginErrorMessage').text()).toEqual(
i18n.translate('xpack.security.noAuthProvidersForDomain', {
defaultMessage: 'No authentication providers have been configured for this domain.',
})
);
});

it('properly redirects after successful login', async () => {
const currentURL = `https://some-host/login?next=${encodeURIComponent(
'/some-base-path/app/kibana#/home?_g=()'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,18 @@ const assistanceCss = (theme: UseEuiTheme) => css`
}
`;

const noProvidersMessage = i18n.translate('xpack.security.noAuthProvidersForDomain', {
defaultMessage: 'No authentication providers have been configured for this domain.',
});

export class LoginForm extends Component<LoginFormProps, State> {
private readonly validator: LoginValidator;

/**
* Available providers that match the current origin.
*/
private readonly availableProviders: LoginSelectorProvider[];

/**
* Optional provider that was suggested by the `auth_provider_hint={providerName}` query string parameter. If provider
* doesn't require Kibana native login form then login process is triggered automatically, otherwise Login Selector
Expand All @@ -142,10 +151,15 @@ export class LoginForm extends Component<LoginFormProps, State> {

constructor(props: LoginFormProps) {
super(props);

this.availableProviders = this.props.selector.providers.filter((provider) =>
this.providerMatchesOrigin(provider)
);

this.validator = new LoginValidator({ shouldValidate: false });

this.suggestedProvider = this.props.authProviderHint
? this.props.selector.providers.find(({ name }) => name === this.props.authProviderHint)
? this.availableProviders.find(({ name }) => name === this.props.authProviderHint)
: undefined;

// Switch to the Form mode right away if provider from the hint requires it.
Expand All @@ -158,7 +172,14 @@ export class LoginForm extends Component<LoginFormProps, State> {
loadingState: { type: LoadingStateType.None },
username: '',
password: '',
message: this.props.message || { type: MessageType.None },
message:
this.props.message ??
(this.availableProviders.length === 0
? {
type: MessageType.Danger,
content: noProvidersMessage,
}
: { type: MessageType.None }),
mode,
previousMode: mode,
};
Expand Down Expand Up @@ -236,6 +257,10 @@ export class LoginForm extends Component<LoginFormProps, State> {
};

public renderContent() {
if (this.availableProviders.length === 0) {
return;
}

switch (this.state.mode) {
case PageMode.Form:
return this.renderLoginForm();
Expand Down Expand Up @@ -339,7 +364,8 @@ export class LoginForm extends Component<LoginFormProps, State> {
};

private renderSelector = () => {
const providers = this.props.selector.providers.filter((p) => p.showInSelector);
const providers = this.availableProviders.filter((p) => p.showInSelector);

return (
<EuiPanel data-test-subj="loginSelector" paddingSize="none">
{providers.map((provider) => {
Expand Down Expand Up @@ -513,9 +539,7 @@ export class LoginForm extends Component<LoginFormProps, State> {
});

// We try to log in with the provider that uses login form and has the lowest order.
const providerToLoginWith = this.props.selector.providers.find(
(provider) => provider.usesLoginForm
)!;
const providerToLoginWith = this.availableProviders.find((provider) => provider.usesLoginForm)!;

try {
const { location } = await this.props.http.post<{ location: string }>(
Expand Down Expand Up @@ -603,9 +627,17 @@ export class LoginForm extends Component<LoginFormProps, State> {
private showLoginSelector() {
return (
this.props.selector.enabled &&
this.props.selector.providers.some(
(provider) => !provider.usesLoginForm && provider.showInSelector
)
this.availableProviders.some((provider) => !provider.usesLoginForm && provider.showInSelector)
);
}

private providerMatchesOrigin(provider: LoginSelectorProvider): boolean {
const { origin } = window.location;
return (
!provider.origin ||
(Array.isArray(provider.origin)
? provider.origin.includes(origin)
: provider.origin === origin)
);
}
}
Loading
Loading