Cleaner login flows; hide options that aren't relevant to the user

This commit is contained in:
Dane Everitt 2022-02-13 17:44:59 -05:00
parent fac4902ccc
commit a4359064ca
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
4 changed files with 37 additions and 32 deletions

View file

@ -14,9 +14,16 @@ interface Values {
recoveryCode: '',
}
type OwnProps = RouteComponentProps<Record<string, string | undefined>, StaticContext, { token?: string, recovery?: boolean }>
export interface LoginCheckpointState {
token: string;
methods: string[];
publicKey?: PublicKeyCredentialRequestOptions;
recovery?: boolean;
}
export default ({ history, location }: OwnProps) => {
type Props = RouteComponentProps<Record<string, string | undefined>, StaticContext, LoginCheckpointState>;
export default ({ history, location }: Props) => {
const { clearFlashes, clearAndAddHttpError } = useFlash();
const onSubmit = ({ code, recoveryCode }: Values, { setSubmitting }: FormikHelpers<Values>) => {
@ -47,9 +54,9 @@ export default ({ history, location }: OwnProps) => {
return (
<Formik initialValues={{ code: '', recoveryCode: '' }} onSubmit={onSubmit}>
{({ isSubmitting, setFieldValue }) => (
<LoginFormContainer title={'Two-Factor Authentication'} css={tw`w-full flex h-full`}>
<div css={tw`flex flex-col`}>
<div css={tw`flex-1`}>
<LoginFormContainer title={'Two-Factor Authentication'}>
<div css={tw`flex flex-col h-full`}>
<div css={tw`flex-1 mb-12`}>
<Field
light
name={isMissingDevice ? 'recoveryCode' : 'code'}
@ -67,13 +74,14 @@ export default ({ history, location }: OwnProps) => {
/>
</div>
<Button
css={tw`mt-12 w-full block`}
css={tw`w-full block`}
type={'submit'}
disabled={isSubmitting}
isLoading={isSubmitting}
>
Login
Submit
</Button>
{(!isMissingDevice || (isMissingDevice && (location.state?.methods || []).includes('totp'))) &&
<button
type={'button'}
onClick={() => {
@ -85,6 +93,7 @@ export default ({ history, location }: OwnProps) => {
>
{!isMissingDevice ? 'I\'ve Lost My Device' : 'I Have My Device'}
</button>
}
</div>
</LoginFormContainer>
)}

View file

@ -50,17 +50,16 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
return;
}
if (response.methods?.includes('webauthn')) {
response.methods = response.methods || [];
if (response.methods.includes('webauthn')) {
history.replace('/auth/login/key', {
token: response.confirmationToken,
methods: response.methods,
publicKey: response.publicKey,
hasTotp: response.methods?.includes('totp'),
});
return;
}
if (response.methods?.includes('totp')) {
history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
} else if (response.methods.includes('totp')) {
history.replace('/auth/login/checkpoint', { token: response.confirmationToken, methods: response.methods });
}
})
.catch(async (error) => {

View file

@ -2,19 +2,14 @@ import React, { useEffect, useRef, useState } from 'react';
import tw from 'twin.macro';
import { DivContainer as LoginFormContainer } from '@/components/auth/LoginFormContainer';
import useFlash from '@/plugins/useFlash';
import { useLocation } from 'react-router';
import { Link, useHistory } from 'react-router-dom';
import { StaticContext } from 'react-router';
import { Link, RouteComponentProps } from 'react-router-dom';
import Button from '@/components/elements/Button';
import { authenticateSecurityKey } from '@/api/account/security-keys';
import { base64Decode, bufferDecode, bufferEncode, decodeSecurityKeyCredentials } from '@/helpers';
import { FingerPrintIcon } from '@heroicons/react/outline';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
interface LocationParams {
token: string;
publicKey: any;
hasTotp: boolean;
}
import { LoginCheckpointState } from '@/components/auth/LoginCheckpointContainer';
interface Credential extends PublicKeyCredential {
response: AuthenticatorAssertionResponse;
@ -34,9 +29,9 @@ const challenge = async (publicKey: PublicKeyCredentialRequestOptions, signal?:
return credential;
};
export default () => {
const history = useHistory();
const location = useLocation<LocationParams>();
type Props = RouteComponentProps<Record<string, string | undefined>, StaticContext, LoginCheckpointState | undefined>;
export default ({ history, location }: Props) => {
const controller = useRef(new AbortController());
const { clearFlashes, clearAndAddHttpError } = useFlash();
const [ redirecting, setRedirecting ] = useState(false);
@ -44,12 +39,12 @@ export default () => {
const triggerChallengePrompt = () => {
clearFlashes();
challenge(location.state.publicKey, controller.current.signal)
challenge(location.state!.publicKey!, controller.current.signal)
.then((credential) => {
setRedirecting(true);
return authenticateSecurityKey({
confirmation_token: location.state.token,
confirmation_token: location.state!.token,
data: JSON.stringify({
id: credential.id,
type: credential.type,
@ -88,7 +83,7 @@ export default () => {
});
useEffect(() => {
if (!location.state?.token) {
if (!location.state?.token || !location.state?.publicKey) {
history.replace('/auth/login');
} else {
triggerChallengePrompt();
@ -103,7 +98,7 @@ export default () => {
>
<SpinnerOverlay size={'base'} visible={redirecting}/>
<div css={tw`flex flex-col md:h-full`}>
<div css={tw`flex-1`}>
<div css={tw`flex-1 mb-12`}>
<p css={tw`text-neutral-700`}>Insert your security key and touch it.</p>
<p css={tw`text-neutral-700 mt-2`}>
If your security key does not respond,&nbsp;
@ -116,16 +111,18 @@ export default () => {
</a>.
</p>
</div>
{(location.state?.methods || []).includes('totp') &&
<Link
css={tw`block mt-12 mb-6`}
css={tw`block mb-6`}
to={{ pathname: '/auth/login/checkpoint', state: location.state }}
>
<Button size={'small'} type={'button'} css={tw`block w-full`}>
Use a Different Method
</Button>
</Link>
}
<Link
to={{ pathname: '/auth/login/checkpoint', state: { token: location.state.token, recovery: true } }}
to={{ pathname: '/auth/login/checkpoint', state: { ...(location.state || {}), recovery: true } }}
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700 text-center cursor-pointer`}
>
{'I\'ve Lost My Device'}

View file

@ -139,7 +139,7 @@ const SetupTwoFactorModal = () => {
</div>
}
</div>
<div css={tw`mt-6 md:mt-0 text-right`}>
<div css={tw`mt-6 text-right`}>
<Button>Setup</Button>
</div>
</div>