From 05a26d0b3fecffd3da18ee87df9cc53d43990a6b Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Sun, 28 Jul 2024 10:52:11 -0600 Subject: [PATCH] feat: Add VERIFY_TWO_FACTOR_AUTH_API_URL constant This commit adds the VERIFY_TWO_FACTOR_AUTH_API_URL constant to the ApiPaths module in order to provide a URL for verifying two-factor authentication. This constant is used in the Authentication module to make API requests for verifying the user's two-factor authentication. The constant is constructed using the IDENTITY_URL and a new route "/verify-two-factor-auth". This addition enables the implementation of the two-factor authentication verification feature. Files modified: - Accounts/src/Utils/ApiPaths.ts --- Accounts/src/Pages/Login.tsx | 235 +++++++++++++----- Accounts/src/Utils/ApiPaths.ts | 4 + App/FeatureSet/Identity/API/Authentication.ts | 107 ++++---- 3 files changed, 233 insertions(+), 113 deletions(-) diff --git a/Accounts/src/Pages/Login.tsx b/Accounts/src/Pages/Login.tsx index cf8add7f73..bd02c9e62d 100644 --- a/Accounts/src/Pages/Login.tsx +++ b/Accounts/src/Pages/Login.tsx @@ -1,7 +1,10 @@ -import { LOGIN_API_URL } from "../Utils/ApiPaths"; +import { + LOGIN_API_URL, + VERIFY_TWO_FACTOR_AUTH_API_URL, +} from "../Utils/ApiPaths"; import Route from "Common/Types/API/Route"; import URL from "Common/Types/API/URL"; -import { JSONObject } from "Common/Types/JSON"; +import { JSONArray, JSONObject } from "Common/Types/JSON"; import ModelForm, { FormType } from "CommonUI/src/Components/Forms/ModelForm"; import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType"; import Link from "CommonUI/src/Components/Link/Link"; @@ -9,11 +12,17 @@ import { DASHBOARD_URL } from "CommonUI/src/Config"; import OneUptimeLogo from "CommonUI/src/Images/logos/OneUptimeSVG/3-transparent.svg"; import UiAnalytics from "CommonUI/src/Utils/Analytics"; import LoginUtil from "CommonUI/src/Utils/Login"; +import UserTwoFactorAuth from "Model/Models/UserTwoFactorAuth"; import Navigation from "CommonUI/src/Utils/Navigation"; import UserUtil from "CommonUI/src/Utils/User"; import User from "Model/Models/User"; import React from "react"; import useAsyncEffect from "use-async-effect"; +import StaticModelList from "CommonUI/src/Components/ModelList/StaticModelList"; +import BasicForm from "CommonUI/src/Components/Forms/BasicForm"; +import API from "CommonUI/src/Utils/API/API"; +import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse"; +import HTTPResponse from "Common/Types/API/HTTPResponse"; const LoginPage: () => JSX.Element = () => { const apiUrl: URL = LOGIN_API_URL; @@ -24,6 +33,22 @@ const LoginPage: () => JSX.Element = () => { const [initialValues, setInitialValues] = React.useState({}); + const [showTwoFactorAuth, setShowTwoFactorAuth] = + React.useState(false); + + const [twoFactorAuthList, setTwoFactorAuthList] = React.useState< + UserTwoFactorAuth[] + >([]); + + const [selectedTwoFactorAuth, setSelectedTwoFactorAuth] = React.useState< + UserTwoFactorAuth | undefined + >(undefined); + + const [isTwoFactorAuthLoading, setIsTwoFactorAuthLoading] = + React.useState(false); + const [twofactorAuthError, setTwoFactorAuthError] = + React.useState(""); + useAsyncEffect(async () => { if (Navigation.getQueryStringByName("email")) { setInitialValues({ @@ -32,6 +57,20 @@ const LoginPage: () => JSX.Element = () => { } }, []); + type LoginFunction = (user: User, miscData: JSONObject) => void; + + const login: LoginFunction = (user: User, miscData: JSONObject): void => { + if (user instanceof User && user && user.email) { + UiAnalytics.userAuth(user.email); + UiAnalytics.capture("accounts/login"); + } + + LoginUtil.login({ + user: user, + token: miscData ? miscData["token"] : undefined, + }); + }; + return (
@@ -51,67 +90,145 @@ const LoginPage: () => JSX.Element = () => {
- - modelType={User} - id="login-form" - name="Login" - fields={[ - { - field: { - email: true, + {!showTwoFactorAuth && ( + + modelType={User} + id="login-form" + name="Login" + fields={[ + { + field: { + email: true, + }, + fieldType: FormFieldSchemaType.Email, + placeholder: "jeff@example.com", + required: true, + disabled: Boolean(initialValues && initialValues["email"]), + title: "Email", + dataTestId: "email", }, - fieldType: FormFieldSchemaType.Email, - placeholder: "jeff@example.com", - required: true, - disabled: Boolean(initialValues && initialValues["email"]), - title: "Email", - dataTestId: "email", - }, - { - field: { - password: true, + { + field: { + password: true, + }, + title: "Password", + required: true, + validation: { + minLength: 6, + }, + fieldType: FormFieldSchemaType.Password, + sideLink: { + text: "Forgot password?", + url: new Route("/accounts/forgot-password"), + openLinkInNewTab: false, + }, + dataTestId: "password", }, - title: "Password", - required: true, - validation: { - minLength: 6, - }, - fieldType: FormFieldSchemaType.Password, - sideLink: { - text: "Forgot password?", - url: new Route("/accounts/forgot-password"), - openLinkInNewTab: false, - }, - dataTestId: "password", - }, - ]} - createOrUpdateApiUrl={apiUrl} - formType={FormType.Create} - submitButtonText={"Login"} - onSuccess={(value: User, miscData: JSONObject | undefined) => { - if (value && value.email) { - UiAnalytics.userAuth(value.email); - UiAnalytics.capture("accounts/login"); - } + ]} + createOrUpdateApiUrl={apiUrl} + formType={FormType.Create} + submitButtonText={"Login"} + onBeforeCreate={(data: User) => { + setInitialValues(User.toJSON(data, User)); + return Promise.resolve(data); + }} + onSuccess={( + value: User | JSONObject, + miscData: JSONObject | undefined, + ) => { + if ((value as JSONObject)["twoFactorAuth"] === true) { + const twoFactorAuthList: Array = + UserTwoFactorAuth.fromJSONArray( + (value as JSONObject)["twoFactorAuthList"] as JSONArray, + UserTwoFactorAuth, + ); + setTwoFactorAuthList(twoFactorAuthList); + setShowTwoFactorAuth(true); + return; + } - LoginUtil.login({ - user: value, - token: miscData ? miscData["token"] : undefined, - }); - }} - maxPrimaryButtonWidth={true} - footer={ -
-
- -
- Use single sign-on (SSO) instead -
- + login(value as User, miscData as JSONObject); + }} + maxPrimaryButtonWidth={true} + footer={ +
+
+ +
+ Use single sign-on (SSO) instead +
+ +
-
- } - /> + } + /> + )} + + {showTwoFactorAuth && !selectedTwoFactorAuth && ( + + titleField="name" + descriptionField="" + selectedItems={[]} + list={twoFactorAuthList} + onClick={(item: UserTwoFactorAuth) => { + setSelectedTwoFactorAuth(item); + }} + /> + )} + + {showTwoFactorAuth && selectedTwoFactorAuth && ( + { + setIsTwoFactorAuthLoading(true); + + try { + const code: string = data["code"] as string; + + const result: HTTPErrorResponse | HTTPResponse = + await API.post(VERIFY_TWO_FACTOR_AUTH_API_URL, { + data: initialValues, + code: code, + }); + + if (result instanceof HTTPErrorResponse) { + throw result; + } + + const user: User = User.fromJSON( + result["data"] as JSONObject, + User, + ) as User; + const miscData: JSONObject = (result["data"] as JSONObject)[ + "miscData" + ] as JSONObject; + + login(user as User, miscData as JSONObject); + } catch (error) { + setTwoFactorAuthError( + API.getFriendlyErrorMessage(error as Error), + ); + } + + setIsTwoFactorAuthLoading(false); + }} + /> + )}
diff --git a/Accounts/src/Utils/ApiPaths.ts b/Accounts/src/Utils/ApiPaths.ts index b2bf715630..374fa0c13f 100644 --- a/Accounts/src/Utils/ApiPaths.ts +++ b/Accounts/src/Utils/ApiPaths.ts @@ -9,6 +9,10 @@ export const LOGIN_API_URL: URL = URL.fromURL(IDENTITY_URL).addRoute( new Route("/login"), ); +export const VERIFY_TWO_FACTOR_AUTH_API_URL: URL = URL.fromURL( + IDENTITY_URL, +).addRoute(new Route("/verify-two-factor-auth")); + export const SERVICE_PROVIDER_LOGIN_URL: URL = URL.fromURL( IDENTITY_URL, ).addRoute(new Route("/service-provider-login")); diff --git a/App/FeatureSet/Identity/API/Authentication.ts b/App/FeatureSet/Identity/API/Authentication.ts index c30986e879..6941bc9dac 100644 --- a/App/FeatureSet/Identity/API/Authentication.ts +++ b/App/FeatureSet/Identity/API/Authentication.ts @@ -518,59 +518,6 @@ router.post( }, ); -router.post( - "/fetch-two-factor-auth-list", - async ( - req: ExpressRequest, - res: ExpressResponse, - next: NextFunction, - ): Promise => { - try { - const data: JSONObject = req.body["data"]; - - if (!data["userId"]) { - return Response.sendErrorResponse( - req, - res, - new BadDataException( - "User Id is required to fetch the two factor auth list.", - ), - ); - } - - const userId: ObjectID = new ObjectID(data["userId"] as string); - - const twoFactorAuthList: Array = - await UserTwoFactorAuthService.findBy({ - query: { - userId: userId, - isVerified: true, - }, - select: { - _id: true, - userId: true, - name: true, - }, - limit: LIMIT_PER_PROJECT, - skip: 0, - props: { - isRoot: true, - }, - }); - - return Response.sendEntityArrayResponse( - req, - res, - twoFactorAuthList, - twoFactorAuthList.length, - UserTwoFactorAuth, - ); - } catch (err) { - return next(err); - } - }, -); - router.post( "/login", async ( @@ -587,7 +534,42 @@ router.post( }, ); -const login = async (options: { +type FetchTwoFactorAuthListFunction = ( + userId: ObjectID, +) => Promise>; + +const fetchTwoFactorAuthList: FetchTwoFactorAuthListFunction = async ( + userId: ObjectID, +): Promise> => { + const twoFactorAuthList: Array = + await UserTwoFactorAuthService.findBy({ + query: { + userId: userId, + isVerified: true, + }, + select: { + _id: true, + userId: true, + name: true, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + props: { + isRoot: true, + }, + }); + + return twoFactorAuthList; +}; + +type LoginFunction = (options: { + req: ExpressRequest; + res: ExpressResponse; + next: NextFunction; + verifyTwoFactorAuth: boolean; +}) => Promise; + +const login: LoginFunction = async (options: { req: ExpressRequest; res: ExpressResponse; next: NextFunction; @@ -648,8 +630,25 @@ const login = async (options: { if (alreadySavedUser.enableTwoFactorAuth && !verifyTwoFactorAuth) { // If two factor auth is enabled then we will send the user to the two factor auth page. + + const twoFactorAuthList: Array = + await fetchTwoFactorAuthList(alreadySavedUser.id!); + + if (!twoFactorAuthList || twoFactorAuthList.length === 0) { + const errorMessage: string = IsBillingEnabled + ? "Two Factor Authentication is enabled but no two factor auth is setup. Please contact OneUptime support for help." + : "Two Factor Authentication is enabled but no two factor auth is setup. Please contact your server admin to disable two factor auth for this account."; + + return Response.sendErrorResponse( + req, + res, + new BadDataException(errorMessage), + ); + } + return Response.sendJsonObjectResponse(req, res, { twoFactorAuth: true, + twoFactorAuthList: twoFactorAuthList, userId: alreadySavedUser.id?.toString(), }); }