Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions web/packages/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"react-dom": "^18.2.0",
"react-is": "^16.8.0",
"react-refresh": "^0.14.0",
"react-select-event": "^5.5.1",
"react-test-renderer": "^18.2.0",
"react-transition-group": "^4.4.2",
"rollup-plugin-visualizer": "^5.9.0",
Expand Down
2 changes: 2 additions & 0 deletions web/packages/shared/utils/createMfaOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import { Auth2faType, PreferredMfaType } from 'shared/services/types';

export default function createMfaOptions(opts: Options) {
console.log('CREATING MFA OPTIONS', opts);
Comment thread
bl-nero marked this conversation as resolved.
Outdated
const { auth2faType, required = false } = opts;
const mfaOptions: MfaOption[] = [];

Expand All @@ -40,6 +41,7 @@ export default function createMfaOptions(opts: Options) {
mfaOptions.push({ value: 'optional', label: 'None' });
}

console.log(mfaOptions);
Comment thread
bl-nero marked this conversation as resolved.
Outdated
return mfaOptions;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import Box from 'design/Box';
import { ButtonPrimary, ButtonSecondary } from 'design/Button';
import Dialog from 'design/Dialog';
import Flex from 'design/Flex';
import * as Icon from 'design/Icon';
import Image from 'design/Image';
import Indicator from 'design/Indicator';
import { RadioGroup } from 'design/RadioGroup';
Expand All @@ -35,7 +34,8 @@ import { useAsync } from 'shared/hooks/useAsync';
import useAttempt from 'shared/hooks/useAttemptNext';
import { Auth2faType } from 'shared/services';
import createMfaOptions, { MfaOption } from 'shared/utils/createMfaOptions';
import styled from 'styled-components';

import { PasskeyIcons } from 'teleport/components/PasskeyIcons';

import { DialogHeader } from 'teleport/Account/DialogHeader';
import useReAuthenticate from 'teleport/components/ReAuthenticate/useReAuthenticate';
Expand Down Expand Up @@ -517,18 +517,7 @@ function PasskeyBlurb() {
borderRadius={3}
p={3}
>
<OverlappingChip>
<Icon.FingerprintSimple p={2} />
</OverlappingChip>
<OverlappingChip>
<Icon.UsbDrive p={2} />
</OverlappingChip>
<OverlappingChip>
<Icon.UserFocus p={2} />
</OverlappingChip>
<OverlappingChip>
<Icon.DeviceMobileCamera p={2} />
</OverlappingChip>
<PasskeyIcons />
<p>
Teleport supports passkeys, a password replacement that validates your
identity using touch, facial recognition, a device password, or a PIN.
Expand All @@ -540,12 +529,3 @@ function PasskeyBlurb() {
</Box>
);
}

const OverlappingChip = styled.span`
display: inline-block;
background: ${props => props.theme.colors.levels.surface};
border: ${props => props.theme.borders[1]};
border-color: ${props => props.theme.colors.interactive.tonal.neutral[2]};
border-radius: 50%;
margin-right: -6px;
`;
46 changes: 46 additions & 0 deletions web/packages/teleport/src/Login/Login.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*/

import React from 'react';
import { userEvent, UserEvent } from '@testing-library/user-event';
import selectEvent from 'react-select-event';
import { render, fireEvent, screen, waitFor } from 'design/utils/testing';

import auth from 'teleport/services/auth/auth';
Expand All @@ -25,10 +27,13 @@ import cfg from 'teleport/config';

import { Login } from './Login';

let user: UserEvent;

beforeEach(() => {
jest.restoreAllMocks();
jest.spyOn(history, 'push').mockImplementation();
jest.spyOn(history, 'getRedirectParam').mockImplementation(() => '/');
user = userEvent.setup();
});

test('basic rendering', () => {
Expand Down Expand Up @@ -58,6 +63,34 @@ test('login with redirect', async () => {
expect(history.push).toHaveBeenCalledWith('http://localhost/web', true);
});

test('login with MFA, changing method to OTP', async () => {
jest.spyOn(cfg, 'getAuth2faType').mockImplementation(() => 'optional');
jest.spyOn(auth, 'login').mockResolvedValue(null);

render(<Login />);

// fill form
const username = screen.getByLabelText(/username/i);
const password = screen.getByLabelText(/password/i);
const mfaType = screen.getByLabelText(/multi-factor type/i);
await user.type(username, 'username');
await user.type(password, '123');

expect(
screen.queryByLabelText(/authenticator code/i)
).not.toBeInTheDocument();
await selectEvent.select(mfaType, 'Authenticator App');
const authCode = screen.getByLabelText(/authenticator code/i);
await user.type(authCode, '987654');

// test login and redirect
fireEvent.click(screen.getByText('Sign In'));
await waitFor(() => {
expect(auth.login).toHaveBeenCalledWith('username', '123', '987654');
});
expect(history.push).toHaveBeenCalledWith('http://localhost/web', true);
});

test('login with SSO', () => {
jest.spyOn(cfg, 'getAuth2faType').mockImplementation(() => 'otp');
jest.spyOn(cfg, 'getPrimaryAuthType').mockImplementation(() => 'sso');
Expand All @@ -80,6 +113,19 @@ test('login with SSO', () => {
);
});

test('passwordless login', async () => {
jest.spyOn(cfg, 'getPrimaryAuthType').mockReturnValue('passwordless');
jest.spyOn(auth, 'loginWithWebauthn').mockResolvedValue(undefined);

render(<Login />);

await user.click(
screen.getByRole('button', { name: 'Sign in with a Passkey' })
);
expect(auth.loginWithWebauthn).toHaveBeenCalledWith(undefined); // No credentials
expect(history.push).toHaveBeenCalledWith('http://localhost/web', true);
});

describe('test MOTD', () => {
test('show motd only if motd is set', async () => {
// default login form
Expand Down
21 changes: 4 additions & 17 deletions web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import React from 'react';
import FormLogin, { Props } from './FormLogin';

const props: Props = {
title: 'Custom Title',
attempt: {
isFailed: false,
isSuccess: false,
Expand Down Expand Up @@ -74,7 +73,6 @@ export const LocalWithOnAndPwdless = () => (
export const Cloud = () => (
<FormLogin
{...props}
title="Teleport Cloud"
auth2faType="on"
isRecoveryEnabled={true}
onRecover={() => null}
Expand All @@ -89,7 +87,7 @@ export const ServerError = () => {
'invalid credentials with looooooooooooooooooooooooooooooooong text',
};

return <FormLogin {...props} title="Welcome!" attempt={attempt} />;
return <FormLogin {...props} attempt={attempt} />;
};

export const LocalWithSso = () => {
Expand All @@ -98,7 +96,7 @@ export const LocalWithSso = () => {
{ name: 'google', type: 'oidc', url: '' } as const,
];

return <FormLogin {...props} title="Welcome!" authProviders={ssoProvider} />;
return <FormLogin {...props} authProviders={ssoProvider} />;
};

export const LocalWithSsoAndPwdless = () => {
Expand All @@ -114,7 +112,6 @@ export const LocalWithSsoAndPwdless = () => {
return (
<FormLogin
{...props}
title="Welcome!"
authProviders={ssoProvider}
isPasswordlessEnabled={true}
/>
Expand All @@ -130,15 +127,14 @@ export const LocalDisabledWithSso = () => {
return (
<FormLogin
{...props}
title="Welcome!"
authProviders={ssoProvider}
isLocalAuthEnabled={false}
/>
);
};

export const LocalDisabledNoSso = () => (
<FormLogin {...props} title="Welcome!" isLocalAuthEnabled={false} />
<FormLogin {...props} isLocalAuthEnabled={false} />
);

export const PrimarySso = () => {
Expand All @@ -160,12 +156,7 @@ export const PrimarySso = () => {
];

return (
<FormLogin
{...props}
title="Welcome!"
primaryAuthType="sso"
authProviders={ssoProvider}
/>
<FormLogin {...props} primaryAuthType="sso" authProviders={ssoProvider} />
);
};

Expand All @@ -178,7 +169,6 @@ export const PrimarySsoWithPwdless = () => {
return (
<FormLogin
{...props}
title="Welcome!"
primaryAuthType="sso"
authProviders={ssoProvider}
isPasswordlessEnabled={true}
Expand All @@ -195,7 +185,6 @@ export const PrimarySsoWithSecondFactor = () => {
return (
<FormLogin
{...props}
title="Welcome!"
primaryAuthType="sso"
auth2faType="on"
authProviders={ssoProvider}
Expand All @@ -212,7 +201,6 @@ export const PrimaryPwdless = () => {
return (
<FormLogin
{...props}
title="Welcome!"
primaryAuthType="passwordless"
auth2faType="webauthn"
authProviders={ssoProvider}
Expand All @@ -224,7 +212,6 @@ export const PrimaryPwdlessWithNoSso = () => {
return (
<FormLogin
{...props}
title="Welcome!"
primaryAuthType="passwordless"
auth2faType="optional"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ test('primary username and password with mfa off', () => {
target: { value: '123' },
});

fireEvent.click(screen.getByText(/sign in/i));
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));

expect(onLogin).toHaveBeenCalledWith('username', '123', '');
});
Expand All @@ -64,7 +64,7 @@ test('auth2faType: otp', () => {
fireEvent.change(screen.getByPlaceholderText(/123 456/i), {
target: { value: '456' },
});
fireEvent.click(screen.getByText(/sign in/i));
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));

expect(onLogin).toHaveBeenCalledWith('username', '123', '456');
});
Expand All @@ -91,7 +91,7 @@ test('auth2faType: webauthn', async () => {
target: { value: '123' },
});

fireEvent.click(screen.getByText(/sign in/i));
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
expect(onLoginWithWebauthn).toHaveBeenCalledWith({
username: 'username',
password: '123',
Expand All @@ -113,7 +113,7 @@ test('input validation error handling', async () => {
/>
);

fireEvent.click(screen.getByText(/sign in/i));
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));

expect(onLogin).not.toHaveBeenCalled();
expect(onLoginWithSso).not.toHaveBeenCalled();
Expand Down
Loading