Cleaner login flows; hide options that aren't relevant to the user
This commit is contained in:
parent
fac4902ccc
commit
a4359064ca
|
@ -14,9 +14,16 @@ interface Values {
|
||||||
recoveryCode: '',
|
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 { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
|
|
||||||
const onSubmit = ({ code, recoveryCode }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
const onSubmit = ({ code, recoveryCode }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
|
@ -47,9 +54,9 @@ export default ({ history, location }: OwnProps) => {
|
||||||
return (
|
return (
|
||||||
<Formik initialValues={{ code: '', recoveryCode: '' }} onSubmit={onSubmit}>
|
<Formik initialValues={{ code: '', recoveryCode: '' }} onSubmit={onSubmit}>
|
||||||
{({ isSubmitting, setFieldValue }) => (
|
{({ isSubmitting, setFieldValue }) => (
|
||||||
<LoginFormContainer title={'Two-Factor Authentication'} css={tw`w-full flex h-full`}>
|
<LoginFormContainer title={'Two-Factor Authentication'}>
|
||||||
<div css={tw`flex flex-col`}>
|
<div css={tw`flex flex-col h-full`}>
|
||||||
<div css={tw`flex-1`}>
|
<div css={tw`flex-1 mb-12`}>
|
||||||
<Field
|
<Field
|
||||||
light
|
light
|
||||||
name={isMissingDevice ? 'recoveryCode' : 'code'}
|
name={isMissingDevice ? 'recoveryCode' : 'code'}
|
||||||
|
@ -67,13 +74,14 @@ export default ({ history, location }: OwnProps) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
css={tw`mt-12 w-full block`}
|
css={tw`w-full block`}
|
||||||
type={'submit'}
|
type={'submit'}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
>
|
>
|
||||||
Login
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
|
{(!isMissingDevice || (isMissingDevice && (location.state?.methods || []).includes('totp'))) &&
|
||||||
<button
|
<button
|
||||||
type={'button'}
|
type={'button'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -85,6 +93,7 @@ export default ({ history, location }: OwnProps) => {
|
||||||
>
|
>
|
||||||
{!isMissingDevice ? 'I\'ve Lost My Device' : 'I Have My Device'}
|
{!isMissingDevice ? 'I\'ve Lost My Device' : 'I Have My Device'}
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</LoginFormContainer>
|
</LoginFormContainer>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -50,17 +50,16 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.methods?.includes('webauthn')) {
|
response.methods = response.methods || [];
|
||||||
|
|
||||||
|
if (response.methods.includes('webauthn')) {
|
||||||
history.replace('/auth/login/key', {
|
history.replace('/auth/login/key', {
|
||||||
token: response.confirmationToken,
|
token: response.confirmationToken,
|
||||||
|
methods: response.methods,
|
||||||
publicKey: response.publicKey,
|
publicKey: response.publicKey,
|
||||||
hasTotp: response.methods?.includes('totp'),
|
|
||||||
});
|
});
|
||||||
return;
|
} else if (response.methods.includes('totp')) {
|
||||||
}
|
history.replace('/auth/login/checkpoint', { token: response.confirmationToken, methods: response.methods });
|
||||||
|
|
||||||
if (response.methods?.includes('totp')) {
|
|
||||||
history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(async (error) => {
|
.catch(async (error) => {
|
||||||
|
|
|
@ -2,19 +2,14 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import { DivContainer as LoginFormContainer } from '@/components/auth/LoginFormContainer';
|
import { DivContainer as LoginFormContainer } from '@/components/auth/LoginFormContainer';
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
import { useLocation } from 'react-router';
|
import { StaticContext } from 'react-router';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
import { authenticateSecurityKey } from '@/api/account/security-keys';
|
import { authenticateSecurityKey } from '@/api/account/security-keys';
|
||||||
import { base64Decode, bufferDecode, bufferEncode, decodeSecurityKeyCredentials } from '@/helpers';
|
import { base64Decode, bufferDecode, bufferEncode, decodeSecurityKeyCredentials } from '@/helpers';
|
||||||
import { FingerPrintIcon } from '@heroicons/react/outline';
|
import { FingerPrintIcon } from '@heroicons/react/outline';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
|
import { LoginCheckpointState } from '@/components/auth/LoginCheckpointContainer';
|
||||||
interface LocationParams {
|
|
||||||
token: string;
|
|
||||||
publicKey: any;
|
|
||||||
hasTotp: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Credential extends PublicKeyCredential {
|
interface Credential extends PublicKeyCredential {
|
||||||
response: AuthenticatorAssertionResponse;
|
response: AuthenticatorAssertionResponse;
|
||||||
|
@ -34,9 +29,9 @@ const challenge = async (publicKey: PublicKeyCredentialRequestOptions, signal?:
|
||||||
return credential;
|
return credential;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default () => {
|
type Props = RouteComponentProps<Record<string, string | undefined>, StaticContext, LoginCheckpointState | undefined>;
|
||||||
const history = useHistory();
|
|
||||||
const location = useLocation<LocationParams>();
|
export default ({ history, location }: Props) => {
|
||||||
const controller = useRef(new AbortController());
|
const controller = useRef(new AbortController());
|
||||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
const [ redirecting, setRedirecting ] = useState(false);
|
const [ redirecting, setRedirecting ] = useState(false);
|
||||||
|
@ -44,12 +39,12 @@ export default () => {
|
||||||
const triggerChallengePrompt = () => {
|
const triggerChallengePrompt = () => {
|
||||||
clearFlashes();
|
clearFlashes();
|
||||||
|
|
||||||
challenge(location.state.publicKey, controller.current.signal)
|
challenge(location.state!.publicKey!, controller.current.signal)
|
||||||
.then((credential) => {
|
.then((credential) => {
|
||||||
setRedirecting(true);
|
setRedirecting(true);
|
||||||
|
|
||||||
return authenticateSecurityKey({
|
return authenticateSecurityKey({
|
||||||
confirmation_token: location.state.token,
|
confirmation_token: location.state!.token,
|
||||||
data: JSON.stringify({
|
data: JSON.stringify({
|
||||||
id: credential.id,
|
id: credential.id,
|
||||||
type: credential.type,
|
type: credential.type,
|
||||||
|
@ -88,7 +83,7 @@ export default () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!location.state?.token) {
|
if (!location.state?.token || !location.state?.publicKey) {
|
||||||
history.replace('/auth/login');
|
history.replace('/auth/login');
|
||||||
} else {
|
} else {
|
||||||
triggerChallengePrompt();
|
triggerChallengePrompt();
|
||||||
|
@ -103,7 +98,7 @@ export default () => {
|
||||||
>
|
>
|
||||||
<SpinnerOverlay size={'base'} visible={redirecting}/>
|
<SpinnerOverlay size={'base'} visible={redirecting}/>
|
||||||
<div css={tw`flex flex-col md:h-full`}>
|
<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`}>Insert your security key and touch it.</p>
|
||||||
<p css={tw`text-neutral-700 mt-2`}>
|
<p css={tw`text-neutral-700 mt-2`}>
|
||||||
If your security key does not respond,
|
If your security key does not respond,
|
||||||
|
@ -116,16 +111,18 @@ export default () => {
|
||||||
</a>.
|
</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{(location.state?.methods || []).includes('totp') &&
|
||||||
<Link
|
<Link
|
||||||
css={tw`block mt-12 mb-6`}
|
css={tw`block mb-6`}
|
||||||
to={{ pathname: '/auth/login/checkpoint', state: location.state }}
|
to={{ pathname: '/auth/login/checkpoint', state: location.state }}
|
||||||
>
|
>
|
||||||
<Button size={'small'} type={'button'} css={tw`block w-full`}>
|
<Button size={'small'} type={'button'} css={tw`block w-full`}>
|
||||||
Use a Different Method
|
Use a Different Method
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
}
|
||||||
<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`}
|
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'}
|
{'I\'ve Lost My Device'}
|
||||||
|
|
|
@ -139,7 +139,7 @@ const SetupTwoFactorModal = () => {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6 md:mt-0 text-right`}>
|
<div css={tw`mt-6 text-right`}>
|
||||||
<Button>Setup</Button>
|
<Button>Setup</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue