mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
Let the user know device is not enrolled.
A message is now displayed to the user when he first sign in in Authelia letting him know that a device must be enrolled. Also introduce a message letting him know when he is already authenticated.
This commit is contained in:
parent
5942e00412
commit
5f8726fe87
7
web/src/components/InformationIcon.test.tsx
Normal file
7
web/src/components/InformationIcon.test.tsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { mount } from "enzyme";
|
||||||
|
import InformationIcon from "./InformationIcon";
|
||||||
|
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
mount(<InformationIcon />);
|
||||||
|
});
|
11
web/src/components/InformationIcon.tsx
Normal file
11
web/src/components/InformationIcon.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React from "react";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
export interface Props { }
|
||||||
|
|
||||||
|
export default function (props: Props) {
|
||||||
|
return (
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} size="4x" color="#5858ff" className="information-icon" />
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { getUserPreferences } from "../services/UserPreferences";
|
import { getUserPreferences } from "../services/UserPreferences";
|
||||||
import { useRemoteCall } from "../hooks/RemoteCall";
|
import { useRemoteCall } from "./RemoteCall";
|
||||||
|
|
||||||
export function useUserPreferences() {
|
export function useUserPreferences() {
|
||||||
return useRemoteCall(getUserPreferences, []);
|
return useRemoteCall(getUserPreferences, []);
|
|
@ -1,5 +1,7 @@
|
||||||
import { SecondFactorMethod } from "./Methods";
|
import { SecondFactorMethod } from "./Methods";
|
||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserInfo {
|
||||||
method: SecondFactorMethod;
|
method: SecondFactorMethod;
|
||||||
|
has_u2f: boolean;
|
||||||
|
has_totp: boolean;
|
||||||
}
|
}
|
|
@ -1,11 +1,17 @@
|
||||||
import { Get, PostWithOptionalResponse } from "./Client";
|
import { Get, PostWithOptionalResponse } from "./Client";
|
||||||
import { UserInfoPath, UserInfo2FAMethodPath } from "./Api";
|
import { UserInfoPath, UserInfo2FAMethodPath } from "./Api";
|
||||||
import { SecondFactorMethod } from "../models/Methods";
|
import { SecondFactorMethod } from "../models/Methods";
|
||||||
import { UserPreferences } from "../models/UserPreferences";
|
import { UserInfo } from "../models/UserInfo";
|
||||||
|
|
||||||
export type Method2FA = "u2f" | "totp" | "mobile_push";
|
export type Method2FA = "u2f" | "totp" | "mobile_push";
|
||||||
|
|
||||||
export interface UserPreferencesPayload {
|
export interface UserInfoPayload {
|
||||||
|
method: Method2FA;
|
||||||
|
has_u2f: boolean;
|
||||||
|
has_totp: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MethodPreferencePayload {
|
||||||
method: Method2FA;
|
method: Method2FA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,12 +37,12 @@ export function toString(method: SecondFactorMethod): Method2FA {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserPreferences(): Promise<UserPreferences> {
|
export async function getUserPreferences(): Promise<UserInfo> {
|
||||||
const res = await Get<UserPreferencesPayload>(UserInfoPath);
|
const res = await Get<UserInfoPayload>(UserInfoPath);
|
||||||
return { method: toEnum(res.method) };
|
return { ...res, method: toEnum(res.method) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setPrefered2FAMethod(method: SecondFactorMethod) {
|
export function setPrefered2FAMethod(method: SecondFactorMethod) {
|
||||||
return PostWithOptionalResponse(UserInfo2FAMethodPath,
|
return PostWithOptionalResponse(UserInfo2FAMethodPath,
|
||||||
{ method: toString(method) } as UserPreferencesPayload);
|
{ method: toString(method) } as MethodPreferencePayload);
|
||||||
}
|
}
|
|
@ -11,7 +11,7 @@ import LoadingPage from "../LoadingPage/LoadingPage";
|
||||||
import { AuthenticationLevel } from "../../services/State";
|
import { AuthenticationLevel } from "../../services/State";
|
||||||
import { useNotifications } from "../../hooks/NotificationsContext";
|
import { useNotifications } from "../../hooks/NotificationsContext";
|
||||||
import { useRedirectionURL } from "../../hooks/RedirectionURL";
|
import { useRedirectionURL } from "../../hooks/RedirectionURL";
|
||||||
import { useUserPreferences } from "../../hooks/UserPreferences";
|
import { useUserPreferences as userUserInfo } from "../../hooks/UserInfo";
|
||||||
import { SecondFactorMethod } from "../../models/Methods";
|
import { SecondFactorMethod } from "../../models/Methods";
|
||||||
import { useAutheliaConfiguration } from "../../hooks/Configuration";
|
import { useAutheliaConfiguration } from "../../hooks/Configuration";
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ export default function () {
|
||||||
const [firstFactorDisabled, setFirstFactorDisabled] = useState(true);
|
const [firstFactorDisabled, setFirstFactorDisabled] = useState(true);
|
||||||
|
|
||||||
const [state, fetchState, , fetchStateError] = useAutheliaState();
|
const [state, fetchState, , fetchStateError] = useAutheliaState();
|
||||||
const [preferences, fetchPreferences, , fetchPreferencesError] = useUserPreferences();
|
const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo();
|
||||||
const [configuration, fetchConfiguration, , fetchConfigurationError] = useAutheliaConfiguration();
|
const [configuration, fetchConfiguration, , fetchConfigurationError] = useAutheliaConfiguration();
|
||||||
|
|
||||||
const redirect = useCallback((url: string) => history.push(url), [history]);
|
const redirect = useCallback((url: string) => history.push(url), [history]);
|
||||||
|
@ -34,10 +34,10 @@ export default function () {
|
||||||
// Fetch preferences and configuration when user is authenticated.
|
// Fetch preferences and configuration when user is authenticated.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state && state.authentication_level >= AuthenticationLevel.OneFactor) {
|
if (state && state.authentication_level >= AuthenticationLevel.OneFactor) {
|
||||||
fetchPreferences();
|
fetchUserInfo();
|
||||||
fetchConfiguration();
|
fetchConfiguration();
|
||||||
}
|
}
|
||||||
}, [state, fetchPreferences, fetchConfiguration]);
|
}, [state, fetchUserInfo, fetchConfiguration]);
|
||||||
|
|
||||||
// Enable first factor when user is unauthenticated.
|
// Enable first factor when user is unauthenticated.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -62,10 +62,10 @@ export default function () {
|
||||||
|
|
||||||
// Display an error when preferences fetching fails
|
// Display an error when preferences fetching fails
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fetchPreferencesError) {
|
if (fetchUserInfoError) {
|
||||||
createErrorNotification("There was an issue retrieving user preferences");
|
createErrorNotification("There was an issue retrieving user preferences");
|
||||||
}
|
}
|
||||||
}, [fetchPreferencesError, createErrorNotification]);
|
}, [fetchUserInfoError, createErrorNotification]);
|
||||||
|
|
||||||
// Redirect to the correct stage if not enough authenticated
|
// Redirect to the correct stage if not enough authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -77,18 +77,17 @@ export default function () {
|
||||||
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
|
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
|
||||||
setFirstFactorDisabled(false);
|
setFirstFactorDisabled(false);
|
||||||
redirect(`${FirstFactorRoute}${redirectionSuffix}`);
|
redirect(`${FirstFactorRoute}${redirectionSuffix}`);
|
||||||
} else if (state.authentication_level >= AuthenticationLevel.OneFactor && preferences) {
|
} else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo) {
|
||||||
console.log("redirect");
|
if (userInfo.method === SecondFactorMethod.U2F) {
|
||||||
if (preferences.method === SecondFactorMethod.U2F) {
|
|
||||||
redirect(`${SecondFactorU2FRoute}${redirectionSuffix}`);
|
redirect(`${SecondFactorU2FRoute}${redirectionSuffix}`);
|
||||||
} else if (preferences.method === SecondFactorMethod.MobilePush) {
|
} else if (userInfo.method === SecondFactorMethod.MobilePush) {
|
||||||
redirect(`${SecondFactorPushRoute}${redirectionSuffix}`);
|
redirect(`${SecondFactorPushRoute}${redirectionSuffix}`);
|
||||||
} else {
|
} else {
|
||||||
redirect(`${SecondFactorTOTPRoute}${redirectionSuffix}`);
|
redirect(`${SecondFactorTOTPRoute}${redirectionSuffix}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [state, redirectionURL, redirect, preferences, setFirstFactorDisabled]);
|
}, [state, redirectionURL, redirect, userInfo, setFirstFactorDisabled]);
|
||||||
|
|
||||||
const handleFirstFactorSuccess = async (redirectionURL: string | undefined) => {
|
const handleFirstFactorSuccess = async (redirectionURL: string | undefined) => {
|
||||||
if (redirectionURL) {
|
if (redirectionURL) {
|
||||||
|
@ -125,12 +124,12 @@ export default function () {
|
||||||
</ComponentOrLoading>
|
</ComponentOrLoading>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={SecondFactorRoute}>
|
<Route path={SecondFactorRoute}>
|
||||||
{state && preferences && configuration ? <SecondFactorForm
|
{state && userInfo && configuration ? <SecondFactorForm
|
||||||
username={state.username}
|
username={state.username}
|
||||||
authenticationLevel={state.authentication_level}
|
authenticationLevel={state.authentication_level}
|
||||||
userPreferences={preferences}
|
userInfo={userInfo}
|
||||||
configuration={configuration}
|
configuration={configuration}
|
||||||
onMethodChanged={() => fetchPreferences()}
|
onMethodChanged={() => fetchUserInfo()}
|
||||||
onAuthenticationSuccess={handleSecondFactorSuccess} /> : null}
|
onAuthenticationSuccess={handleSecondFactorSuccess} /> : null}
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
|
|
|
@ -12,9 +12,7 @@ interface IconWithContextProps {
|
||||||
export default function (props: IconWithContextProps) {
|
export default function (props: IconWithContextProps) {
|
||||||
const iconSize = 64;
|
const iconSize = 64;
|
||||||
const style = makeStyles(theme => ({
|
const style = makeStyles(theme => ({
|
||||||
root: {
|
root: {},
|
||||||
height: iconSize + theme.spacing(6),
|
|
||||||
},
|
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
@ -26,7 +24,6 @@ export default function (props: IconWithContextProps) {
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
display: "block",
|
display: "block",
|
||||||
height: theme.spacing(6),
|
|
||||||
}
|
}
|
||||||
}))();
|
}))();
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,50 @@
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode, Fragment } from "react";
|
||||||
import { makeStyles, Typography, Link } from "@material-ui/core";
|
import { makeStyles, Typography, Link, useTheme } from "@material-ui/core";
|
||||||
|
import SuccessIcon from "../../../components/SuccessIcon";
|
||||||
|
import InformationIcon from "../../../components/InformationIcon";
|
||||||
|
|
||||||
interface MethodContainerProps {
|
export enum State {
|
||||||
|
ALREADY_AUTHENTICATED = 1,
|
||||||
|
NOT_REGISTERED = 2,
|
||||||
|
METHOD = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
explanation: string;
|
explanation: string;
|
||||||
|
state: State;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|
||||||
onRegisterClick?: () => void;
|
onRegisterClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (props: MethodContainerProps) {
|
export default function (props: Props) {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
|
|
||||||
|
let container: ReactNode;
|
||||||
|
switch (props.state) {
|
||||||
|
case State.ALREADY_AUTHENTICATED:
|
||||||
|
container = <AlreadyAuthenticatedContainer />
|
||||||
|
break;
|
||||||
|
case State.NOT_REGISTERED:
|
||||||
|
container = <NotRegisteredContainer />
|
||||||
|
break;
|
||||||
|
case State.METHOD:
|
||||||
|
container = <MethodContainer explanation={props.explanation}>
|
||||||
|
{props.children}
|
||||||
|
</MethodContainer>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={props.id}>
|
<div id={props.id}>
|
||||||
<Typography variant="h6">{props.title}</Typography>
|
<Typography variant="h6">{props.title}</Typography>
|
||||||
<div className={style.icon}>{props.children}</div>
|
<div className={style.container} id="2fa-container">
|
||||||
<Typography>{props.explanation}</Typography>
|
<div className={style.containerFlex}>
|
||||||
|
{container}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{props.onRegisterClick
|
{props.onRegisterClick
|
||||||
? <Link component="button"
|
? <Link component="button"
|
||||||
id="register-link"
|
id="register-link"
|
||||||
|
@ -29,8 +57,51 @@ export default function (props: MethodContainerProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
icon: {
|
container: {
|
||||||
paddingTop: theme.spacing(2),
|
height: "200px",
|
||||||
paddingBottom: theme.spacing(2),
|
|
||||||
},
|
},
|
||||||
}));
|
containerFlex: {
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
alignContent: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
function AlreadyAuthenticatedContainer() {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div style={{ marginBottom: theme.spacing(2), flex: "0 0 100%" }}><SuccessIcon /></div>
|
||||||
|
<Typography style={{ color: "green" }}>Authenticated!</Typography>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotRegisteredContainer() {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div style={{ marginBottom: theme.spacing(2), flex: "0 0 100%" }}><InformationIcon /></div>
|
||||||
|
<Typography style={{ color: "#5858ff" }}>Register your first device by clicking on the link below</Typography>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MethodContainerProps {
|
||||||
|
explanation: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MethodContainer(props: MethodContainerProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div style={{ marginBottom: theme.spacing(2) }}>{props.children}</div>
|
||||||
|
<Typography>{props.explanation}</Typography>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import MethodContainer from "./MethodContainer";
|
import MethodContainer, { State as MethodContainerState } from "./MethodContainer";
|
||||||
import OTPDial from "./OTPDial";
|
import OTPDial from "./OTPDial";
|
||||||
import { completeTOTPSignIn } from "../../../services/OneTimePassword";
|
import { completeTOTPSignIn } from "../../../services/OneTimePassword";
|
||||||
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
||||||
|
@ -15,6 +15,7 @@ export enum State {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
authenticationLevel: AuthenticationLevel;
|
authenticationLevel: AuthenticationLevel;
|
||||||
|
registered: boolean;
|
||||||
|
|
||||||
onRegisterClick: () => void;
|
onRegisterClick: () => void;
|
||||||
onSignInError: (err: Error) => void;
|
onSignInError: (err: Error) => void;
|
||||||
|
@ -65,11 +66,19 @@ export default function (props: Props) {
|
||||||
|
|
||||||
useEffect(() => { signInFunc() }, [signInFunc]);
|
useEffect(() => { signInFunc() }, [signInFunc]);
|
||||||
|
|
||||||
|
let methodState = MethodContainerState.METHOD;
|
||||||
|
if (props.authenticationLevel === AuthenticationLevel.TwoFactor) {
|
||||||
|
methodState = MethodContainerState.ALREADY_AUTHENTICATED;
|
||||||
|
} else if (!props.registered) {
|
||||||
|
methodState = MethodContainerState.NOT_REGISTERED;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MethodContainer
|
<MethodContainer
|
||||||
id={props.id}
|
id={props.id}
|
||||||
title="One-Time Password"
|
title="One-Time Password"
|
||||||
explanation="Enter one-time password"
|
explanation="Enter one-time password"
|
||||||
|
state={methodState}
|
||||||
onRegisterClick={props.onRegisterClick}>
|
onRegisterClick={props.onRegisterClick}>
|
||||||
<OTPDial
|
<OTPDial
|
||||||
passcode={passcode}
|
passcode={passcode}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect, useCallback, useState, ReactNode } from "react";
|
import React, { useEffect, useCallback, useState, ReactNode } from "react";
|
||||||
import MethodContainer from "./MethodContainer";
|
import MethodContainer, { State as MethodContainerState } from "./MethodContainer";
|
||||||
import PushNotificationIcon from "../../../components/PushNotificationIcon";
|
import PushNotificationIcon from "../../../components/PushNotificationIcon";
|
||||||
import { completePushNotificationSignIn } from "../../../services/PushNotification";
|
import { completePushNotificationSignIn } from "../../../services/PushNotification";
|
||||||
import { Button, makeStyles } from "@material-ui/core";
|
import { Button, makeStyles } from "@material-ui/core";
|
||||||
|
@ -82,11 +82,17 @@ export default function (props: Props) {
|
||||||
icon = <FailureIcon />;
|
icon = <FailureIcon />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let methodState = MethodContainerState.METHOD;
|
||||||
|
if (props.authenticationLevel === AuthenticationLevel.TwoFactor) {
|
||||||
|
methodState = MethodContainerState.ALREADY_AUTHENTICATED;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MethodContainer
|
<MethodContainer
|
||||||
id={props.id}
|
id={props.id}
|
||||||
title="Push Notification"
|
title="Push Notification"
|
||||||
explanation="A notification has been sent to your smartphone">
|
explanation="A notification has been sent to your smartphone"
|
||||||
|
state={methodState}>
|
||||||
<div className={style.icon}>
|
<div className={style.icon}>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
SecondFactorPushRoute, SecondFactorU2FRoute, SecondFactorRoute
|
SecondFactorPushRoute, SecondFactorU2FRoute, SecondFactorRoute
|
||||||
} from "../../../Routes";
|
} from "../../../Routes";
|
||||||
import { setPrefered2FAMethod } from "../../../services/UserPreferences";
|
import { setPrefered2FAMethod } from "../../../services/UserPreferences";
|
||||||
import { UserPreferences } from "../../../models/UserPreferences";
|
import { UserInfo } from "../../../models/UserInfo";
|
||||||
import { Configuration } from "../../../models/Configuration";
|
import { Configuration } from "../../../models/Configuration";
|
||||||
import u2fApi from "u2f-api";
|
import u2fApi from "u2f-api";
|
||||||
import { AuthenticationLevel } from "../../../services/State";
|
import { AuthenticationLevel } from "../../../services/State";
|
||||||
|
@ -28,7 +28,7 @@ export interface Props {
|
||||||
username: string;
|
username: string;
|
||||||
authenticationLevel: AuthenticationLevel;
|
authenticationLevel: AuthenticationLevel;
|
||||||
|
|
||||||
userPreferences: UserPreferences;
|
userInfo: UserInfo;
|
||||||
configuration: Configuration;
|
configuration: Configuration;
|
||||||
|
|
||||||
onMethodChanged: (method: SecondFactorMethod) => void;
|
onMethodChanged: (method: SecondFactorMethod) => void;
|
||||||
|
@ -109,6 +109,8 @@ export default function (props: Props) {
|
||||||
<OneTimePasswordMethod
|
<OneTimePasswordMethod
|
||||||
id="one-time-password-method"
|
id="one-time-password-method"
|
||||||
authenticationLevel={props.authenticationLevel}
|
authenticationLevel={props.authenticationLevel}
|
||||||
|
// Whether the user has a TOTP secret registered already
|
||||||
|
registered={props.userInfo.has_totp}
|
||||||
onRegisterClick={initiateRegistration(initiateTOTPRegistrationProcess)}
|
onRegisterClick={initiateRegistration(initiateTOTPRegistrationProcess)}
|
||||||
onSignInError={err => createErrorNotification(err.message)}
|
onSignInError={err => createErrorNotification(err.message)}
|
||||||
onSignInSuccess={props.onAuthenticationSuccess} />
|
onSignInSuccess={props.onAuthenticationSuccess} />
|
||||||
|
@ -117,6 +119,8 @@ export default function (props: Props) {
|
||||||
<SecurityKeyMethod
|
<SecurityKeyMethod
|
||||||
id="security-key-method"
|
id="security-key-method"
|
||||||
authenticationLevel={props.authenticationLevel}
|
authenticationLevel={props.authenticationLevel}
|
||||||
|
// Whether the user has a U2F device registered already
|
||||||
|
registered={props.userInfo.has_u2f}
|
||||||
onRegisterClick={initiateRegistration(initiateU2FRegistrationProcess)}
|
onRegisterClick={initiateRegistration(initiateU2FRegistrationProcess)}
|
||||||
onSignInError={err => createErrorNotification(err.message)}
|
onSignInError={err => createErrorNotification(err.message)}
|
||||||
onSignInSuccess={props.onAuthenticationSuccess} />
|
onSignInSuccess={props.onAuthenticationSuccess} />
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useCallback, useEffect, useState, Fragment } from "react";
|
import React, { useCallback, useEffect, useState, Fragment } from "react";
|
||||||
import MethodContainer from "./MethodContainer";
|
import MethodContainer, { State as MethodContainerState } from "./MethodContainer";
|
||||||
import { makeStyles, Button, useTheme } from "@material-ui/core";
|
import { makeStyles, Button, useTheme } from "@material-ui/core";
|
||||||
import { initiateU2FSignin, completeU2FSignin } from "../../../services/SecurityKey";
|
import { initiateU2FSignin, completeU2FSignin } from "../../../services/SecurityKey";
|
||||||
import u2fApi from "u2f-api";
|
import u2fApi from "u2f-api";
|
||||||
|
@ -8,7 +8,6 @@ import { useIsMountedRef } from "../../../hooks/Mounted";
|
||||||
import { useTimer } from "../../../hooks/Timer";
|
import { useTimer } from "../../../hooks/Timer";
|
||||||
import LinearProgressBar from "../../../components/LinearProgressBar";
|
import LinearProgressBar from "../../../components/LinearProgressBar";
|
||||||
import FingerTouchIcon from "../../../components/FingerTouchIcon";
|
import FingerTouchIcon from "../../../components/FingerTouchIcon";
|
||||||
import SuccessIcon from "../../../components/SuccessIcon";
|
|
||||||
import FailureIcon from "../../../components/FailureIcon";
|
import FailureIcon from "../../../components/FailureIcon";
|
||||||
import IconWithContext from "./IconWithContext";
|
import IconWithContext from "./IconWithContext";
|
||||||
import { CSSProperties } from "@material-ui/styles";
|
import { CSSProperties } from "@material-ui/styles";
|
||||||
|
@ -17,13 +16,13 @@ import { AuthenticationLevel } from "../../../services/State";
|
||||||
export enum State {
|
export enum State {
|
||||||
WaitTouch = 1,
|
WaitTouch = 1,
|
||||||
SigninInProgress = 2,
|
SigninInProgress = 2,
|
||||||
Success = 3,
|
Failure = 3,
|
||||||
Failure = 4,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
authenticationLevel: AuthenticationLevel;
|
authenticationLevel: AuthenticationLevel;
|
||||||
|
registered: boolean;
|
||||||
|
|
||||||
onRegisterClick: () => void;
|
onRegisterClick: () => void;
|
||||||
onSignInError: (err: Error) => void;
|
onSignInError: (err: Error) => void;
|
||||||
|
@ -44,7 +43,7 @@ export default function (props: Props) {
|
||||||
|
|
||||||
const doInitiateSignIn = useCallback(async () => {
|
const doInitiateSignIn = useCallback(async () => {
|
||||||
// If user is already authenticated, we don't initiate sign in process.
|
// If user is already authenticated, we don't initiate sign in process.
|
||||||
if (props.authenticationLevel >= AuthenticationLevel.TwoFactor) {
|
if (!props.registered || props.authenticationLevel >= AuthenticationLevel.TwoFactor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,8 +68,7 @@ export default function (props: Props) {
|
||||||
|
|
||||||
setState(State.SigninInProgress);
|
setState(State.SigninInProgress);
|
||||||
const res = await completeU2FSignin(signResponse, redirectionURL);
|
const res = await completeU2FSignin(signResponse, redirectionURL);
|
||||||
setState(State.Success);
|
onSignInSuccessCallback(res ? res.redirect : undefined);
|
||||||
setTimeout(() => { onSignInSuccessCallback(res ? res.redirect : undefined) }, 1500);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If the request was initiated and the user changed 2FA method in the meantime,
|
// 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.
|
// the process is interrupted to avoid updating state of unmounted component.
|
||||||
|
@ -79,22 +77,23 @@ export default function (props: Props) {
|
||||||
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
|
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
|
||||||
setState(State.Failure);
|
setState(State.Failure);
|
||||||
}
|
}
|
||||||
}, [onSignInSuccessCallback, onSignInErrorCallback, redirectionURL, mounted, triggerTimer, props.authenticationLevel]);
|
}, [onSignInSuccessCallback, onSignInErrorCallback, redirectionURL, mounted, triggerTimer, props.authenticationLevel, props.registered]);
|
||||||
|
|
||||||
// Set successful state if user is already authenticated.
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.authenticationLevel >= AuthenticationLevel.TwoFactor) {
|
|
||||||
setState(State.Success);
|
|
||||||
}
|
|
||||||
}, [props.authenticationLevel, setState]);
|
|
||||||
|
|
||||||
useEffect(() => { doInitiateSignIn() }, [doInitiateSignIn]);
|
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 (
|
return (
|
||||||
<MethodContainer
|
<MethodContainer
|
||||||
id={props.id}
|
id={props.id}
|
||||||
title="Security Key"
|
title="Security Key"
|
||||||
explanation="Touch the token of your security key"
|
explanation="Touch the token of your security key"
|
||||||
|
state={methodState}
|
||||||
onRegisterClick={props.onRegisterClick}>
|
onRegisterClick={props.onRegisterClick}>
|
||||||
<div className={style.icon}>
|
<div className={style.icon}>
|
||||||
<Icon state={state} timer={timerPercent} onRetryClick={doInitiateSignIn} />
|
<Icon state={state} timer={timerPercent} onRetryClick={doInitiateSignIn} />
|
||||||
|
@ -134,15 +133,9 @@ function Icon(props: IconProps) {
|
||||||
context={<Button color="secondary" onClick={props.onRetryClick}>Retry</Button>}
|
context={<Button color="secondary" onClick={props.onRetryClick}>Retry</Button>}
|
||||||
className={state === State.Failure ? undefined : "hidden"} />
|
className={state === State.Failure ? undefined : "hidden"} />
|
||||||
|
|
||||||
const success = <IconWithContext
|
|
||||||
icon={<SuccessIcon />}
|
|
||||||
context={<div style={{ color: "green", padding: theme.spacing() }}>Success!</div>}
|
|
||||||
className={state === State.Success || state === State.SigninInProgress ? undefined : "hidden"} />
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{touch}
|
{touch}
|
||||||
{success}
|
|
||||||
{failure}
|
{failure}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user