Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TwoFa revamp #226

Merged
merged 1 commit into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 16 additions & 22 deletions backend/app/rest/auth_routes.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import os
import pyotp
from ..utilities.exceptions.firebase_exceptions import (
InvalidPasswordException,
TooManyLoginAttemptsException,
)
from ..utilities.exceptions.auth_exceptions import EmailAlreadyInUseException

from flask import Blueprint, current_app, jsonify, request
from twilio.rest import Client

from ..middlewares.auth import (
require_authorization_by_user_id,
Expand Down Expand Up @@ -44,7 +44,7 @@

blueprint = Blueprint("auth", __name__, url_prefix="/auth")

client = Client(os.getenv("TWILIO_ACCOUNT_SID"), os.getenv("TWILIO_AUTH_TOKEN"))
totp = pyotp.TOTP(os.getenv("TWO_FA_SECRET"))


@blueprint.route("/login", methods=["POST"], strict_slashes=False)
Expand All @@ -63,7 +63,7 @@ def login():
)
response = {"requires_two_fa": False, "auth_user": None}

if os.getenv("TWILIO_ENABLED") == "True" and auth_dto.role == "Relief Staff":
if os.getenv("TWO_FA_ENABLED") == "True" and auth_dto.role == "Relief Staff":
response["requires_two_fa"] = True
return jsonify(response), 200

Expand Down Expand Up @@ -100,27 +100,15 @@ def two_fa():
returns access token in response body and sets refreshToken as an httpOnly cookie only
"""

passcode = request.args.get("passcode")

if not passcode:
return (
jsonify({"error": "Must supply passcode as a query parameter.t"}),
400,
)
passcode = request.args.get("passcode") if request.args.get("passcode") else ""

try:
challenge = (
client.verify.v2.services(os.getenv("TWILIO_SERVICE_SID"))
.entities(os.getenv("TWILIO_ENTITY_ID"))
.challenges.create(
auth_payload=passcode, factor_sid=os.getenv("TWILIO_FACTOR_SID")
)
)
verified = totp.verify(passcode)

if challenge.status != "approved":
if not verified:
return (
jsonify({"error": "Invalid passcode."}),
400,
jsonify({"error": "Invalid passcode. Please try again."}),
401,
)

auth_dto = None
Expand All @@ -131,7 +119,13 @@ def two_fa():
request.json["email"], request.json["password"]
)

auth_service.send_email_verification_link(request.json["email"])
is_authorized_by_token = auth_service.is_authorized_by_token(
auth_dto.access_token
)

if not is_authorized_by_token:
auth_service.send_email_verification_link(request.json["email"])

sign_in_logs_service.create_sign_in_log(auth_dto.id)

response = jsonify(
Expand All @@ -142,7 +136,7 @@ def two_fa():
"last_name": auth_dto.last_name,
"email": auth_dto.email,
"role": auth_dto.role,
"verified": auth_service.is_authorized_by_token(auth_dto.access_token),
"verified": is_authorized_by_token,
}
)
response.set_cookie(
Expand Down
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ pytz
alembic
pytest
black
twilio
pyotp
urllib3==1.26.15
17 changes: 13 additions & 4 deletions frontend/src/APIClients/AuthAPIClient.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {
FetchResult,

Check warning on line 2 in frontend/src/APIClients/AuthAPIClient.ts

View workflow job for this annotation

GitHub Actions / run-lint

'FetchResult' is defined but never used
MutationFunctionOptions,

Check warning on line 3 in frontend/src/APIClients/AuthAPIClient.ts

View workflow job for this annotation

GitHub Actions / run-lint

'MutationFunctionOptions' is defined but never used
OperationVariables,

Check warning on line 4 in frontend/src/APIClients/AuthAPIClient.ts

View workflow job for this annotation

GitHub Actions / run-lint

'OperationVariables' is defined but never used
} from "@apollo/client";
import { AxiosError } from "axios";
import { getAuthErrMessage } from "../helper/error";
import AUTHENTICATED_USER_KEY from "../constants/AuthConstants";
import { AuthenticatedUser, AuthTokenResponse } from "../types/AuthTypes";
import { AuthErrorResponse } from "../types/ErrorTypes";
import { AuthErrorResponse, ErrorResponse } from "../types/ErrorTypes";
import baseAPIClient from "./BaseAPIClient";
import {
getLocalStorageObjProperty,
Expand All @@ -26,7 +26,7 @@
);
return data;
} catch (error) {
const axiosErr = (error as any) as AxiosError;

Check warning on line 29 in frontend/src/APIClients/AuthAPIClient.ts

View workflow job for this annotation

GitHub Actions / run-lint

Unexpected any. Specify a different type
if (axiosErr.response && axiosErr.response.status === 401) {
return {
errCode: axiosErr.response.status,
Expand All @@ -44,16 +44,25 @@
passcode: string,
email: string,
password: string,
): Promise<AuthenticatedUser | null> => {
): Promise<AuthenticatedUser | ErrorResponse> => {
try {
const { data } = await baseAPIClient.post(
const { data } = await baseAPIClient.post<AuthenticatedUser>(
`/auth/twoFa?passcode=${passcode}`,
{ email, password },
{ withCredentials: true },
);
return data;
} catch (error) {
return null;
const axiosErr = (error as any) as AxiosError;

Check warning on line 56 in frontend/src/APIClients/AuthAPIClient.ts

View workflow job for this annotation

GitHub Actions / run-lint

Unexpected any. Specify a different type
if (axiosErr.response && axiosErr.response.status === 401) {
return {
errMessage:
axiosErr.response.data.error ?? "Invalid passcode. Please try again.",
};
}
return {
errMessage: "Unable to authenticate. Please try again.",
};
}
};

Expand Down Expand Up @@ -105,7 +114,7 @@
);
return data;
} catch (error) {
const axiosErr = (error as any) as AxiosError;

Check warning on line 117 in frontend/src/APIClients/AuthAPIClient.ts

View workflow job for this annotation

GitHub Actions / run-lint

Unexpected any. Specify a different type
if (axiosErr.response && axiosErr.response.status === 409) {
return {
errCode: axiosErr.response.status,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,66 @@
import React, { useContext, useState, useRef } from "react";
import { Redirect } from "react-router-dom";
import { Box, Button, Flex, Input, Text, VStack } from "@chakra-ui/react";
import {
Box,
Button,
Center,
Flex,
Input,
Spinner,
Text,
VStack,
} from "@chakra-ui/react";
import authAPIClient from "../../APIClients/AuthAPIClient";
import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants";
import { HOME_PAGE } from "../../constants/Routes";
import AuthContext from "../../contexts/AuthContext";
import { AuthenticatedUser } from "../../types/AuthTypes";
import CreateToast from "../common/Toasts";
import { isErrorResponse } from "../../helper/error";

type AuthyProps = {
type TwoFaProps = {
email: string;
password: string;
token: string;
toggle: boolean;
};

const Authy = ({
const TwoFa = ({
email,
password,
token,
toggle,
}: AuthyProps): React.ReactElement => {
}: TwoFaProps): React.ReactElement => {
const newToast = CreateToast();
const { authenticatedUser, setAuthenticatedUser } = useContext(AuthContext);
const [error, setError] = useState("");
const [authCode, setAuthCode] = useState("");

const [isLoading, setIsLoading] = useState(false);

const inputRef = useRef<HTMLInputElement>(null);

const onAuthySubmit = async () => {
let authUser: AuthenticatedUser | null;
const twoFaSubmit = async () => {
// Uncomment this if Google/Outlook sign in is ever needed
// authUser = await authAPIClient.twoFaWithGoogle(authCode, token);

if (token) {
authUser = await authAPIClient.twoFaWithGoogle(authCode, token);
} else {
authUser = await authAPIClient.twoFa(authCode, email, password);
if (authCode.length < 6) {
newToast(
"Authentication Failed",
"Please enter a 6 digit authentication code.",
"error",
);
return;
}

if (authUser) {
setIsLoading(true);
const authUser = await authAPIClient.twoFa(authCode, email, password);

if (isErrorResponse(authUser)) {
setIsLoading(false);
newToast("Authentication Failed", authUser.errMessage, "error");
} else {
localStorage.setItem(AUTHENTICATED_USER_KEY, JSON.stringify(authUser));
setAuthenticatedUser(authUser);
} else {
setError("Error: Invalid token");
}
};

Expand Down Expand Up @@ -76,8 +97,8 @@ const Authy = ({
<VStack width="75%" align="flex-start" gap="3vh">
<Text variant="login">One last step!</Text>
<Text variant="loginSecondary">
In order to protect your account, please enter the authorization
code from the Twilio Authy application.
In order to protect your account, please enter the 6 digit
authentication code from the Authenticator extension.
</Text>
<Flex direction="row" width="100%" justifyContent="space-between">
{boxIndexes.map((boxIndex) => {
Expand All @@ -98,17 +119,30 @@ const Authy = ({
);
})}
</Flex>
<Button
variant="login"
disabled={authCode.length < 6}
_hover={{
background: "teal.500",
transition:
"transition: background-color 0.5s ease !important",
}}
>
Authenticate
</Button>
{isLoading ? (
<Flex width="100%">
<Spinner
thickness="4px"
speed="0.65s"
emptyColor="gray.200"
size="lg"
margin="0 auto"
textAlign="center"
/>{" "}
</Flex>
) : (
<Button
variant="login"
_hover={{
background: "teal.500",
transition:
"transition: background-color 0.5s ease !important",
}}
onClick={twoFaSubmit}
>
Authenticate
</Button>
)}
<Input
ref={inputRef}
autoFocus
Expand All @@ -126,4 +160,4 @@ const Authy = ({
return <></>;
};

export default Authy;
export default TwoFa;
4 changes: 2 additions & 2 deletions frontend/src/components/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import Login from "../forms/Login";
import Authy from "../auth/Authy";
import TwoFa from "../auth/TwoFa";

const LoginPage = (): React.ReactElement => {
const [email, setEmail] = useState("");
Expand All @@ -19,7 +19,7 @@ const LoginPage = (): React.ReactElement => {
toggle={toggle}
setToggle={setToggle}
/>
<Authy email={email} password={password} token={token} toggle={!toggle} />
<TwoFa email={email} password={password} token={token} toggle={!toggle} />
</>
);
};
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/pages/SignupPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import Signup from "../forms/Signup";
import Authy from "../auth/Authy";
import TwoFa from "../auth/TwoFa";

const SignupPage = (): React.ReactElement => {
const [toggle, setToggle] = useState(true);
Expand All @@ -23,7 +23,7 @@ const SignupPage = (): React.ReactElement => {
toggle={toggle}
setToggle={setToggle}
/>
<Authy email={email} password={password} token="" toggle={!toggle} />
<TwoFa email={email} password={password} token="" toggle={!toggle} />
</>
);
};
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/helper/error.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { AxiosError } from "axios";
import { AuthTokenResponse, AuthFlow } from "../types/AuthTypes";
import {
AuthTokenResponse,
AuthFlow,
AuthenticatedUser,
} from "../types/AuthTypes";
import { AuthErrorResponse, ErrorResponse } from "../types/ErrorTypes";

export const getAuthErrMessage = (
Expand All @@ -21,7 +25,7 @@ export const isAuthErrorResponse = (
};

export const isErrorResponse = (
res: boolean | string | ErrorResponse,
res: boolean | string | AuthenticatedUser | ErrorResponse,
): res is ErrorResponse => {
return (
typeof res !== "boolean" && typeof res !== "string" && "errMessage" in res
Expand Down
Loading