Skip to content

Commit

Permalink
Implement AnonymousAuthenticationProvider.
Browse files Browse the repository at this point in the history
  • Loading branch information
azasypkin committed Oct 30, 2020
1 parent 076bb73 commit 892b88a
Show file tree
Hide file tree
Showing 10 changed files with 408 additions and 104 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/security/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export const UNKNOWN_SPACE = '?';
export const GLOBAL_RESOURCE = '*';
export const APPLICATION_PREFIX = 'kibana-';
export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*';

export const KBN_LOGIN_HINT_QUERY_STRING_PARAMETER = 'kbnLoginHint';
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { HttpStart, IHttpFetchError, NotificationsStart } from 'src/core/public';
import { LoginSelector } from '../../../../../common/login_state';
import { SectionLoading } from '../../../../../../../../src/plugins/es_ui_shared/public';
import { LoginSelector, LoginSelectorProvider } from '../../../../../common/login_state';
import { LoginValidator } from './validate_login';

interface Props {
Expand All @@ -39,12 +40,12 @@ interface Props {
infoMessage?: string;
loginAssistanceMessage: string;
loginHelp?: string;
loginHint?: string;
}

interface State {
loadingState:
| { type: LoadingStateType.None }
| { type: LoadingStateType.Form }
| { type: LoadingStateType.None | LoadingStateType.Form | LoadingStateType.AutoLogin }
| { type: LoadingStateType.Selector; providerName: string };
username: string;
password: string;
Expand All @@ -59,6 +60,7 @@ enum LoadingStateType {
None,
Form,
Selector,
AutoLogin,
}

enum MessageType {
Expand All @@ -76,11 +78,26 @@ export enum PageMode {
export class LoginForm extends Component<Props, State> {
private readonly validator: LoginValidator;

/**
* Optional provider that was suggested by the `kbnLoginHint={providerName}` query string parameter. If provider
* doesn't require Kibana native login form then login process is triggered automatically, otherwise Login Selector
* just switches to the Login Form mode.
*/
private readonly providerFromHint?: LoginSelectorProvider;

constructor(props: Props) {
super(props);
this.validator = new LoginValidator({ shouldValidate: false });

const mode = this.showLoginSelector() ? PageMode.Selector : PageMode.Form;
this.providerFromHint = this.props.loginHint
? this.props.selector.providers.find((provider) => provider.name === this.props.loginHint)
: undefined;

// Switch to the Form mode right away if provider from the hint requires it.
const mode =
this.showLoginSelector() && !this.providerFromHint?.usesLoginForm
? PageMode.Selector
: PageMode.Form;

this.state = {
loadingState: { type: LoadingStateType.None },
Expand All @@ -94,6 +111,12 @@ export class LoginForm extends Component<Props, State> {
};
}

async componentDidMount() {
if (this.providerFromHint && !this.providerFromHint.usesLoginForm) {
await this.loginWithSelector({ provider: this.providerFromHint, autoLogin: true });
}
}

public render() {
return (
<Fragment>
Expand Down Expand Up @@ -160,7 +183,9 @@ export class LoginForm extends Component<Props, State> {
case PageMode.Form:
return this.renderLoginForm();
case PageMode.Selector:
return this.renderSelector();
return this.isLoadingState(LoadingStateType.AutoLogin)
? this.renderAutoLoginOverlay()
: this.renderSelector();
case PageMode.LoginHelp:
return this.renderLoginHelp();
}
Expand Down Expand Up @@ -267,7 +292,7 @@ export class LoginForm extends Component<Props, State> {
onClick={() =>
provider.usesLoginForm
? this.onPageModeChange(PageMode.Form)
: this.loginWithSelector(provider.type, provider.name)
: this.loginWithSelector({ provider })
}
className={`secLoginCard ${
this.isLoadingState(LoadingStateType.Selector, provider.name)
Expand Down Expand Up @@ -360,6 +385,19 @@ export class LoginForm extends Component<Props, State> {
return null;
};

private renderAutoLoginOverlay = () => {
return (
<EuiPanel data-test-subj="loginSelector" paddingSize="none">
<SectionLoading>
<FormattedMessage
id="xpack.security.loginPage.autoLoginLabel"
defaultMessage="Logging in…"
/>
</SectionLoading>
</EuiPanel>
);
};

private setUsernameInputRef(ref: HTMLInputElement) {
if (ref) {
ref.focus();
Expand Down Expand Up @@ -438,9 +476,17 @@ export class LoginForm extends Component<Props, State> {
}
};

private loginWithSelector = async (providerType: string, providerName: string) => {
private loginWithSelector = async ({
provider: { type: providerType, name: providerName },
autoLogin,
}: {
provider: LoginSelectorProvider;
autoLogin?: boolean;
}) => {
this.setState({
loadingState: { type: LoadingStateType.Selector, providerName },
loadingState: autoLogin
? { type: LoadingStateType.AutoLogin }
: { type: LoadingStateType.Selector, providerName },
message: { type: MessageType.None },
});

Expand All @@ -466,7 +512,9 @@ export class LoginForm extends Component<Props, State> {
}
};

private isLoadingState(type: LoadingStateType.None | LoadingStateType.Form): boolean;
private isLoadingState(
type: LoadingStateType.None | LoadingStateType.Form | LoadingStateType.AutoLogin
): boolean;
private isLoadingState(type: LoadingStateType.Selector, providerName: string): boolean;
private isLoadingState(type: LoadingStateType, providerName?: string) {
const { loadingState } = this.state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, EuiTitle } from '@elasti
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { CoreStart, FatalErrorsStart, HttpStart, NotificationsStart } from 'src/core/public';
import { KBN_LOGIN_HINT_QUERY_STRING_PARAMETER } from '../../../common/constants';
import { LoginState } from '../../../common/login_state';
import { LoginForm, DisabledLoginForm } from './components';

Expand Down Expand Up @@ -212,14 +213,16 @@ export class LoginPage extends Component<Props, State> {
);
}

const query = parse(window.location.href, true).query;
return (
<LoginForm
http={this.props.http}
notifications={this.props.notifications}
selector={selector}
infoMessage={infoMessageMap.get(parse(window.location.href, true).query.msg?.toString())}
infoMessage={infoMessageMap.get(query.msg?.toString())}
loginAssistanceMessage={this.props.loginAssistanceMessage}
loginHelp={loginHelp}
loginHint={query[KBN_LOGIN_HINT_QUERY_STRING_PARAMETER]?.toString()}
/>
);
};
Expand Down
36 changes: 24 additions & 12 deletions x-pack/plugins/security/server/authentication/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ILegacyClusterClient,
IBasePath,
} from '../../../../../src/core/server';
import { KBN_LOGIN_HINT_QUERY_STRING_PARAMETER } from '../../common/constants';
import { SecurityLicense } from '../../common/licensing';
import { AuthenticatedUser } from '../../common/model';
import { AuthenticationProvider } from '../../common/types';
Expand All @@ -20,6 +21,7 @@ import { SecurityFeatureUsageServiceStart } from '../feature_usage';
import { SessionValue, Session } from '../session_management';

import {
AnonymousAuthenticationProvider,
AuthenticationProviderOptions,
AuthenticationProviderSpecificOptions,
BaseAuthenticationProvider,
Expand Down Expand Up @@ -86,6 +88,7 @@ const providerMap = new Map<
[TokenAuthenticationProvider.type, TokenAuthenticationProvider],
[OIDCAuthenticationProvider.type, OIDCAuthenticationProvider],
[PKIAuthenticationProvider.type, PKIAuthenticationProvider],
[AnonymousAuthenticationProvider.type, AnonymousAuthenticationProvider],
]);

/**
Expand Down Expand Up @@ -259,7 +262,7 @@ export class Authenticator {
isLoginAttemptWithProviderName(attempt) && this.providers.has(attempt.provider.name)
? [[attempt.provider.name, this.providers.get(attempt.provider.name)!]]
: isLoginAttemptWithProviderType(attempt)
? [...this.providerIterator(existingSessionValue)].filter(
? [...this.providerIterator(existingSessionValue?.provider.name)].filter(
([, { type }]) => type === attempt.provider.type
)
: [];
Expand Down Expand Up @@ -328,17 +331,26 @@ export class Authenticator {
assertRequest(request);

const existingSessionValue = await this.getSessionValue(request);
const suggestedProviderName =
existingSessionValue?.provider.name ??
new URLSearchParams(request.url.search).get(KBN_LOGIN_HINT_QUERY_STRING_PARAMETER);

if (this.shouldRedirectToLoginSelector(request, existingSessionValue)) {
this.logger.debug('Redirecting request to Login Selector.');
return AuthenticationResult.redirectTo(
`${this.options.basePath.serverBasePath}/login?next=${encodeURIComponent(
`${this.options.basePath.get(request)}${request.url.pathname}${request.url.search}`
)}`
)}${
suggestedProviderName && !existingSessionValue
? `&${KBN_LOGIN_HINT_QUERY_STRING_PARAMETER}=${encodeURIComponent(
suggestedProviderName
)}`
: ''
}`
);
}

for (const [providerName, provider] of this.providerIterator(existingSessionValue)) {
for (const [providerName, provider] of this.providerIterator(suggestedProviderName)) {
// Check if current session has been set by this provider.
const ownsSession =
existingSessionValue?.provider.name === providerName &&
Expand Down Expand Up @@ -395,7 +407,7 @@ export class Authenticator {
// active session already some providers can still properly respond to the 3rd-party logout
// request. For example SAML provider can process logout request encoded in `SAMLRequest`
// query string parameter.
for (const [, provider] of this.providerIterator(null)) {
for (const [, provider] of this.providerIterator()) {
const deauthenticationResult = await provider.logout(request);
if (!deauthenticationResult.notHandled()) {
return deauthenticationResult;
Expand Down Expand Up @@ -473,22 +485,22 @@ export class Authenticator {
}

/**
* Returns provider iterator where providers are sorted in the order of priority (based on the session ownership).
* @param sessionValue Current session value.
* Returns provider iterator starting from the suggested provider if any.
* @param suggestedProviderName Optional name of the provider to return first.
*/
private *providerIterator(
sessionValue: SessionValue | null
suggestedProviderName?: string | null
): IterableIterator<[string, BaseAuthenticationProvider]> {
// If there is no session to predict which provider to use first, let's use the order
// providers are configured in. Otherwise return provider that owns session first, and only then the rest
// If there is no provider suggested or suggested provider isn't configured, let's use the order
// providers are configured in. Otherwise return suggested provider first, and only then the rest
// of providers.
if (!sessionValue) {
if (!suggestedProviderName || !this.providers.has(suggestedProviderName)) {
yield* this.providers;
} else {
yield [sessionValue.provider.name, this.providers.get(sessionValue.provider.name)!];
yield [suggestedProviderName, this.providers.get(suggestedProviderName)!];

for (const [providerName, provider] of this.providers) {
if (providerName !== sessionValue.provider.name) {
if (providerName !== suggestedProviderName) {
yield [providerName, provider];
}
}
Expand Down
Loading

0 comments on commit 892b88a

Please sign in to comment.