[Web] Make language on mfa verify step dialog more clear (#20825)

* update text on dialog

* tweak language and overwrite error message

* address comments

* fix 'enter' to submit on newdevice form
This commit is contained in:
Yassine Bounekhla 2023-02-10 10:04:47 -05:00 committed by GitHub
parent f93d401df2
commit 6edb26c60f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 939 additions and 881 deletions

View file

@ -118,138 +118,143 @@ export function AddDevice({
onClose={onClose}
open={true}
>
<DialogHeader style={{ flexDirection: 'column' }}>
<DialogTitle>Add New Two-Factor Device</DialogTitle>
</DialogHeader>
{addDeviceAttempt.status === 'failed' && (
<Danger mt={2} width="100%">
{addDeviceAttempt.statusText}
</Danger>
)}
{fetchQrCodeAttempt.status === 'failed' && (
<Danger mt={2} width="100%">
{fetchQrCodeAttempt.statusText}
</Danger>
)}
<DialogContent>
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
bg="primary.light"
borderRadius={8}
height="256px"
p={3}
mb={4}
>
{mfaOption.value === 'otp' && (
<>
<Flex
height="168px"
justifyContent="center"
alignItems="center"
>
{fetchQrCodeAttempt.status === 'processing' && (
<Indicator />
)}
{fetchQrCodeAttempt.status === 'success' && (
<Image
src={`data:image/png;base64,${qrCode}`}
height="100%"
style={{
boxSizing: 'border-box',
border: '8px solid white',
}}
/>
)}
</Flex>
<Text fontSize={1} textAlign="center" mt={2}>
Scan the QR Code with any authenticator app and enter the
generated code.{' '}
<Text color="text.secondary">
We recommend{' '}
<Link href="https://authy.com/download/" target="_blank">
Authy
</Link>
.
<form>
<DialogHeader style={{ flexDirection: 'column' }}>
<DialogTitle>Add New Two-Factor Device</DialogTitle>
</DialogHeader>
{addDeviceAttempt.status === 'failed' && (
<Danger mt={2} width="100%">
{addDeviceAttempt.statusText}
</Danger>
)}
{fetchQrCodeAttempt.status === 'failed' && (
<Danger mt={2} width="100%">
{fetchQrCodeAttempt.statusText}
</Danger>
)}
<DialogContent>
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
bg="primary.light"
borderRadius={8}
height="256px"
p={3}
mb={4}
>
{mfaOption.value === 'otp' && (
<>
<Flex
height="168px"
justifyContent="center"
alignItems="center"
>
{fetchQrCodeAttempt.status === 'processing' && (
<Indicator />
)}
{fetchQrCodeAttempt.status === 'success' && (
<Image
src={`data:image/png;base64,${qrCode}`}
height="100%"
style={{
boxSizing: 'border-box',
border: '8px solid white',
}}
/>
)}
</Flex>
<Text fontSize={1} textAlign="center" mt={2}>
Scan the QR Code with any authenticator app and enter the
generated code.{' '}
<Text color="text.secondary">
We recommend{' '}
<Link
href="https://authy.com/download/"
target="_blank"
>
Authy
</Link>
.
</Text>
</Text>
</Text>
</>
)}
{mfaOption.value === 'webauthn' && (
<>
<Image src={secKeyGraphic} height="168px" />
<Text mt={3}>{hardwareInstructions}</Text>
</>
)}
</Flex>
<Flex alignItems="center">
<FieldSelect
maxWidth="50%"
width="100%"
label="Two-factor type"
data-testid="mfa-select"
value={mfaOption}
options={mfaOptions}
onChange={(o: MfaOption) => {
validator.reset();
onSetMfaOption(o);
}}
mr={3}
isDisabled={addDeviceAttempt.status === 'processing'}
/>
{mfaOption.value === 'otp' && (
<FieldInput
width="50%"
label="Authenticator code"
rule={requiredToken}
inputMode="numeric"
autoComplete="one-time-code"
value={otpToken}
onChange={e => setOtpToken(e.target.value)}
placeholder="123 456"
readonly={addDeviceAttempt.status === 'processing'}
/>
)}
{mfaOption.value === 'webauthn' && isPasswordlessEnabled && (
</>
)}
{mfaOption.value === 'webauthn' && (
<>
<Image src={secKeyGraphic} height="168px" />
<Text mt={3}>{hardwareInstructions}</Text>
</>
)}
</Flex>
<Flex alignItems="center">
<FieldSelect
width="50%"
label="Allow Passwordless Login?"
value={usageOption}
options={deviceUsageOpts}
onChange={(o: DeviceusageOpt) => setUsageOption(o)}
maxWidth="50%"
width="100%"
label="Two-factor type"
data-testid="mfa-select"
value={mfaOption}
options={mfaOptions}
onChange={(o: MfaOption) => {
validator.reset();
onSetMfaOption(o);
}}
mr={3}
isDisabled={addDeviceAttempt.status === 'processing'}
/>
)}
</Flex>
<FieldInput
rule={requiredField('Device name is required')}
label="Device name"
placeholder="Name"
width="100%"
autoFocus
value={deviceName}
type="text"
onChange={e => setDeviceName(e.target.value)}
readonly={addDeviceAttempt.status === 'processing'}
mb={1}
/>
</DialogContent>
<DialogFooter>
<ButtonPrimary
size="large"
width="45%"
type="submit"
onClick={e => validator.validate() && onSubmit(e)}
disabled={addDeviceAttempt.status === 'processing'}
mr={3}
>
Add device
</ButtonPrimary>
<ButtonSecondary size="large" width="30%" onClick={onClose}>
Cancel
</ButtonSecondary>
</DialogFooter>
{mfaOption.value === 'otp' && (
<FieldInput
width="50%"
label="Authenticator code"
rule={requiredToken}
inputMode="numeric"
autoComplete="one-time-code"
value={otpToken}
onChange={e => setOtpToken(e.target.value)}
placeholder="123 456"
readonly={addDeviceAttempt.status === 'processing'}
/>
)}
{mfaOption.value === 'webauthn' && isPasswordlessEnabled && (
<FieldSelect
width="50%"
label="Allow Passwordless Login?"
value={usageOption}
options={deviceUsageOpts}
onChange={(o: DeviceusageOpt) => setUsageOption(o)}
isDisabled={addDeviceAttempt.status === 'processing'}
/>
)}
</Flex>
<FieldInput
rule={requiredField('Device name is required')}
label="Device name"
placeholder="Name"
width="100%"
autoFocus
value={deviceName}
type="text"
onChange={e => setDeviceName(e.target.value)}
readonly={addDeviceAttempt.status === 'processing'}
mb={1}
/>
</DialogContent>
<DialogFooter>
<ButtonPrimary
size="large"
width="45%"
type="submit"
onClick={e => validator.validate() && onSubmit(e)}
disabled={addDeviceAttempt.status === 'processing'}
mr={3}
>
Add device
</ButtonPrimary>
<ButtonSecondary size="large" width="30%" onClick={onClose}>
Cancel
</ButtonSecondary>
</DialogFooter>
</form>
</Dialog>
)}
</Validation>

View file

@ -97,6 +97,7 @@ export function ManageDevices({
<ReAuthenticate
onAuthenticated={setToken}
onClose={hideReAuthenticate}
actionText="registering a new device"
/>
)}
{isAddDeviceVisible && (

View file

@ -44,4 +44,5 @@ const props: State = {
preferredMfaType: 'webauthn',
onClose: () => null,
auth2faType: 'on',
actionText: 'performing this action',
};

View file

@ -44,6 +44,7 @@ export function ReAuthenticate({
onClose,
auth2faType,
preferredMfaType,
actionText,
}: State) {
const [otpToken, setOtpToken] = useState('');
const mfaOptions = createMfaOptions({
@ -69,68 +70,72 @@ export function ReAuthenticate({
{({ validator }) => (
<Dialog
dialogCss={() => ({
width: '400px',
width: '416px',
})}
disableEscapeKeyDown={false}
onClose={onClose}
open={true}
>
<DialogHeader style={{ flexDirection: 'column' }}>
<DialogTitle>Verify your identity</DialogTitle>
<Text textAlign="center" color="text.secondary">
You must verify your identity before peforming this action.
</Text>
</DialogHeader>
{attempt.status === 'failed' && (
<Danger mt={2} width="100%">
{attempt.statusText}
</Danger>
)}
<DialogContent>
<Flex mt={2} alignItems="flex-end">
<FieldSelect
width="50%"
label="Two-factor type"
value={mfaOption}
options={mfaOptions}
onChange={(o: MfaOption) => {
setMfaOption(o);
clearAttempt();
}}
data-testid="mfa-select"
<form>
<DialogHeader style={{ flexDirection: 'column' }}>
<DialogTitle>Verify your identity</DialogTitle>
<Text textAlign="center" color="text.secondary">
You must verify your identity with one of your existing
two-factor devices before {actionText}.
</Text>
</DialogHeader>
{attempt.status === 'failed' && (
<Danger mt={2} width="100%">
{attempt.statusText}
</Danger>
)}
<DialogContent>
<Flex mt={2} alignItems="flex-end">
<FieldSelect
width="50%"
label="Two-factor type"
value={mfaOption}
options={mfaOptions}
onChange={(o: MfaOption) => {
setMfaOption(o);
clearAttempt();
}}
data-testid="mfa-select"
mr={3}
mb={0}
isDisabled={attempt.status === 'processing'}
/>
<Box width="50%">
{mfaOption.value === 'otp' && (
<FieldInput
label="Authenticator code"
rule={requiredToken}
inputMode="numeric"
autoComplete="one-time-code"
value={otpToken}
onChange={e => setOtpToken(e.target.value)}
placeholder="123 456"
readonly={attempt.status === 'processing'}
mb={0}
/>
)}
</Box>
</Flex>
</DialogContent>
<DialogFooter>
<ButtonPrimary
onClick={e => validator.validate() && onSubmit(e)}
disabled={attempt.status === 'processing'}
mr={3}
mb={0}
isDisabled={attempt.status === 'processing'}
/>
<Box width="50%">
{mfaOption.value === 'otp' && (
<FieldInput
label="Authenticator code"
rule={requiredToken}
inputMode="numeric"
autoComplete="one-time-code"
value={otpToken}
onChange={e => setOtpToken(e.target.value)}
placeholder="123 456"
readonly={attempt.status === 'processing'}
mb={0}
/>
)}
</Box>
</Flex>
</DialogContent>
<DialogFooter>
<ButtonPrimary
onClick={e => validator.validate() && onSubmit(e)}
disabled={attempt.status === 'processing'}
mr={3}
mt={3}
autoFocus
>
Continue
</ButtonPrimary>
<ButtonSecondary onClick={onClose}>Cancel</ButtonSecondary>
</DialogFooter>
mt={3}
type="submit"
autoFocus
>
Continue
</ButtonPrimary>
<ButtonSecondary onClick={onClose}>Cancel</ButtonSecondary>
</DialogFooter>
</form>
</Dialog>
)}
</Validation>

View file

@ -203,7 +203,7 @@ exports[`render failed state for re-authentication dialog 1`] = `
position: relative;
overflow-y: auto;
max-height: calc(100% - 96px);
width: 400px;
width: 416px;
}
.c4 {
@ -313,124 +313,129 @@ exports[`render failed state for re-authentication dialog 1`] = `
class="c3"
data-testid="dialogbox"
>
<div
class="c4"
style="flex-direction: column;"
>
<form>
<div
class="c5"
color="text.primary"
>
Verify your identity
</div>
<div
class="c6"
color="text.secondary"
>
You must verify your identity before peforming this action.
</div>
</div>
<div
class="c7"
kind="danger"
width="100%"
>
an error has occurred
</div>
<div
class="c8"
>
<div
class="c9"
class="c4"
style="flex-direction: column;"
>
<div
class="c10"
data-testid="mfa-select"
width="50%"
class="c5"
color="text.primary"
>
Verify your identity
</div>
<div
class="c6"
color="text.secondary"
>
You must verify your identity with one of your existing two-factor devices before
performing this action
.
</div>
</div>
<div
class="c7"
kind="danger"
width="100%"
>
an error has occurred
</div>
<div
class="c8"
>
<div
class="c9"
>
<label
class="c11"
font-size="0"
for="select"
>
Two-factor type
</label>
<div
class="c12"
class="c10"
data-testid="mfa-select"
width="50%"
>
<label
class="c11"
font-size="0"
for="select"
>
Two-factor type
</label>
<div
class="react-select-container css-2b097c-container"
class="c12"
>
<div
class="react-select__control css-yk16xz-control"
class="react-select-container css-2b097c-container"
>
<div
class="react-select__value-container react-select__value-container--has-value css-g1d714-ValueContainer"
class="react-select__control css-yk16xz-control"
>
<div
class="react-select__single-value css-1uccc91-singleValue"
class="react-select__value-container react-select__value-container--has-value css-g1d714-ValueContainer"
>
Hardware Key
</div>
<input
aria-autocomplete="list"
class="css-62g3xt-dummyInput"
id="select"
readonly=""
tabindex="0"
value=""
/>
</div>
<div
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="react-select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-6q0nyr-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
<div
class="react-select__single-value css-1uccc91-singleValue"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
Hardware Key
</div>
<input
aria-autocomplete="list"
class="css-62g3xt-dummyInput"
id="select"
readonly=""
tabindex="0"
value=""
/>
</div>
<div
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="react-select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-6q0nyr-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="c13"
width="50%"
/>
</div>
<div
class="c13"
width="50%"
/>
</div>
</div>
<div
class="c14"
>
<button
class="c15"
kind="primary"
<div
class="c14"
>
Continue
</button>
<button
class="c16"
kind="secondary"
>
Cancel
</button>
</div>
<button
class="c15"
kind="primary"
type="submit"
>
Continue
</button>
<button
class="c16"
kind="secondary"
>
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
@ -616,7 +621,7 @@ exports[`render re-authentication dialog 1`] = `
position: relative;
overflow-y: auto;
max-height: calc(100% - 96px);
width: 400px;
width: 416px;
}
.c4 {
@ -726,117 +731,122 @@ exports[`render re-authentication dialog 1`] = `
class="c3"
data-testid="dialogbox"
>
<div
class="c4"
style="flex-direction: column;"
>
<form>
<div
class="c5"
color="text.primary"
>
Verify your identity
</div>
<div
class="c6"
color="text.secondary"
>
You must verify your identity before peforming this action.
</div>
</div>
<div
class="c7"
>
<div
class="c8"
class="c4"
style="flex-direction: column;"
>
<div
class="c9"
data-testid="mfa-select"
width="50%"
class="c5"
color="text.primary"
>
Verify your identity
</div>
<div
class="c6"
color="text.secondary"
>
You must verify your identity with one of your existing two-factor devices before
performing this action
.
</div>
</div>
<div
class="c7"
>
<div
class="c8"
>
<label
class="c10"
font-size="0"
for="select"
>
Two-factor type
</label>
<div
class="c11"
class="c9"
data-testid="mfa-select"
width="50%"
>
<label
class="c10"
font-size="0"
for="select"
>
Two-factor type
</label>
<div
class="react-select-container css-2b097c-container"
class="c11"
>
<div
class="react-select__control css-yk16xz-control"
class="react-select-container css-2b097c-container"
>
<div
class="react-select__value-container react-select__value-container--has-value css-g1d714-ValueContainer"
class="react-select__control css-yk16xz-control"
>
<div
class="react-select__single-value css-1uccc91-singleValue"
class="react-select__value-container react-select__value-container--has-value css-g1d714-ValueContainer"
>
Hardware Key
</div>
<input
aria-autocomplete="list"
class="css-62g3xt-dummyInput"
id="select"
readonly=""
tabindex="0"
value=""
/>
</div>
<div
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="react-select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-6q0nyr-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
<div
class="react-select__single-value css-1uccc91-singleValue"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
Hardware Key
</div>
<input
aria-autocomplete="list"
class="css-62g3xt-dummyInput"
id="select"
readonly=""
tabindex="0"
value=""
/>
</div>
<div
class="react-select__indicators css-1hb7zxy-IndicatorsContainer"
>
<span
class="react-select__indicator-separator css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class="react-select__indicator react-select__dropdown-indicator css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-6q0nyr-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="c12"
width="50%"
/>
</div>
<div
class="c12"
width="50%"
/>
</div>
</div>
<div
class="c13"
>
<button
class="c14"
kind="primary"
<div
class="c13"
>
Continue
</button>
<button
class="c15"
kind="secondary"
>
Cancel
</button>
</div>
<button
class="c14"
kind="primary"
type="submit"
>
Continue
</button>
<button
class="c15"
kind="secondary"
>
Cancel
</button>
</div>
</form>
</div>
</div>
</div>

View file

@ -21,7 +21,11 @@ import useAttempt from 'shared/hooks/useAttemptNext';
import cfg from 'teleport/config';
import auth from 'teleport/services/auth';
export default function useReAuthenticate({ onAuthenticated, onClose }: Props) {
export default function useReAuthenticate({
onAuthenticated,
onClose,
actionText = defaultActionText,
}: Props) {
const { attempt, setAttempt, handleError } = useAttempt('');
function submitWithTotp(secondFactorToken: string) {
@ -37,7 +41,20 @@ export default function useReAuthenticate({ onAuthenticated, onClose }: Props) {
auth
.createPrivilegeTokenWithWebauthn()
.then(onAuthenticated)
.catch(handleError);
.catch((err: Error) => {
// This catches a webauthn frontend error that occurs on Firefox and replaces it with a more helpful error message.
if (
err.message.includes('attempt was made to use an object that is not')
) {
setAttempt({
status: 'failed',
statusText:
'The two-factor device you used is not registered on this account. You must verify using a device that has already been registered.',
});
} else {
setAttempt({ status: 'failed', statusText: err.message });
}
});
}
function clearAttempt() {
@ -51,13 +68,26 @@ export default function useReAuthenticate({ onAuthenticated, onClose }: Props) {
submitWithWebauthn,
auth2faType: cfg.getAuth2faType(),
preferredMfaType: cfg.getPreferredMfaType(),
actionText,
onClose,
};
}
const defaultActionText = 'performing this action';
export type Props = {
onAuthenticated: React.Dispatch<React.SetStateAction<string>>;
onClose: () => void;
/**
* The text that will be appended to the text in the re-authentication dialog.
*
* Default value: "performing this action"
*
* Example: If `actionText` is set to "registering a new device" then the dialog will say
* "You must verify your identity with one of your existing two-factor devices before registering a new device."
*
* */
actionText?: string;
};
export type State = ReturnType<typeof useReAuthenticate>;