Skip to content

Commit

Permalink
Add verification code protection (#110856)
Browse files Browse the repository at this point in the history
* Add verification code protection

* Fix bug where verification code could be less than 6 digits

* Added suggestions from code review

* fix type errors

* Added suggestions from code review
  • Loading branch information
thomheymann authored Sep 6, 2021
1 parent 705fe22 commit 219ff6c
Show file tree
Hide file tree
Showing 26 changed files with 946 additions and 105 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ pageLoadAssetSize:
expressionImage: 19288
expressionMetric: 22238
expressionShape: 34008
interactiveSetup: 70000
interactiveSetup: 80000
expressionTagcloud: 27505
expressions: 239290
securitySolution: 231753
9 changes: 9 additions & 0 deletions src/plugins/interactive_setup/common/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export const VERIFICATION_CODE_LENGTH = 6;
1 change: 1 addition & 0 deletions src/plugins/interactive_setup/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

export type { InteractiveSetupViewState, EnrollmentToken, Certificate, PingResult } from './types';
export { ElasticsearchConnectionStatus } from './elasticsearch_connection_status';
export { VERIFICATION_CODE_LENGTH } from './constants';
10 changes: 6 additions & 4 deletions src/plugins/interactive_setup/public/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import { ClusterConfigurationForm } from './cluster_configuration_form';
import { EnrollmentTokenForm } from './enrollment_token_form';
import { ProgressIndicator } from './progress_indicator';

export const App: FunctionComponent = () => {
export interface AppProps {
onSuccess?(): void;
}

export const App: FunctionComponent<AppProps> = ({ onSuccess }) => {
const [page, setPage] = useState<'token' | 'manual' | 'success'>('token');
const [cluster, setCluster] = useState<
Omit<ClusterConfigurationFormProps, 'onCancel' | 'onSuccess'>
Expand Down Expand Up @@ -71,9 +75,7 @@ export const App: FunctionComponent = () => {
/>
)}
</div>
{page === 'success' && (
<ProgressIndicator onSuccess={() => window.location.replace(window.location.href)} />
)}
{page === 'success' && <ProgressIndicator onSuccess={onSuccess} />}
</EuiPanel>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const ClusterAddressForm: FunctionComponent<ClusterAddressFormProps> = ({
const [form, eventHandlers] = useForm({
defaultValues,
validate: async (values) => {
const errors: ValidationErrors<typeof values> = {};
const errors: ValidationErrors<ClusterAddressFormValues> = {};

if (!values.host) {
errors.host = i18n.translate('interactiveSetup.clusterAddressForm.hostRequiredError', {
Expand Down
27 changes: 17 additions & 10 deletions src/plugins/interactive_setup/public/cluster_configuration_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from '@elastic/eui';
import type { FunctionComponent } from 'react';
import React from 'react';
import useUpdateEffect from 'react-use/lib/useUpdateEffect';

import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
Expand All @@ -37,6 +38,8 @@ import type { ValidationErrors } from './use_form';
import { useForm } from './use_form';
import { useHtmlId } from './use_html_id';
import { useHttp } from './use_http';
import { useVerification } from './use_verification';
import { useVisibility } from './use_visibility';

export interface ClusterConfigurationFormValues {
username: string;
Expand Down Expand Up @@ -66,10 +69,10 @@ export const ClusterConfigurationForm: FunctionComponent<ClusterConfigurationFor
onSuccess,
}) => {
const http = useHttp();

const { status, getCode } = useVerification();
const [form, eventHandlers] = useForm({
defaultValues,
validate: async (values) => {
validate: (values) => {
const errors: ValidationErrors<ClusterConfigurationFormValues> = {};

if (authRequired) {
Expand All @@ -93,7 +96,7 @@ export const ClusterConfigurationForm: FunctionComponent<ClusterConfigurationFor
errors.password = i18n.translate(
'interactiveSetup.clusterConfigurationForm.passwordRequiredError',
{
defaultMessage: `Enter a password.`,
defaultMessage: 'Enter a password.',
}
);
}
Expand All @@ -117,17 +120,24 @@ export const ClusterConfigurationForm: FunctionComponent<ClusterConfigurationFor
username: values.username,
password: values.password,
caCert: values.caCert,
code: getCode(),
}),
});
onSuccess?.();
},
});

const [isVisible, buttonRef] = useVisibility<HTMLButtonElement>();
const trustCaCertId = useHtmlId('clusterConfigurationForm', 'trustCaCert');

useUpdateEffect(() => {
if (status === 'verified' && isVisible) {
form.submit();
}
}, [status]);

return (
<EuiForm component="form" noValidate {...eventHandlers}>
{form.submitError && (
{status !== 'unverified' && !form.isSubmitting && !form.isValidating && form.submitError && (
<>
<EuiCallOut
color="danger"
Expand All @@ -140,7 +150,6 @@ export const ClusterConfigurationForm: FunctionComponent<ClusterConfigurationFor
<EuiSpacer />
</>
)}

<EuiFlexGroup responsive={false} alignItems="center" gutterSize="s">
<EuiFlexItem grow={false} className="eui-textNoWrap">
<FormattedMessage
Expand All @@ -155,7 +164,6 @@ export const ClusterConfigurationForm: FunctionComponent<ClusterConfigurationFor
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />

{authRequired ? (
<>
<EuiFormRow
Expand Down Expand Up @@ -221,7 +229,6 @@ export const ClusterConfigurationForm: FunctionComponent<ClusterConfigurationFor
<EuiSpacer />
</>
)}

{certificateChain && certificateChain.length > 0 && (
<>
<EuiFormRow
Expand All @@ -242,8 +249,8 @@ export const ClusterConfigurationForm: FunctionComponent<ClusterConfigurationFor
checked={!!form.values.caCert}
onChange={() => {
const intermediateCa = certificateChain[Math.min(1, certificateChain.length - 1)];
form.setValue('caCert', form.values.caCert ? '' : intermediateCa.raw);
form.setTouched('caCert');
form.setValue('caCert', form.values.caCert ? '' : intermediateCa.raw);
}}
>
<CertificatePanel certificate={certificateChain[0]} />
Expand All @@ -252,7 +259,6 @@ export const ClusterConfigurationForm: FunctionComponent<ClusterConfigurationFor
<EuiSpacer />
</>
)}

<EuiFlexGroup responsive={false} justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty flush="right" iconType="arrowLeft" onClick={onCancel}>
Expand All @@ -264,6 +270,7 @@ export const ClusterConfigurationForm: FunctionComponent<ClusterConfigurationFor
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
buttonRef={buttonRef}
type="submit"
isLoading={form.isSubmitting}
isDisabled={form.isSubmitted && form.isInvalid}
Expand Down
33 changes: 17 additions & 16 deletions src/plugins/interactive_setup/public/enrollment_token_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import {
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiIcon,
EuiSpacer,
EuiText,
EuiTextArea,
} from '@elastic/eui';
import type { FunctionComponent } from 'react';
import React from 'react';
import useUpdateEffect from 'react-use/lib/useUpdateEffect';

import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
Expand All @@ -31,6 +31,8 @@ import { TextTruncate } from './text_truncate';
import type { ValidationErrors } from './use_form';
import { useForm } from './use_form';
import { useHttp } from './use_http';
import { useVerification } from './use_verification';
import { useVisibility } from './use_visibility';

export interface EnrollmentTokenFormValues {
token: string;
Expand All @@ -50,6 +52,7 @@ export const EnrollmentTokenForm: FunctionComponent<EnrollmentTokenFormProps> =
onSuccess,
}) => {
const http = useHttp();
const { status, getCode } = useVerification();
const [form, eventHandlers] = useForm({
defaultValues,
validate: (values) => {
Expand Down Expand Up @@ -77,17 +80,25 @@ export const EnrollmentTokenForm: FunctionComponent<EnrollmentTokenFormProps> =
hosts: decoded.adr,
apiKey: decoded.key,
caFingerprint: decoded.fgr,
code: getCode(),
}),
});
onSuccess?.();
},
});
const [isVisible, buttonRef] = useVisibility<HTMLButtonElement>();

useUpdateEffect(() => {
if (status === 'verified' && isVisible) {
form.submit();
}
}, [status]);

const enrollmentToken = decodeEnrollmentToken(form.values.token);

return (
<EuiForm component="form" noValidate {...eventHandlers}>
{form.submitError && (
{status !== 'unverified' && !form.isSubmitting && !form.isValidating && form.submitError && (
<>
<EuiCallOut
color="danger"
Expand Down Expand Up @@ -133,6 +144,7 @@ export const EnrollmentTokenForm: FunctionComponent<EnrollmentTokenFormProps> =
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
buttonRef={buttonRef}
type="submit"
isLoading={form.isSubmitting}
isDisabled={form.isSubmitted && form.isInvalid}
Expand Down Expand Up @@ -163,21 +175,10 @@ const EnrollmentTokenDetails: FunctionComponent<EnrollmentTokenDetailsProps> = (
defaultMessage="Connect to"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type="lock" />
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ overflow: 'hidden' }}>
<TextTruncate>{token.adr[0]}</TextTruncate>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type="logoElasticsearch" />
</EuiFlexItem>
<EuiFlexItem grow={false} className="eui-textNoWrap">
<FormattedMessage
id="interactiveSetup.enrollmentTokenDetails.elasticsearchVersion"
defaultMessage="Elasticsearch (v{version})"
values={{ version: token.ver }}
/>
<TextTruncate>
<strong>{token.adr[0]}</strong>
</TextTruncate>
</EuiFlexItem>
</EuiFlexGroup>
</EuiText>
Expand Down
19 changes: 15 additions & 4 deletions src/plugins/interactive_setup/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { CoreSetup, CoreStart, HttpSetup, Plugin } from 'src/core/public';

import { App } from './app';
import { HttpProvider } from './use_http';
import { VerificationProvider } from './use_verification';

export class InteractiveSetupPlugin implements Plugin<void, void, {}, {}> {
public setup(core: CoreSetup) {
Expand All @@ -24,9 +25,16 @@ export class InteractiveSetupPlugin implements Plugin<void, void, {}, {}> {
appRoute: '/',
chromeless: true,
mount: (params) => {
const url = new URL(window.location.href);
const defaultCode = url.searchParams.get('code') || undefined;
const onSuccess = () => {
url.searchParams.delete('code');
window.location.replace(url.href);
};

ReactDOM.render(
<Providers http={core.http}>
<App />
<Providers defaultCode={defaultCode} http={core.http}>
<App onSuccess={onSuccess} />
</Providers>,
params.element
);
Expand All @@ -40,10 +48,13 @@ export class InteractiveSetupPlugin implements Plugin<void, void, {}, {}> {

export interface ProvidersProps {
http: HttpSetup;
defaultCode?: string;
}

export const Providers: FunctionComponent<ProvidersProps> = ({ http, children }) => (
export const Providers: FunctionComponent<ProvidersProps> = ({ defaultCode, http, children }) => (
<I18nProvider>
<HttpProvider http={http}>{children}</HttpProvider>
<HttpProvider http={http}>
<VerificationProvider defaultCode={defaultCode}>{children}</VerificationProvider>
</HttpProvider>
</I18nProvider>
);
Loading

0 comments on commit 219ff6c

Please sign in to comment.