Skip to content

Commit 26943e2

Browse files
author
Connor Bechthold
authored
replace authy with authenticator chrome extension (#226)
1 parent 5b73871 commit 26943e2

File tree

7 files changed

+102
-61
lines changed

7 files changed

+102
-61
lines changed

backend/app/rest/auth_routes.py

+16-22
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import os
2+
import pyotp
23
from ..utilities.exceptions.firebase_exceptions import (
34
InvalidPasswordException,
45
TooManyLoginAttemptsException,
56
)
67
from ..utilities.exceptions.auth_exceptions import EmailAlreadyInUseException
78

89
from flask import Blueprint, current_app, jsonify, request
9-
from twilio.rest import Client
1010

1111
from ..middlewares.auth import (
1212
require_authorization_by_user_id,
@@ -44,7 +44,7 @@
4444

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

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

4949

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

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

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

103-
passcode = request.args.get("passcode")
104-
105-
if not passcode:
106-
return (
107-
jsonify({"error": "Must supply passcode as a query parameter.t"}),
108-
400,
109-
)
103+
passcode = request.args.get("passcode") if request.args.get("passcode") else ""
110104

111105
try:
112-
challenge = (
113-
client.verify.v2.services(os.getenv("TWILIO_SERVICE_SID"))
114-
.entities(os.getenv("TWILIO_ENTITY_ID"))
115-
.challenges.create(
116-
auth_payload=passcode, factor_sid=os.getenv("TWILIO_FACTOR_SID")
117-
)
118-
)
106+
verified = totp.verify(passcode)
119107

120-
if challenge.status != "approved":
108+
if not verified:
121109
return (
122-
jsonify({"error": "Invalid passcode."}),
123-
400,
110+
jsonify({"error": "Invalid passcode. Please try again."}),
111+
401,
124112
)
125113

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

134-
auth_service.send_email_verification_link(request.json["email"])
122+
is_authorized_by_token = auth_service.is_authorized_by_token(
123+
auth_dto.access_token
124+
)
125+
126+
if not is_authorized_by_token:
127+
auth_service.send_email_verification_link(request.json["email"])
128+
135129
sign_in_logs_service.create_sign_in_log(auth_dto.id)
136130

137131
response = jsonify(
@@ -142,7 +136,7 @@ def two_fa():
142136
"last_name": auth_dto.last_name,
143137
"email": auth_dto.email,
144138
"role": auth_dto.role,
145-
"verified": auth_service.is_authorized_by_token(auth_dto.access_token),
139+
"verified": is_authorized_by_token,
146140
}
147141
)
148142
response.set_cookie(

backend/requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ pytz
1212
alembic
1313
pytest
1414
black
15-
twilio
15+
pyotp
1616
urllib3==1.26.15

frontend/src/APIClients/AuthAPIClient.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { AxiosError } from "axios";
77
import { getAuthErrMessage } from "../helper/error";
88
import AUTHENTICATED_USER_KEY from "../constants/AuthConstants";
99
import { AuthenticatedUser, AuthTokenResponse } from "../types/AuthTypes";
10-
import { AuthErrorResponse } from "../types/ErrorTypes";
10+
import { AuthErrorResponse, ErrorResponse } from "../types/ErrorTypes";
1111
import baseAPIClient from "./BaseAPIClient";
1212
import {
1313
getLocalStorageObjProperty,
@@ -44,16 +44,25 @@ const twoFa = async (
4444
passcode: string,
4545
email: string,
4646
password: string,
47-
): Promise<AuthenticatedUser | null> => {
47+
): Promise<AuthenticatedUser | ErrorResponse> => {
4848
try {
49-
const { data } = await baseAPIClient.post(
49+
const { data } = await baseAPIClient.post<AuthenticatedUser>(
5050
`/auth/twoFa?passcode=${passcode}`,
5151
{ email, password },
5252
{ withCredentials: true },
5353
);
5454
return data;
5555
} catch (error) {
56-
return null;
56+
const axiosErr = (error as any) as AxiosError;
57+
if (axiosErr.response && axiosErr.response.status === 401) {
58+
return {
59+
errMessage:
60+
axiosErr.response.data.error ?? "Invalid passcode. Please try again.",
61+
};
62+
}
63+
return {
64+
errMessage: "Unable to authenticate. Please try again.",
65+
};
5766
}
5867
};
5968

frontend/src/components/auth/Authy.tsx frontend/src/components/auth/TwoFa.tsx

+62-28
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,66 @@
11
import React, { useContext, useState, useRef } from "react";
22
import { Redirect } from "react-router-dom";
3-
import { Box, Button, Flex, Input, Text, VStack } from "@chakra-ui/react";
3+
import {
4+
Box,
5+
Button,
6+
Center,
7+
Flex,
8+
Input,
9+
Spinner,
10+
Text,
11+
VStack,
12+
} from "@chakra-ui/react";
413
import authAPIClient from "../../APIClients/AuthAPIClient";
514
import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants";
615
import { HOME_PAGE } from "../../constants/Routes";
716
import AuthContext from "../../contexts/AuthContext";
817
import { AuthenticatedUser } from "../../types/AuthTypes";
18+
import CreateToast from "../common/Toasts";
19+
import { isErrorResponse } from "../../helper/error";
920

10-
type AuthyProps = {
21+
type TwoFaProps = {
1122
email: string;
1223
password: string;
1324
token: string;
1425
toggle: boolean;
1526
};
1627

17-
const Authy = ({
28+
const TwoFa = ({
1829
email,
1930
password,
2031
token,
2132
toggle,
22-
}: AuthyProps): React.ReactElement => {
33+
}: TwoFaProps): React.ReactElement => {
34+
const newToast = CreateToast();
2335
const { authenticatedUser, setAuthenticatedUser } = useContext(AuthContext);
24-
const [error, setError] = useState("");
2536
const [authCode, setAuthCode] = useState("");
2637

38+
const [isLoading, setIsLoading] = useState(false);
39+
2740
const inputRef = useRef<HTMLInputElement>(null);
2841

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

32-
if (token) {
33-
authUser = await authAPIClient.twoFaWithGoogle(authCode, token);
34-
} else {
35-
authUser = await authAPIClient.twoFa(authCode, email, password);
46+
if (authCode.length < 6) {
47+
newToast(
48+
"Authentication Failed",
49+
"Please enter a 6 digit authentication code.",
50+
"error",
51+
);
52+
return;
3653
}
3754

38-
if (authUser) {
55+
setIsLoading(true);
56+
const authUser = await authAPIClient.twoFa(authCode, email, password);
57+
58+
if (isErrorResponse(authUser)) {
59+
setIsLoading(false);
60+
newToast("Authentication Failed", authUser.errMessage, "error");
61+
} else {
3962
localStorage.setItem(AUTHENTICATED_USER_KEY, JSON.stringify(authUser));
4063
setAuthenticatedUser(authUser);
41-
} else {
42-
setError("Error: Invalid token");
4364
}
4465
};
4566

@@ -76,8 +97,8 @@ const Authy = ({
7697
<VStack width="75%" align="flex-start" gap="3vh">
7798
<Text variant="login">One last step!</Text>
7899
<Text variant="loginSecondary">
79-
In order to protect your account, please enter the authorization
80-
code from the Twilio Authy application.
100+
In order to protect your account, please enter the 6 digit
101+
authentication code from the Authenticator extension.
81102
</Text>
82103
<Flex direction="row" width="100%" justifyContent="space-between">
83104
{boxIndexes.map((boxIndex) => {
@@ -98,17 +119,30 @@ const Authy = ({
98119
);
99120
})}
100121
</Flex>
101-
<Button
102-
variant="login"
103-
disabled={authCode.length < 6}
104-
_hover={{
105-
background: "teal.500",
106-
transition:
107-
"transition: background-color 0.5s ease !important",
108-
}}
109-
>
110-
Authenticate
111-
</Button>
122+
{isLoading ? (
123+
<Flex width="100%">
124+
<Spinner
125+
thickness="4px"
126+
speed="0.65s"
127+
emptyColor="gray.200"
128+
size="lg"
129+
margin="0 auto"
130+
textAlign="center"
131+
/>{" "}
132+
</Flex>
133+
) : (
134+
<Button
135+
variant="login"
136+
_hover={{
137+
background: "teal.500",
138+
transition:
139+
"transition: background-color 0.5s ease !important",
140+
}}
141+
onClick={twoFaSubmit}
142+
>
143+
Authenticate
144+
</Button>
145+
)}
112146
<Input
113147
ref={inputRef}
114148
autoFocus
@@ -126,4 +160,4 @@ const Authy = ({
126160
return <></>;
127161
};
128162

129-
export default Authy;
163+
export default TwoFa;

frontend/src/components/pages/LoginPage.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState } from "react";
22
import Login from "../forms/Login";
3-
import Authy from "../auth/Authy";
3+
import TwoFa from "../auth/TwoFa";
44

55
const LoginPage = (): React.ReactElement => {
66
const [email, setEmail] = useState("");
@@ -19,7 +19,7 @@ const LoginPage = (): React.ReactElement => {
1919
toggle={toggle}
2020
setToggle={setToggle}
2121
/>
22-
<Authy email={email} password={password} token={token} toggle={!toggle} />
22+
<TwoFa email={email} password={password} token={token} toggle={!toggle} />
2323
</>
2424
);
2525
};

frontend/src/components/pages/SignupPage.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState } from "react";
22
import Signup from "../forms/Signup";
3-
import Authy from "../auth/Authy";
3+
import TwoFa from "../auth/TwoFa";
44

55
const SignupPage = (): React.ReactElement => {
66
const [toggle, setToggle] = useState(true);
@@ -23,7 +23,7 @@ const SignupPage = (): React.ReactElement => {
2323
toggle={toggle}
2424
setToggle={setToggle}
2525
/>
26-
<Authy email={email} password={password} token="" toggle={!toggle} />
26+
<TwoFa email={email} password={password} token="" toggle={!toggle} />
2727
</>
2828
);
2929
};

frontend/src/helper/error.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { AxiosError } from "axios";
2-
import { AuthTokenResponse, AuthFlow } from "../types/AuthTypes";
2+
import {
3+
AuthTokenResponse,
4+
AuthFlow,
5+
AuthenticatedUser,
6+
} from "../types/AuthTypes";
37
import { AuthErrorResponse, ErrorResponse } from "../types/ErrorTypes";
48

59
export const getAuthErrMessage = (
@@ -21,7 +25,7 @@ export const isAuthErrorResponse = (
2125
};
2226

2327
export const isErrorResponse = (
24-
res: boolean | string | ErrorResponse,
28+
res: boolean | string | AuthenticatedUser | ErrorResponse,
2529
): res is ErrorResponse => {
2630
return (
2731
typeof res !== "boolean" && typeof res !== "string" && "errMessage" in res

0 commit comments

Comments
 (0)