Skip to content

Commit

Permalink
replace authy with authenticator chrome extension (#226)
Browse files Browse the repository at this point in the history
  • Loading branch information
Connor Bechthold authored Jan 25, 2024
1 parent 5b73871 commit 26943e2
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 61 deletions.
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
Expand Up @@ -7,7 +7,7 @@ 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 Down Expand Up @@ -44,16 +44,25 @@ const twoFa = async (
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
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

0 comments on commit 26943e2

Please sign in to comment.