feat: TOTP (two-factor) Authentication (#55)
* Working on some initial prototype stuff for TOTP * Fixed a bug that prevented the change password menu from working * Enable/disable totp working * Added the new login procedure including TOTP! :) * misc: Changed bad description for the TOTP_SECRET env var * I forgot to include the migration for the new prisma stuff * fix: refresh user context instead refreshing the page * refactor: simplify totp error handling * Removed U2F tab + format schema * fix: tokens not saved in cookies * refactor: deleted commented out code * refactor: move password text to input description * refactor: remove tabler icon package Co-authored-by: Elias Schneider <login@eliasschneider.com> Co-authored-by: Elias Schneider <58886915+stonith404@users.noreply.github.com>
This commit is contained in:
@@ -9,7 +9,11 @@ import {
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
import { setCookie } from "cookies-next";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { TbInfoCircle } from "react-icons/tb";
|
||||
import * as yup from "yup";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import authService from "../../services/auth.service";
|
||||
@@ -17,16 +21,24 @@ import toast from "../../utils/toast.util";
|
||||
|
||||
const SignInForm = () => {
|
||||
const config = useConfig();
|
||||
const [showTotp, setShowTotp] = React.useState(false);
|
||||
const [loginToken, setLoginToken] = React.useState("");
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
emailOrUsername: yup.string().required(),
|
||||
password: yup.string().min(8).required(),
|
||||
totp: yup.string().when("totpRequired", {
|
||||
is: true,
|
||||
then: yup.string().min(6).max(6).required(),
|
||||
otherwise: yup.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
emailOrUsername: "",
|
||||
password: "",
|
||||
totp: "",
|
||||
},
|
||||
validate: yupResolver(validationSchema),
|
||||
});
|
||||
@@ -34,10 +46,47 @@ const SignInForm = () => {
|
||||
const signIn = (email: string, password: string) => {
|
||||
authService
|
||||
.signIn(email, password)
|
||||
.then(() => window.location.replace("/"))
|
||||
.then((response) => {
|
||||
if (response.data["loginToken"]) {
|
||||
// Prompt the user to enter their totp code
|
||||
setShowTotp(true);
|
||||
showNotification({
|
||||
icon: <TbInfoCircle />,
|
||||
color: "blue",
|
||||
radius: "md",
|
||||
title: "Two-factor authentication required",
|
||||
message: "Please enter your two-factor authentication code",
|
||||
});
|
||||
setLoginToken(response.data["loginToken"]);
|
||||
} else {
|
||||
setCookie("access_token", response.data.accessToken);
|
||||
setCookie("refresh_token", response.data.refreshToken);
|
||||
window.location.replace("/");
|
||||
}
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
};
|
||||
|
||||
const signInTotp = (email: string, password: string, totp: string) => {
|
||||
authService
|
||||
.signInTotp(email, password, totp, loginToken)
|
||||
.then((response) => {
|
||||
setCookie("access_token", response.data.accessToken);
|
||||
setCookie("refresh_token", response.data.refreshToken);
|
||||
window.location.replace("/");
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error?.response?.data?.message == "Login token expired") {
|
||||
toast.error("Login token expired");
|
||||
// Refresh the page to start over
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
toast.axiosError(error);
|
||||
form.setValues({ totp: "" });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size={420} my={40}>
|
||||
<Title
|
||||
@@ -59,9 +108,11 @@ const SignInForm = () => {
|
||||
)}
|
||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) =>
|
||||
signIn(values.emailOrUsername, values.password)
|
||||
)}
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
if (showTotp)
|
||||
signInTotp(values.emailOrUsername, values.password, values.totp);
|
||||
else signIn(values.emailOrUsername, values.password);
|
||||
})}
|
||||
>
|
||||
<TextInput
|
||||
label="Email or username"
|
||||
@@ -74,6 +125,15 @@ const SignInForm = () => {
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
{showTotp && (
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label="Code"
|
||||
placeholder="******"
|
||||
mt="md"
|
||||
{...form.getInputProps("totp")}
|
||||
/>
|
||||
)}
|
||||
<Button fullWidth mt="xl" type="submit">
|
||||
Sign in
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user