ui(auth): add support for using a security key
This commit is contained in:
parent
3c21770c25
commit
59f2ea37d8
6 changed files with 195 additions and 70 deletions
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import { StaticContext } from 'react-router';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { StaticContext, useLocation } from 'react-router';
|
||||
import { Link, RouteComponentProps, useHistory } from 'react-router-dom';
|
||||
import loginCheckpoint from '@/api/auth/loginCheckpoint';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import { ActionCreator } from 'easy-peasy';
|
||||
|
@ -23,54 +23,80 @@ type Props = OwnProps & {
|
|||
}
|
||||
|
||||
const LoginCheckpointContainer = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const { isSubmitting, setFieldValue } = useFormikContext<Values>();
|
||||
const [ isMissingDevice, setIsMissingDevice ] = useState(false);
|
||||
|
||||
const switchToSecurityKey = () => {
|
||||
history.replace('/auth/login/key', { ...location.state });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue('code', '');
|
||||
setFieldValue('recoveryCode', '');
|
||||
setIsMissingDevice(location.state?.recovery || false);
|
||||
}, [ location.state ]);
|
||||
|
||||
return (
|
||||
<LoginFormContainer title={'Device Checkpoint'} css={tw`w-full flex`}>
|
||||
<div css={tw`mt-6`}>
|
||||
<Field
|
||||
light
|
||||
name={isMissingDevice ? 'recoveryCode' : 'code'}
|
||||
title={isMissingDevice ? 'Recovery Code' : 'Authentication Code'}
|
||||
description={
|
||||
isMissingDevice
|
||||
? 'Enter one of the recovery codes generated when you setup 2-Factor authentication on this account in order to continue.'
|
||||
: 'Enter the two-factor token generated by your device.'
|
||||
}
|
||||
type={'text'}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
<Button
|
||||
size={'xlarge'}
|
||||
type={'submit'}
|
||||
disabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<span
|
||||
onClick={() => {
|
||||
setFieldValue('code', '');
|
||||
setFieldValue('recoveryCode', '');
|
||||
setIsMissingDevice(s => !s);
|
||||
}}
|
||||
css={tw`cursor-pointer text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
{!isMissingDevice ? 'I\'ve Lost My Device' : 'I Have My Device'}
|
||||
</span>
|
||||
</div>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
to={'/auth/login'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
<div css={tw`flex flex-col items-center justify-center w-full md:h-full md:pt-4`}>
|
||||
<div>
|
||||
<Field
|
||||
light
|
||||
name={isMissingDevice ? 'recoveryCode' : 'code'}
|
||||
title={isMissingDevice ? 'Recovery Code' : 'Authentication Code'}
|
||||
description={
|
||||
isMissingDevice
|
||||
? 'Enter one of the recovery codes generated when you setup 2-Factor authentication on this account in order to continue.'
|
||||
: 'Enter the two-factor token generated by your device.'
|
||||
}
|
||||
type={'text'}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-6 md:mt-auto`}>
|
||||
<Button
|
||||
size={'large'}
|
||||
type={'submit'}
|
||||
disabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div css={tw`flex flex-row text-center mt-6 md:mt-auto`}>
|
||||
<div css={tw`mr-4`}>
|
||||
<a
|
||||
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700 text-center cursor-pointer`}
|
||||
onClick={() => switchToSecurityKey()}
|
||||
>
|
||||
Use security key
|
||||
</a>
|
||||
</div>
|
||||
<div css={tw`ml-4`}>
|
||||
<span
|
||||
onClick={() => {
|
||||
setFieldValue('code', '');
|
||||
setFieldValue('recoveryCode', '');
|
||||
setIsMissingDevice(s => !s);
|
||||
}}
|
||||
css={tw`cursor-pointer text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
{!isMissingDevice ? 'I\'ve Lost My Device' : 'I Have My Device'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
to={'/auth/login'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
);
|
||||
|
|
|
@ -1,11 +1,112 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import webauthnChallenge from '@/api/account/webauthn/webauthnChallenge';
|
||||
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 Spinner from '@/components/elements/Spinner';
|
||||
import Button from '@/components/elements/Button';
|
||||
|
||||
interface LocationParams {
|
||||
token: string;
|
||||
publicKey: any;
|
||||
hasTotp: boolean;
|
||||
}
|
||||
|
||||
const LoginKeyCheckpointContainer = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation<LocationParams>();
|
||||
|
||||
const { clearAndAddHttpError } = useFlash();
|
||||
|
||||
const [ challenging, setChallenging ] = useState(false);
|
||||
|
||||
const switchToCode = () => {
|
||||
history.replace('/auth/login/checkpoint', { ...location.state, recovery: false });
|
||||
};
|
||||
|
||||
const switchToRecovery = () => {
|
||||
history.replace('/auth/login/checkpoint', { ...location.state, recovery: true });
|
||||
};
|
||||
|
||||
const doChallenge = () => {
|
||||
setChallenging(true);
|
||||
|
||||
webauthnChallenge(location.state.token, location.state.publicKey)
|
||||
.then(response => {
|
||||
if (!response.complete) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
window.location = response.intended || '/';
|
||||
})
|
||||
.catch(error => {
|
||||
clearAndAddHttpError({ error });
|
||||
console.error(error);
|
||||
setChallenging(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
doChallenge();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`} />
|
||||
<LoginFormContainer title={'Key Checkpoint'} css={tw`w-full flex`}>
|
||||
<div css={tw`flex flex-col items-center justify-center w-full md:h-full md:pt-4`}>
|
||||
<h3 css={tw`font-sans text-2xl text-center text-neutral-500 font-normal`}>Attempting challenge...</h3>
|
||||
|
||||
<div css={tw`mt-6 md:mt-auto`}>
|
||||
{challenging ?
|
||||
<Spinner size={'large'} isBlue/>
|
||||
:
|
||||
<Button onClick={() => doChallenge()}>
|
||||
Retry
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div css={tw`flex flex-row text-center mt-6 md:mt-auto`}>
|
||||
<div css={tw`mr-4`}>
|
||||
<a
|
||||
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700 text-center cursor-pointer`}
|
||||
onClick={() => switchToCode()}
|
||||
>
|
||||
Use two-factor token
|
||||
</a>
|
||||
</div>
|
||||
<div css={tw`ml-4`}>
|
||||
<a
|
||||
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700 text-center cursor-pointer`}
|
||||
onClick={() => switchToRecovery()}
|
||||
>
|
||||
I've Lost My Device
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
to={'/auth/login'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginKeyCheckpointContainer;
|
||||
export default () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation<LocationParams>();
|
||||
|
||||
if (!location.state?.token) {
|
||||
history.replace('/auth/login');
|
||||
return null;
|
||||
}
|
||||
|
||||
return <LoginKeyCheckpointContainer/>;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue