Skip to content

Commit

Permalink
[Connect] Refactor FormLogin and add passwordless capabilities (#1019) (
Browse files Browse the repository at this point in the history
  • Loading branch information
kimlisa authored Aug 29, 2022
1 parent 4492b58 commit e1ec35a
Show file tree
Hide file tree
Showing 38 changed files with 3,271 additions and 595 deletions.
6 changes: 4 additions & 2 deletions web/packages/shared/components/ButtonSso/ButtonSso.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import styled from 'styled-components';
import Button from 'design/Button';
import { fade } from 'design/theme/utils/colorManipulator';
import { darken, lighten } from 'design/theme/utils/colorManipulator';
import * as Icons from 'design/Icon';
import { AuthProviderType } from 'shared/services';

Expand Down Expand Up @@ -101,10 +101,12 @@ const StyledButton = styled(Button)`
background-color: ${props => props.color};
display: block;
width: 100%;
border: 1px solid transparent;
&:hover,
&:focus {
background: ${props => fade(props.color, 0.4)};
background: ${props => darken(props.color, 0.1)};
border: 1px solid ${props => lighten(props.color, 0.4)};
}
height: 40px;
position: relative;
Expand Down
131 changes: 77 additions & 54 deletions web/packages/teleport/src/components/FormLogin/FormLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,29 @@ export default function LoginForm(props: Props) {
);
}

const SsoList = ({ attempt, authProviders, onLoginWithSso }: Props) => {
const SsoList = ({
attempt,
authProviders,
onLoginWithSso,
autoFocus = false,
}: Props) => {
const { isProcessing } = attempt;
return (
<SSOButtonList
prefixText="Login with"
isDisabled={isProcessing}
providers={authProviders}
onClick={onLoginWithSso}
autoFocus={autoFocus}
/>
);
};

const Passwordless = ({ onLoginWithWebauthn, attempt }: Props) => {
const Passwordless = ({
onLoginWithWebauthn,
attempt,
autoFocus = false,
}: Props) => {
// Firefox currently does not support passwordless and when
// logging in, it will return an ambigugous error.
// We display a soft warning because firefox may provide
Expand All @@ -144,6 +154,7 @@ const Passwordless = ({ onLoginWithWebauthn, attempt }: Props) => {
width="100%"
onClick={() => onLoginWithWebauthn()}
disabled={attempt.isProcessing}
autoFocus={autoFocus}
>
<Flex alignItems="center" justifyContent="space-between">
<Flex alignItems="center">
Expand Down Expand Up @@ -171,6 +182,7 @@ const LocalForm = ({
onLoginWithWebauthn,
clearAttempt,
hasTransitionEnded,
autoFocus = false,
}: Props & { hasTransitionEnded: boolean }) => {
const { isProcessing } = attempt;
const [pass, setPass] = useState('');
Expand All @@ -183,7 +195,7 @@ const LocalForm = ({
);

const usernameInputRef = useRefAutoFocus<HTMLInputElement>({
shouldFocus: hasTransitionEnded,
shouldFocus: hasTransitionEnded && autoFocus,
});

const [mfaType, setMfaType] = useState(mfaOptions[0]);
Expand Down Expand Up @@ -233,8 +245,9 @@ const LocalForm = ({
value={user}
onChange={e => setUser(e.target.value)}
placeholder="Username"
mb={3}
/>
<Box mb={isRecoveryEnabled ? 2 : 4}>
<Box mb={isRecoveryEnabled ? 1 : 3}>
<FieldInput
rule={requiredField('Password is required')}
label="Password"
Expand All @@ -257,7 +270,7 @@ const LocalForm = ({
)}
</Box>
{auth2faType !== 'off' && (
<Box mb={isRecoveryEnabled ? 3 : 4}>
<Box mb={isRecoveryEnabled ? 2 : 3}>
<Flex alignItems="flex-end">
<FieldSelect
maxWidth="50%"
Expand Down Expand Up @@ -313,6 +326,8 @@ const LocalForm = ({
);
};

// Primary determines which authentication type to display
// on initial render of the login form.
const Primary = ({
next,
refCallback,
Expand All @@ -323,19 +338,23 @@ const Primary = ({
let otherOptionsAvailable = true;
let $primary;

if (otherProps.primaryAuthType === 'passwordless') {
$primary = <Passwordless {...otherProps} />;
}

if (otherProps.primaryAuthType === 'local') {
otherOptionsAvailable = otherProps.isPasswordlessEnabled || ssoEnabled;
$primary = (
<LocalForm {...otherProps} hasTransitionEnded={hasTransitionEnded} />
);
}

if (otherProps.primaryAuthType === 'sso') {
$primary = <SsoList {...otherProps} />;
switch (otherProps.primaryAuthType) {
case 'passwordless':
$primary = <Passwordless {...otherProps} autoFocus={true} />;
break;
case 'sso':
$primary = <SsoList {...otherProps} autoFocus={true} />;
break;
case 'local':
otherOptionsAvailable = otherProps.isPasswordlessEnabled || ssoEnabled;
$primary = (
<LocalForm
{...otherProps}
hasTransitionEnded={hasTransitionEnded}
autoFocus={true}
/>
);
break;
}

return (
Expand All @@ -358,6 +377,11 @@ const Primary = ({
);
};

// Secondary determines what other forms of authentication
// is allowed for the user to login with.
//
// There can be multiple authn types available, which will
// be visually separated by a divider.
const Secondary = ({
prev,
refCallback,
Expand All @@ -366,50 +390,48 @@ const Secondary = ({
const ssoEnabled = otherProps.authProviders?.length > 0;
const { primaryAuthType, isPasswordlessEnabled } = otherProps;

const $local = <LocalForm {...otherProps} />;
const $sso = <SsoList {...otherProps} />;
const $passwordless = <Passwordless {...otherProps} />;

let $secondary;

if (primaryAuthType === 'passwordless') {
$secondary = (
<>
{ssoEnabled && (
switch (primaryAuthType) {
case 'passwordless':
if (ssoEnabled) {
$secondary = (
<>
{$sso}
<SsoList {...otherProps} autoFocus={true} />
<Divider />
<LocalForm {...otherProps} />
</>
)}
{$local}
</>
);
}

if (primaryAuthType === 'local') {
$secondary = (
<>
{isPasswordlessEnabled && $passwordless}
{isPasswordlessEnabled && ssoEnabled && <Divider />}
{ssoEnabled && $sso}
</>
);
}

if (primaryAuthType === 'sso') {
$secondary = (
<>
{isPasswordlessEnabled && (
);
} else {
$secondary = <LocalForm {...otherProps} autoFocus={true} />;
}
break;
case 'sso':
if (isPasswordlessEnabled) {
$secondary = (
<>
{$passwordless}
<Passwordless {...otherProps} autoFocus={true} />
<Divider />
<LocalForm {...otherProps} />
</>
)}
{$local}
</>
);
);
} else {
$secondary = <LocalForm {...otherProps} autoFocus={true} />;
}
break;
case 'local':
if (isPasswordlessEnabled) {
$secondary = (
<>
<Passwordless {...otherProps} autoFocus={true} />
{otherProps.isPasswordlessEnabled && ssoEnabled && <Divider />}
{ssoEnabled && <SsoList {...otherProps} />}
</>
);
} else {
$secondary = <SsoList {...otherProps} autoFocus={true} />;
}
break;
}

return (
<Box ref={refCallback}>
{$secondary}
Expand Down Expand Up @@ -490,6 +512,7 @@ export type Props = {
onLoginWithSso(provider: AuthProvider): void;
onLoginWithWebauthn(creds?: UserCredentials): void;
onLogin(username: string, password: string, token: string): void;
autoFocus?: boolean;
};

type AttemptState = ReturnType<typeof useAttempt>[0];
21 changes: 18 additions & 3 deletions web/packages/teleport/src/components/FormLogin/SsoButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,31 @@ limitations under the License.
*/

import React from 'react';
import { Box } from 'design';
import { Box, Text } from 'design';
import ButtonSso, { guessProviderType } from 'shared/components/ButtonSso';
import { AuthProvider } from 'shared/services';

const SSOBtnList = ({ providers, prefixText, isDisabled, onClick }: Props) => {
const SSOBtnList = ({
providers,
prefixText,
isDisabled,
onClick,
autoFocus = false,
}: Props) => {
const $btns = providers.map((item, index) => {
let { name, type, displayName } = item;
const title = displayName || `${prefixText} ${name}`;
const ssoType = guessProviderType(title, type);
const len = providers.length - 1;
return (
<ButtonSso
key={index}
title={title}
ssoType={ssoType}
disabled={isDisabled}
mt={3}
mb={index < len ? 3 : 0}
autoFocus={index === 0 && autoFocus}
onClick={e => {
e.preventDefault();
onClick(item);
Expand All @@ -40,7 +49,11 @@ const SSOBtnList = ({ providers, prefixText, isDisabled, onClick }: Props) => {
});

if ($btns.length === 0) {
return <h4> You have no SSO providers configured </h4>;
return (
<Text textAlign="center" bold pt={3}>
You have no SSO providers configured
</Text>
);
}

return (
Expand All @@ -55,6 +68,8 @@ type Props = {
isDisabled: boolean;
onClick(provider: AuthProvider): void;
providers: AuthProvider[];
// autoFocus focuses on the first button in list.
autoFocus?: boolean;
};

export default SSOBtnList;
Loading

0 comments on commit e1ec35a

Please # to comment.