mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
b34b10322b
introduced a regression where including deps in the associated useCallback functions would cause React to re-render components.
This resulted in unexpected symptoms like multiple Duo push requests, even if a successful or errored request had already been received.
Empty deps/no re-rendering for the respective callbacks is an expected result therefore we can safely ignore these issues the linter is suggesting needs to be fixed.
147 lines
5.4 KiB
TypeScript
147 lines
5.4 KiB
TypeScript
import React, { useCallback, useEffect, useState, Fragment } from "react";
|
|
import MethodContainer, { State as MethodContainerState } from "./MethodContainer";
|
|
import { makeStyles, Button, useTheme } from "@material-ui/core";
|
|
import { initiateU2FSignin, completeU2FSignin } from "../../../services/SecurityKey";
|
|
import u2fApi from "u2f-api";
|
|
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
|
import { useIsMountedRef } from "../../../hooks/Mounted";
|
|
import { useTimer } from "../../../hooks/Timer";
|
|
import LinearProgressBar from "../../../components/LinearProgressBar";
|
|
import FingerTouchIcon from "../../../components/FingerTouchIcon";
|
|
import FailureIcon from "../../../components/FailureIcon";
|
|
import IconWithContext from "./IconWithContext";
|
|
import { CSSProperties } from "@material-ui/styles";
|
|
import { AuthenticationLevel } from "../../../services/State";
|
|
|
|
export enum State {
|
|
WaitTouch = 1,
|
|
SigninInProgress = 2,
|
|
Failure = 3,
|
|
}
|
|
|
|
export interface Props {
|
|
id: string;
|
|
authenticationLevel: AuthenticationLevel;
|
|
registered: boolean;
|
|
|
|
onRegisterClick: () => void;
|
|
onSignInError: (err: Error) => void;
|
|
onSignInSuccess: (redirectURL: string | undefined) => void;
|
|
}
|
|
|
|
const SecurityKeyMethod = function (props: Props) {
|
|
const signInTimeout = 30;
|
|
const [state, setState] = useState(State.WaitTouch);
|
|
const style = useStyles();
|
|
const redirectionURL = useRedirectionURL();
|
|
const mounted = useIsMountedRef();
|
|
const [timerPercent, triggerTimer,] = useTimer(signInTimeout * 1000 - 500);
|
|
|
|
const { onSignInSuccess, onSignInError } = props;
|
|
/* eslint-disable react-hooks/exhaustive-deps */
|
|
const onSignInErrorCallback = useCallback(onSignInError, []);
|
|
const onSignInSuccessCallback = useCallback(onSignInSuccess, []);
|
|
/* eslint-enable react-hooks/exhaustive-deps */
|
|
|
|
const doInitiateSignIn = useCallback(async () => {
|
|
// If user is already authenticated, we don't initiate sign in process.
|
|
if (!props.registered || props.authenticationLevel >= AuthenticationLevel.TwoFactor) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
triggerTimer();
|
|
setState(State.WaitTouch);
|
|
const signRequest = await initiateU2FSignin();
|
|
const signRequests: u2fApi.SignRequest[] = [];
|
|
for (var i in signRequest.registeredKeys) {
|
|
const r = signRequest.registeredKeys[i];
|
|
signRequests.push({
|
|
appId: signRequest.appId,
|
|
challenge: signRequest.challenge,
|
|
keyHandle: r.keyHandle,
|
|
version: r.version,
|
|
})
|
|
}
|
|
const signResponse = await u2fApi.sign(signRequests, signInTimeout);
|
|
// If the request was initiated and the user changed 2FA method in the meantime,
|
|
// the process is interrupted to avoid updating state of unmounted component.
|
|
if (!mounted.current) return;
|
|
|
|
setState(State.SigninInProgress);
|
|
const res = await completeU2FSignin(signResponse, redirectionURL);
|
|
onSignInSuccessCallback(res ? res.redirect : undefined);
|
|
} catch (err) {
|
|
// If the request was initiated and the user changed 2FA method in the meantime,
|
|
// the process is interrupted to avoid updating state of unmounted component.
|
|
if (!mounted.current) return;
|
|
console.error(err);
|
|
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
|
|
setState(State.Failure);
|
|
}
|
|
}, [onSignInSuccessCallback, onSignInErrorCallback, redirectionURL, mounted, triggerTimer, props.authenticationLevel, props.registered]);
|
|
|
|
useEffect(() => { doInitiateSignIn() }, [doInitiateSignIn]);
|
|
|
|
let methodState = MethodContainerState.METHOD;
|
|
if (props.authenticationLevel === AuthenticationLevel.TwoFactor) {
|
|
methodState = MethodContainerState.ALREADY_AUTHENTICATED;
|
|
} else if (!props.registered) {
|
|
methodState = MethodContainerState.NOT_REGISTERED;
|
|
}
|
|
|
|
return (
|
|
<MethodContainer
|
|
id={props.id}
|
|
title="Security Key"
|
|
explanation="Touch the token of your security key"
|
|
state={methodState}
|
|
onRegisterClick={props.onRegisterClick}>
|
|
<div className={style.icon}>
|
|
<Icon state={state} timer={timerPercent} onRetryClick={doInitiateSignIn} />
|
|
</div>
|
|
</MethodContainer>
|
|
)
|
|
}
|
|
|
|
export default SecurityKeyMethod
|
|
|
|
const useStyles = makeStyles(theme => ({
|
|
icon: {
|
|
display: "inline-block",
|
|
}
|
|
}));
|
|
|
|
interface IconProps {
|
|
state: State;
|
|
|
|
timer: number;
|
|
onRetryClick: () => void;
|
|
}
|
|
|
|
function Icon(props: IconProps) {
|
|
const state = props.state as State;
|
|
const theme = useTheme();
|
|
|
|
const progressBarStyle: CSSProperties = {
|
|
marginTop: theme.spacing(),
|
|
}
|
|
|
|
const touch = <IconWithContext
|
|
icon={<FingerTouchIcon size={64} animated strong />}
|
|
context={<LinearProgressBar value={props.timer} style={progressBarStyle} height={theme.spacing(2)} />}
|
|
className={state === State.WaitTouch ? undefined : "hidden"} />
|
|
|
|
const failure = <IconWithContext
|
|
icon={<FailureIcon />}
|
|
context={<Button color="secondary" onClick={props.onRetryClick}>Retry</Button>}
|
|
className={state === State.Failure ? undefined : "hidden"} />
|
|
|
|
return (
|
|
<Fragment>
|
|
{touch}
|
|
{failure}
|
|
</Fragment>
|
|
)
|
|
}
|