From 502429d914f7a24ddbf834dda1b8e9c2f3b24d4d Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 10 Jan 2025 23:55:21 +0800 Subject: [PATCH] misc: resolve shared secret within org and added login redirect --- .../secret-sharing/secret-sharing-service.ts | 7 ++- frontend/src/const.ts | 3 +- frontend/src/hooks/api/auth/queries.tsx | 16 +++++++ .../ViewSharedSecretByIDPage.tsx | 47 +++++++++++++++++-- .../public/ViewSharedSecretByIDPage/route.tsx | 13 ++++- 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts index 171ab54db..b262cad5d 100644 --- a/backend/src/services/secret-sharing/secret-sharing-service.ts +++ b/backend/src/services/secret-sharing/secret-sharing-service.ts @@ -206,8 +206,13 @@ export const secretSharingServiceFactory = ({ const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : ""; - if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) + if (accessType === SecretSharingAccessType.Organization && orgId === undefined) { + throw new UnauthorizedError(); + } + + if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) { throw new ForbiddenRequestError(); + } // all secrets pass through here, meaning we check if its expired first and then check if it needs verification // or can be safely sent to the client. diff --git a/frontend/src/const.ts b/frontend/src/const.ts index d2623431d..797254893 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -58,7 +58,8 @@ export const leaveConfirmDefaultMessage = "Your changes will be lost if you leave the page. Are you sure you want to continue?"; export enum SessionStorageKeys { - CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN" + CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN", + ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL" } export const secretTagsColors = [ diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 07baf011e..8d8ee0c3f 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { apiRequest } from "@app/config/request"; +import { SessionStorageKeys } from "@app/const"; import { organizationKeys } from "../organization/queries"; import { setAuthToken } from "../reactQuery"; @@ -86,6 +87,21 @@ export const useSelectOrganization = () => { SecurityClient.setProviderAuthToken(""); } + if (data.token && !data.isMfaEnabled) { + // We check if there is a pending callback after organization login success and redirect to it if valid + const loginRedirectInfo = sessionStorage.getItem( + SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL + ); + sessionStorage.removeItem(SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL); + + if (loginRedirectInfo) { + const { expiry, data: redirectUrl } = JSON.parse(loginRedirectInfo); + if (new Date() < new Date(expiry)) { + window.location.assign(redirectUrl); + } + } + } + return data; }, onSuccess: () => { diff --git a/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx b/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx index 387982fbd..5112bd47e 100644 --- a/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx +++ b/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx @@ -1,10 +1,13 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Helmet } from "react-helmet"; import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useParams, useSearch } from "@tanstack/react-router"; +import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; import { AxiosError } from "axios"; +import { addSeconds, formatISO } from "date-fns"; +import { createNotification } from "@app/components/notifications"; +import { SessionStorageKeys } from "@app/const"; import { ROUTE_PATHS } from "@app/const/routes"; import { useGetActiveSharedSecretById } from "@app/hooks/api/secretSharing"; @@ -36,7 +39,6 @@ export const ViewSharedSecretByIDPage = () => { select: (el) => el.key }); const [password, setPassword] = useState(); - const { hashedHex, key } = extractDetailsFromUrl(urlEncodedKey); const { @@ -50,10 +52,47 @@ export const ViewSharedSecretByIDPage = () => { password }); + const navigate = useNavigate(); + + const isUnauthorized = + ((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode === 401; + + const isForbidden = + ((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode === 403; + const isInvalidCredential = ((error as AxiosError)?.response?.data as { message: string })?.message === "Invalid credentials"; + useEffect(() => { + if (isUnauthorized && !isInvalidCredential) { + // persist current URL in session storage so that we can come back to this after successful login + sessionStorage.setItem( + SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL, + JSON.stringify({ + expiry: formatISO(addSeconds(new Date(), 60)), + data: window.location.href + }) + ); + + createNotification({ + type: "info", + text: "Login is required in order to access the shared secret." + }); + + navigate({ + to: "/login" + }); + } + + if (isForbidden) { + createNotification({ + type: "error", + text: "You do not have access to this shared secret." + }); + } + }, [error]); + const shouldShowPasswordPrompt = isInvalidCredential || (fetchSecret?.isPasswordProtected && !fetchSecret.secret); const isValidatingPassword = Boolean(password) && isFetching; @@ -111,7 +150,7 @@ export const ViewSharedSecretByIDPage = () => { {!error && fetchSecret?.secret && ( )} - {error && !isInvalidCredential && } + {error && !isInvalidCredential && !isUnauthorized && } )}
diff --git a/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx b/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx index f50368503..7b98dded3 100644 --- a/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx +++ b/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx @@ -2,6 +2,8 @@ import { createFileRoute, stripSearchParams } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; +import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries"; + import { ViewSharedSecretByIDPage } from "./ViewSharedSecretByIDPage"; const SharedSecretByIDPageQuerySchema = z.object({ @@ -9,9 +11,18 @@ const SharedSecretByIDPageQuerySchema = z.object({ }); export const Route = createFileRoute("/shared/secret/$secretId")({ - component: ViewSharedSecretByIDPage, validateSearch: zodValidator(SharedSecretByIDPageQuerySchema), + component: ViewSharedSecretByIDPage, search: { middlewares: [stripSearchParams({ key: "" })] + }, + beforeLoad: async ({ context }) => { + // we load the auth token because the view shared secret screen serves both public and authenticated users + await context.queryClient + .ensureQueryData({ + queryKey: authKeys.getAuthToken, + queryFn: fetchAuthToken + }) + .catch(() => undefined); } });