Skip to content

Commit

Permalink
[7.x] Add username/password validation to login form (elastic#60681) (e…
Browse files Browse the repository at this point in the history
  • Loading branch information
jportner authored Apr 1, 2020
1 parent 1d95c21 commit 6214dbe
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 22 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ReactMarkdown from 'react-markdown';
import {
EuiButton,
EuiCallOut,
EuiFieldPassword,
EuiFieldText,
EuiFormRow,
EuiPanel,
Expand All @@ -18,6 +19,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { HttpStart, IHttpFetchError, NotificationsStart } from 'src/core/public';
import { LoginValidator, LoginValidationResult } from './validate_login';
import { parseNext } from '../../../../../common/parse_next';
import { LoginSelector } from '../../../../../common/login_state';

Expand All @@ -40,6 +42,7 @@ interface State {
message:
| { type: MessageType.None }
| { type: MessageType.Danger | MessageType.Info; content: string };
formError: LoginValidationResult | null;
}

enum LoadingStateType {
Expand All @@ -55,14 +58,21 @@ enum MessageType {
}

export class LoginForm extends Component<Props, State> {
public state: State = {
loadingState: { type: LoadingStateType.None },
username: '',
password: '',
message: this.props.infoMessage
? { type: MessageType.Info, content: this.props.infoMessage }
: { type: MessageType.None },
};
private readonly validator: LoginValidator;

constructor(props: Props) {
super(props);
this.validator = new LoginValidator({ shouldValidate: false });
this.state = {
loadingState: { type: LoadingStateType.None },
username: '',
password: '',
message: this.props.infoMessage
? { type: MessageType.Info, content: this.props.infoMessage }
: { type: MessageType.None },
formError: null,
};
}

public render() {
return (
Expand Down Expand Up @@ -90,6 +100,7 @@ export class LoginForm extends Component<Props, State> {
defaultMessage="Username"
/>
}
{...this.validator.validateUsername(this.state.username)}
>
<EuiFieldText
id="username"
Expand All @@ -111,13 +122,13 @@ export class LoginForm extends Component<Props, State> {
defaultMessage="Password"
/>
}
{...this.validator.validatePassword(this.state.password)}
>
<EuiFieldText
<EuiFieldPassword
autoComplete="off"
id="password"
name="password"
data-test-subj="loginPassword"
type="password"
value={this.state.password}
onChange={this.onPasswordChange}
disabled={!this.isLoadingState(LoadingStateType.None)}
Expand Down Expand Up @@ -248,12 +259,6 @@ export class LoginForm extends Component<Props, State> {
}
}

private isFormValid = () => {
const { username, password } = this.state;

return username && password;
};

private onUsernameChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
username: e.target.value,
Expand All @@ -271,8 +276,15 @@ export class LoginForm extends Component<Props, State> {
) => {
e.preventDefault();

if (!this.isFormValid()) {
this.validator.enableValidation();

const { username, password } = this.state;
const result = this.validator.validateForLogin(username, password);
if (result.isInvalid) {
this.setState({ formError: result });
return;
} else {
this.setState({ formError: null });
}

this.setState({
Expand All @@ -281,7 +293,6 @@ export class LoginForm extends Component<Props, State> {
});

const { http } = this.props;
const { username, password } = this.state;

try {
await http.post('/internal/security/login', { body: JSON.stringify({ username, password }) });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { LoginValidator, LoginValidationResult } from './validate_login';

function expectValid(result: LoginValidationResult) {
expect(result.isInvalid).toBe(false);
}

function expectInvalid(result: LoginValidationResult) {
expect(result.isInvalid).toBe(true);
}

describe('LoginValidator', () => {
describe('#validateUsername', () => {
it(`returns 'valid' if validation is disabled`, () => {
expectValid(new LoginValidator().validateUsername(''));
});

it(`returns 'invalid' if username is missing`, () => {
expectInvalid(new LoginValidator({ shouldValidate: true }).validateUsername(''));
});

it(`returns 'valid' for correct usernames`, () => {
expectValid(new LoginValidator({ shouldValidate: true }).validateUsername('u'));
});
});

describe('#validatePassword', () => {
it(`returns 'valid' if validation is disabled`, () => {
expectValid(new LoginValidator().validatePassword(''));
});

it(`returns 'invalid' if password is missing`, () => {
expectInvalid(new LoginValidator({ shouldValidate: true }).validatePassword(''));
});

it(`returns 'valid' for correct passwords`, () => {
expectValid(new LoginValidator({ shouldValidate: true }).validatePassword('p'));
});
});

describe('#validateForLogin', () => {
it(`returns 'valid' if validation is disabled`, () => {
expectValid(new LoginValidator().validateForLogin('', ''));
});

it(`returns 'invalid' if username is invalid`, () => {
expectInvalid(new LoginValidator({ shouldValidate: true }).validateForLogin('', 'p'));
});

it(`returns 'invalid' if password is invalid`, () => {
expectInvalid(new LoginValidator({ shouldValidate: true }).validateForLogin('u', ''));
});

it(`returns 'valid' if username and password are valid`, () => {
expectValid(new LoginValidator({ shouldValidate: true }).validateForLogin('u', 'p'));
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { i18n } from '@kbn/i18n';

interface LoginValidatorOptions {
shouldValidate?: boolean;
}

export interface LoginValidationResult {
isInvalid: boolean;
error?: string;
}

export class LoginValidator {
private shouldValidate?: boolean;

constructor(options: LoginValidatorOptions = {}) {
this.shouldValidate = options.shouldValidate;
}

public enableValidation() {
this.shouldValidate = true;
}

public disableValidation() {
this.shouldValidate = false;
}

public validateUsername(username: string): LoginValidationResult {
if (!this.shouldValidate) {
return valid();
}

if (!username) {
// Elasticsearch has more stringent requirements for usernames in the Native realm. However, the login page is used for other realms,
// such as LDAP and Active Directory. Because of that, the best validation we can do here is to ensure the username is not empty.
return invalid(
i18n.translate(
'xpack.security.authentication.login.validateLogin.requiredUsernameErrorMessage',
{
defaultMessage: 'Username is required',
}
)
);
}

return valid();
}

public validatePassword(password: string): LoginValidationResult {
if (!this.shouldValidate) {
return valid();
}

if (!password) {
// Elasticsearch has more stringent requirements for passwords in the Native realm. However, the login page is used for other realms,
// such as LDAP and Active Directory. Because of that, the best validation we can do here is to ensure the password is not empty.
return invalid(
i18n.translate(
'xpack.security.authentication.login.validateLogin.requiredPasswordErrorMessage',
{
defaultMessage: 'Password is required',
}
)
);
}
return valid();
}

public validateForLogin(username: string, password: string): LoginValidationResult {
const { isInvalid: isUsernameInvalid } = this.validateUsername(username);
const { isInvalid: isPasswordInvalid } = this.validatePassword(password);

if (isUsernameInvalid || isPasswordInvalid) {
return invalid();
}

return valid();
}
}

function invalid(error?: string): LoginValidationResult {
return {
isInvalid: true,
error,
};
}

function valid(): LoginValidationResult {
return {
isInvalid: false,
};
}

0 comments on commit 6214dbe

Please sign in to comment.