From 5148435f5fa7b8adc1e0771ba9d6b71d15333463 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 17 Feb 2025 09:20:19 -0800 Subject: [PATCH 1/4] Move user to create org screen on no org --- backend/package-lock.json | 2 +- .../src/pages/middlewares/authenticate.tsx | 2 +- .../organization/NoOrgPage/NoOrgPage.tsx | 2 +- .../pages/organization/NoOrgPage/route.tsx | 12 ++-- frontend/src/pages/organization/layout.tsx | 12 ++-- frontend/src/routeTree.gen.ts | 61 +++++++++---------- frontend/src/routes.ts | 2 +- 7 files changed, 46 insertions(+), 47 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 6373ef244..873dbb217 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -21,7 +21,7 @@ "@fastify/etag": "^5.1.0", "@fastify/formbody": "^7.4.0", "@fastify/helmet": "^11.1.1", - "@fastify/multipart": "^8.3.1", + "@fastify/multipart": "8.3.1", "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.0.0", "@fastify/request-context": "^5.1.0", diff --git a/frontend/src/pages/middlewares/authenticate.tsx b/frontend/src/pages/middlewares/authenticate.tsx index cb9690837..56b3fdbfa 100644 --- a/frontend/src/pages/middlewares/authenticate.tsx +++ b/frontend/src/pages/middlewares/authenticate.tsx @@ -27,7 +27,7 @@ export const Route = createFileRoute("/_authenticate")({ }); }); - if (!data.organizationId && location.pathname !== ROUTE_PATHS.Auth.PasswordSetupPage.path) { + if (!data.organizationId && location.pathname !== ROUTE_PATHS.Auth.PasswordSetupPage.path && location.pathname !== "/organization/none") { throw redirect({ to: "/login/select-organization" }); } diff --git a/frontend/src/pages/organization/NoOrgPage/NoOrgPage.tsx b/frontend/src/pages/organization/NoOrgPage/NoOrgPage.tsx index 9eca51722..495842bf5 100644 --- a/frontend/src/pages/organization/NoOrgPage/NoOrgPage.tsx +++ b/frontend/src/pages/organization/NoOrgPage/NoOrgPage.tsx @@ -20,7 +20,7 @@ export const NoOrgPage = () => { {t("common.head-title", { title: t("settings.org.title") })} -
+
handlePopUpToggle("createOrg", false)} diff --git a/frontend/src/pages/organization/NoOrgPage/route.tsx b/frontend/src/pages/organization/NoOrgPage/route.tsx index 5c5573dc8..d89f77c1c 100644 --- a/frontend/src/pages/organization/NoOrgPage/route.tsx +++ b/frontend/src/pages/organization/NoOrgPage/route.tsx @@ -1,9 +1,7 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute } from '@tanstack/react-router' -import { NoOrgPage } from "./NoOrgPage"; +import { NoOrgPage } from './NoOrgPage' -export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/none" -)({ - component: NoOrgPage -}); +export const Route = createFileRoute('/_authenticate/organization/none')({ + component: NoOrgPage, +}) diff --git a/frontend/src/pages/organization/layout.tsx b/frontend/src/pages/organization/layout.tsx index 79bdbf216..5da0757be 100644 --- a/frontend/src/pages/organization/layout.tsx +++ b/frontend/src/pages/organization/layout.tsx @@ -1,7 +1,9 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute } from '@tanstack/react-router' -import { OrganizationLayout } from "@app/layouts/OrganizationLayout"; +import { OrganizationLayout } from '@app/layouts/OrganizationLayout' -export const Route = createFileRoute("/_authenticate/_inject-org-details/_org-layout")({ - component: OrganizationLayout -}); +export const Route = createFileRoute( + '/_authenticate/_inject-org-details/_org-layout', +)({ + component: OrganizationLayout, +}) diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 968e098cf..1791af6f1 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -33,6 +33,7 @@ import { Route as authLoginSsoPageRouteImport } from './pages/auth/LoginSsoPage/ import { Route as authSelectOrgPageRouteImport } from './pages/auth/SelectOrgPage/route' import { Route as authLoginLdapPageRouteImport } from './pages/auth/LoginLdapPage/route' import { Route as adminSignUpPageRouteImport } from './pages/admin/SignUpPage/route' +import { Route as organizationNoOrgPageRouteImport } from './pages/organization/NoOrgPage/route' import { Route as authSignUpPageRouteImport } from './pages/auth/SignUpPage/route' import { Route as authLoginPageRouteImport } from './pages/auth/LoginPage/route' import { Route as adminLayoutImport } from './pages/admin/layout' @@ -42,7 +43,6 @@ import { Route as userPersonalSettingsPageRouteImport } from './pages/user/Perso import { Route as organizationSettingsPageRouteImport } from './pages/organization/SettingsPage/route' import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route' import { Route as organizationSecretScanningPageRouteImport } from './pages/organization/SecretScanningPage/route' -import { Route as organizationNoOrgPageRouteImport } from './pages/organization/NoOrgPage/route' import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route' import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route' import { Route as organizationAdminPageRouteImport } from './pages/organization/AdminPage/route' @@ -378,6 +378,14 @@ const adminSignUpPageRouteRoute = adminSignUpPageRouteImport.update({ getParentRoute: () => middlewaresRestrictLoginSignupRoute, } as any) +const organizationNoOrgPageRouteRoute = organizationNoOrgPageRouteImport.update( + { + id: '/organization/none', + path: '/organization/none', + getParentRoute: () => middlewaresAuthenticateRoute, + } as any, +) + const authSignUpPageRouteRoute = authSignUpPageRouteImport.update({ id: '/', path: '/', @@ -491,15 +499,6 @@ const organizationSecretScanningPageRouteRoute = AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, } as any) -const organizationNoOrgPageRouteRoute = organizationNoOrgPageRouteImport.update( - { - id: '/none', - path: '/none', - getParentRoute: () => - AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute, - } as any, -) - const organizationBillingPageRouteRoute = organizationBillingPageRouteImport.update({ id: '/billing', @@ -1691,6 +1690,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof authSignUpPageRouteImport parentRoute: typeof RestrictLoginSignupSignupImport } + '/_authenticate/organization/none': { + id: '/_authenticate/organization/none' + path: '/organization/none' + fullPath: '/organization/none' + preLoaderRoute: typeof organizationNoOrgPageRouteImport + parentRoute: typeof middlewaresAuthenticateImport + } '/_restrict-login-signup/admin/signup': { id: '/_restrict-login-signup/admin/signup' path: '/admin/signup' @@ -1831,13 +1837,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof organizationBillingPageRouteImport parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport } - '/_authenticate/_inject-org-details/_org-layout/organization/none': { - id: '/_authenticate/_inject-org-details/_org-layout/organization/none' - path: '/none' - fullPath: '/organization/none' - preLoaderRoute: typeof organizationNoOrgPageRouteImport - parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport - } '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning': { id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning' path: '/secret-scanning' @@ -2892,7 +2891,6 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren { organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute - organizationNoOrgPageRouteRoute: typeof organizationNoOrgPageRouteRoute organizationSecretScanningPageRouteRoute: typeof organizationSecretScanningPageRouteRoute organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute @@ -2914,7 +2912,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica organizationAdminPageRouteRoute: organizationAdminPageRouteRoute, organizationAuditLogsPageRouteRoute: organizationAuditLogsPageRouteRoute, organizationBillingPageRouteRoute: organizationBillingPageRouteRoute, - organizationNoOrgPageRouteRoute: organizationNoOrgPageRouteRoute, organizationSecretScanningPageRouteRoute: organizationSecretScanningPageRouteRoute, organizationSecretSharingPageRouteRoute: @@ -3469,6 +3466,7 @@ interface middlewaresAuthenticateRouteChildren { authPasswordSetupPageRouteRoute: typeof authPasswordSetupPageRouteRoute middlewaresInjectOrgDetailsRoute: typeof middlewaresInjectOrgDetailsRouteWithChildren AuthenticatePersonalSettingsRoute: typeof AuthenticatePersonalSettingsRouteWithChildren + organizationNoOrgPageRouteRoute: typeof organizationNoOrgPageRouteRoute } const middlewaresAuthenticateRouteChildren: middlewaresAuthenticateRouteChildren = @@ -3478,6 +3476,7 @@ const middlewaresAuthenticateRouteChildren: middlewaresAuthenticateRouteChildren middlewaresInjectOrgDetailsRouteWithChildren, AuthenticatePersonalSettingsRoute: AuthenticatePersonalSettingsRouteWithChildren, + organizationNoOrgPageRouteRoute: organizationNoOrgPageRouteRoute, } const middlewaresAuthenticateRouteWithChildren = @@ -3569,6 +3568,7 @@ export interface FileRoutesByFullPath { '/signup': typeof RestrictLoginSignupSignupRouteWithChildren '/login/': typeof authLoginPageRouteRoute '/signup/': typeof authSignUpPageRouteRoute + '/organization/none': typeof organizationNoOrgPageRouteRoute '/admin/signup': typeof adminSignUpPageRouteRoute '/login/ldap': typeof authLoginLdapPageRouteRoute '/login/select-organization': typeof authSelectOrgPageRouteRoute @@ -3586,7 +3586,6 @@ export interface FileRoutesByFullPath { '/organization/admin': typeof organizationAdminPageRouteRoute '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute - '/organization/none': typeof organizationNoOrgPageRouteRoute '/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute '/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute '/organization/settings': typeof organizationSettingsPageRouteRoute @@ -3740,6 +3739,7 @@ export interface FileRoutesByTo { '/personal-settings': typeof userPersonalSettingsPageRouteRoute '/login': typeof authLoginPageRouteRoute '/signup': typeof authSignUpPageRouteRoute + '/organization/none': typeof organizationNoOrgPageRouteRoute '/admin/signup': typeof adminSignUpPageRouteRoute '/login/ldap': typeof authLoginLdapPageRouteRoute '/login/select-organization': typeof authSelectOrgPageRouteRoute @@ -3755,7 +3755,6 @@ export interface FileRoutesByTo { '/organization/admin': typeof organizationAdminPageRouteRoute '/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/organization/billing': typeof organizationBillingPageRouteRoute - '/organization/none': typeof organizationNoOrgPageRouteRoute '/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute '/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute '/organization/settings': typeof organizationSettingsPageRouteRoute @@ -3912,6 +3911,7 @@ export interface FileRoutesById { '/_restrict-login-signup/signup': typeof RestrictLoginSignupSignupRouteWithChildren '/_restrict-login-signup/login/': typeof authLoginPageRouteRoute '/_restrict-login-signup/signup/': typeof authSignUpPageRouteRoute + '/_authenticate/organization/none': typeof organizationNoOrgPageRouteRoute '/_restrict-login-signup/admin/signup': typeof adminSignUpPageRouteRoute '/_restrict-login-signup/login/ldap': typeof authLoginLdapPageRouteRoute '/_restrict-login-signup/login/select-organization': typeof authSelectOrgPageRouteRoute @@ -3932,7 +3932,6 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organization/admin': typeof organizationAdminPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute - '/_authenticate/_inject-org-details/_org-layout/organization/none': typeof organizationNoOrgPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning': typeof organizationSecretScanningPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organization/settings': typeof organizationSettingsPageRouteRoute @@ -4094,6 +4093,7 @@ export interface FileRouteTypes { | '/signup' | '/login/' | '/signup/' + | '/organization/none' | '/admin/signup' | '/login/ldap' | '/login/select-organization' @@ -4111,7 +4111,6 @@ export interface FileRouteTypes { | '/organization/admin' | '/organization/audit-logs' | '/organization/billing' - | '/organization/none' | '/organization/secret-scanning' | '/organization/secret-sharing' | '/organization/settings' @@ -4264,6 +4263,7 @@ export interface FileRouteTypes { | '/personal-settings' | '/login' | '/signup' + | '/organization/none' | '/admin/signup' | '/login/ldap' | '/login/select-organization' @@ -4279,7 +4279,6 @@ export interface FileRouteTypes { | '/organization/admin' | '/organization/audit-logs' | '/organization/billing' - | '/organization/none' | '/organization/secret-scanning' | '/organization/secret-sharing' | '/organization/settings' @@ -4434,6 +4433,7 @@ export interface FileRouteTypes { | '/_restrict-login-signup/signup' | '/_restrict-login-signup/login/' | '/_restrict-login-signup/signup/' + | '/_authenticate/organization/none' | '/_restrict-login-signup/admin/signup' | '/_restrict-login-signup/login/ldap' | '/_restrict-login-signup/login/select-organization' @@ -4454,7 +4454,6 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organization/admin' | '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs' | '/_authenticate/_inject-org-details/_org-layout/organization/billing' - | '/_authenticate/_inject-org-details/_org-layout/organization/none' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning' | '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing' | '/_authenticate/_inject-org-details/_org-layout/organization/settings' @@ -4651,7 +4650,8 @@ export const routeTree = rootRoute "children": [ "/_authenticate/password-setup", "/_authenticate/_inject-org-details", - "/_authenticate/personal-settings" + "/_authenticate/personal-settings", + "/_authenticate/organization/none" ] }, "/_restrict-login-signup": { @@ -4734,6 +4734,10 @@ export const routeTree = rootRoute "filePath": "auth/SignUpPage/route.tsx", "parent": "/_restrict-login-signup/signup" }, + "/_authenticate/organization/none": { + "filePath": "organization/NoOrgPage/route.tsx", + "parent": "/_authenticate" + }, "/_restrict-login-signup/admin/signup": { "filePath": "admin/SignUpPage/route.tsx", "parent": "/_restrict-login-signup" @@ -4818,7 +4822,6 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/organization/admin", "/_authenticate/_inject-org-details/_org-layout/organization/audit-logs", "/_authenticate/_inject-org-details/_org-layout/organization/billing", - "/_authenticate/_inject-org-details/_org-layout/organization/none", "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning", "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing", "/_authenticate/_inject-org-details/_org-layout/organization/settings", @@ -4860,10 +4863,6 @@ export const routeTree = rootRoute "filePath": "organization/BillingPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" }, - "/_authenticate/_inject-org-details/_org-layout/organization/none": { - "filePath": "organization/NoOrgPage/route.tsx", - "parent": "/_authenticate/_inject-org-details/_org-layout/organization" - }, "/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning": { "filePath": "organization/SecretScanningPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/organization" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index c284f816f..eb3a1caf3 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -16,7 +16,6 @@ const organizationRoutes = route("/organization", [ route("/admin", "organization/AdminPage/route.tsx"), route("/audit-logs", "organization/AuditLogsPage/route.tsx"), route("/billing", "organization/BillingPage/route.tsx"), - route("/none", "organization/NoOrgPage/route.tsx"), route("/secret-sharing", "organization/SecretSharingPage/route.tsx"), route("/settings", "organization/SettingsPage/route.tsx"), route("/secret-scanning", "organization/SecretScanningPage/route.tsx"), @@ -342,6 +341,7 @@ export const routes = rootRoute("root.tsx", [ route("/personal-settings", [ layout("user/layout.tsx", [index("user/PersonalSettingsPage/route.tsx")]) ]), + route("/organization/none", "organization/NoOrgPage/route.tsx"), middleware("inject-org-details.tsx", [ adminRoute, layout("org-layout", "organization/layout.tsx", [ From 823bf134dddd2273458be4f75b069c05548b4b50 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 18 Feb 2025 18:35:22 -0800 Subject: [PATCH 2/4] Fix case where user can get stuck in a deleted organization if they were prev logged into it --- backend/package-lock.json | 2 +- backend/src/server/routes/v1/auth-router.ts | 44 +++------------- .../services/auth-token/auth-token-service.ts | 38 +++++++++++++- backend/src/services/auth/auth-fns.ts | 1 - frontend/src/hooks/api/users/queries.tsx | 51 ++++++++++--------- .../src/pages/auth/LoginPage/LoginPage.tsx | 1 + .../SignUpInvitePage/SignUpInvitePage.tsx | 28 +++++----- .../src/pages/middlewares/authenticate.tsx | 30 +++++++++-- 8 files changed, 114 insertions(+), 81 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 873dbb217..6373ef244 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -21,7 +21,7 @@ "@fastify/etag": "^5.1.0", "@fastify/formbody": "^7.4.0", "@fastify/helmet": "^11.1.1", - "@fastify/multipart": "8.3.1", + "@fastify/multipart": "^8.3.1", "@fastify/passport": "^2.4.0", "@fastify/rate-limit": "^9.0.0", "@fastify/request-context": "^5.1.0", diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index 46f856105..4465053ea 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -1,11 +1,9 @@ import jwt from "jsonwebtoken"; import { z } from "zod"; - import { getConfig } from "@app/lib/config/env"; -import { NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; +import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type"; export const registerAuthRoutes = async (server: FastifyZodProvider) => { server.route({ @@ -21,18 +19,19 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), handler: async (req, res) => { + const { decodedToken } = await server.services.authToken.validateRefreshToken(req.cookies.jid); const appCfg = getConfig(); - if (req.auth.authMode === AuthMode.JWT) { - await server.services.login.logout(req.permission.id, req.auth.tokenVersionId); - } + + await server.services.login.logout(decodedToken.userId, decodedToken.tokenVersionId); + void res.cookie("jid", "", { httpOnly: true, path: "/", sameSite: "strict", secure: appCfg.HTTPS_ENABLED }); + return { message: "Successfully logged out" }; } }); @@ -69,37 +68,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { } }, handler: async (req) => { - const refreshToken = req.cookies.jid; + const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid); const appCfg = getConfig(); - if (!refreshToken) - throw new NotFoundError({ - name: "AuthTokenNotFound", - message: "Failed to find refresh token" - }); - - const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload; - if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) - throw new UnauthorizedError({ - message: "The token provided is not a refresh token", - name: "InvalidToken" - }); - - const tokenVersion = await server.services.authToken.getUserTokenSessionById( - decodedToken.tokenVersionId, - decodedToken.userId - ); - if (!tokenVersion) - throw new UnauthorizedError({ - message: "Valid token version not found", - name: "InvalidToken" - }); - - if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) { - throw new UnauthorizedError({ - message: "Token version mismatch", - name: "InvalidToken" - }); - } const token = jwt.sign( { diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index d15fa4543..67920acf1 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -1,4 +1,5 @@ import crypto from "node:crypto"; +import jwt from "jsonwebtoken"; import bcrypt from "bcrypt"; import { Knex } from "knex"; @@ -8,7 +9,7 @@ import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; -import { AuthModeJwtTokenPayload } from "../auth/auth-type"; +import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; import { TUserDALFactory } from "../user/user-dal"; import { TTokenDALFactory } from "./auth-token-dal"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; @@ -100,6 +101,40 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu return token; }; + const validateRefreshToken = async (refreshToken?: string) => { + const appCfg = getConfig(); + if (!refreshToken) + throw new NotFoundError({ + name: "AuthTokenNotFound", + message: "Failed to find refresh token" + }); + + const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload; + + if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) + throw new UnauthorizedError({ + message: "The token provided is not a refresh token", + name: "InvalidToken" + }); + + const tokenVersion = await getUserTokenSessionById(decodedToken.tokenVersionId, decodedToken.userId); + + if (!tokenVersion) + throw new UnauthorizedError({ + message: "Valid token version not found", + name: "InvalidToken" + }); + + if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) { + throw new UnauthorizedError({ + message: "Token version mismatch", + name: "InvalidToken" + }); + } + + return { decodedToken, tokenVersion }; + }; + const validateTokenForUser = async ({ type, userId, @@ -183,6 +218,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu return { createTokenForUser, + validateRefreshToken, validateTokenForUser, getUserTokenSession, clearTokenSessionById, diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index 5f7aca812..c73b32c1e 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -2,7 +2,6 @@ import jwt from "jsonwebtoken"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; - import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type"; export const validateProviderAuthToken = (providerToken: string, username?: string) => { diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 88486ee48..6b85af9eb 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -24,10 +24,10 @@ import { User, UserEnc } from "./types"; +import { queryClient } from "@app/hooks/api/reactQuery"; export const fetchUserDetails = async () => { const { data } = await apiRequest.get<{ user: User & UserEnc }>("/api/v1/user"); - return data.user; }; @@ -278,30 +278,33 @@ export const useRegisterUserAction = () => { }); }; -export const useLogoutUser = (keepQueryClient?: boolean) => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async () => { - await apiRequest.post("/api/v1/auth/logout"); - }, - onSuccess: () => { - setAuthToken(""); - // Delete the cookie by not setting a value; Alternatively clear the local storage - localStorage.removeItem("protectedKey"); - localStorage.removeItem("protectedKeyIV"); - localStorage.removeItem("protectedKeyTag"); - localStorage.removeItem("publicKey"); - localStorage.removeItem("encryptedPrivateKey"); - localStorage.removeItem("iv"); - localStorage.removeItem("tag"); - localStorage.removeItem("PRIVATE_KEY"); - localStorage.removeItem("orgData.id"); - sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN); +export const logoutUser = async () => { + await apiRequest.post("/api/v1/auth/logout"); +}; - if (!keepQueryClient) { - queryClient.clear(); - } - } +// Utility function to clear session storage and query cache +export const clearSession = (keepQueryClient?: boolean) => { + setAuthToken(""); // Clear authentication token + localStorage.removeItem("protectedKey"); + localStorage.removeItem("protectedKeyIV"); + localStorage.removeItem("protectedKeyTag"); + localStorage.removeItem("publicKey"); + localStorage.removeItem("encryptedPrivateKey"); + localStorage.removeItem("iv"); + localStorage.removeItem("tag"); + localStorage.removeItem("PRIVATE_KEY"); + localStorage.removeItem("orgData.id"); + sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN); + + if (!keepQueryClient) { + queryClient.clear(); // Clear React Query cache + } +}; + +export const useLogoutUser = (keepQueryClient?: boolean) => { + return useMutation({ + mutationFn: logoutUser, + onSuccess: () => clearSession(keepQueryClient) }); }; diff --git a/frontend/src/pages/auth/LoginPage/LoginPage.tsx b/frontend/src/pages/auth/LoginPage/LoginPage.tsx index c7f121bb1..74ced95ef 100644 --- a/frontend/src/pages/auth/LoginPage/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage/LoginPage.tsx @@ -33,6 +33,7 @@ export const LoginPage = () => { console.log("Error - Not logged in yet"); } }; + if (isLoggedIn()) { handleRedirects(); } diff --git a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx index 3252bc9f9..d3106e341 100644 --- a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx +++ b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx @@ -250,20 +250,24 @@ export const SignupInvitePage = () => { setStep(2); } else { const redirectExistingUser = async () => { - const { token: mfaToken, isMfaEnabled } = await selectOrganization({ - organizationId - }); + try { + const { token: mfaToken, isMfaEnabled } = await selectOrganization({ + organizationId + }); - if (isMfaEnabled) { - SecurityClient.setMfaToken(mfaToken); - toggleShowMfa.on(); - setMfaSuccessCallback(() => redirectExistingUser); - return; + if (isMfaEnabled) { + SecurityClient.setMfaToken(mfaToken); + toggleShowMfa.on(); + setMfaSuccessCallback(() => redirectExistingUser); + return; + } + + // user will be redirected to dashboard + // if not logged in gets kicked out to login + await navigateUserToOrg(navigate, organizationId); + } catch (err) { + navigate({ to: "/login" }); } - - // user will be redirected to dashboard - // if not logged in gets kicked out to login - await navigateUserToOrg(navigate, organizationId); }; await redirectExistingUser(); diff --git a/frontend/src/pages/middlewares/authenticate.tsx b/frontend/src/pages/middlewares/authenticate.tsx index 56b3fdbfa..93462a095 100644 --- a/frontend/src/pages/middlewares/authenticate.tsx +++ b/frontend/src/pages/middlewares/authenticate.tsx @@ -5,12 +5,15 @@ import { ROUTE_PATHS } from "@app/const/routes"; import { userKeys } from "@app/hooks/api"; import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries"; import { fetchUserDetails } from "@app/hooks/api/users/queries"; +import { AxiosError } from "axios"; +import { clearSession, logoutUser } from "@app/hooks/api/users/queries"; export const Route = createFileRoute("/_authenticate")({ beforeLoad: async ({ context, location }) => { if (!context.serverConfig.initialized) { throw redirect({ to: "/admin/signup" }); } + const data = await context.queryClient .ensureQueryData({ queryKey: authKeys.getAuthToken, @@ -27,14 +30,31 @@ export const Route = createFileRoute("/_authenticate")({ }); }); - if (!data.organizationId && location.pathname !== ROUTE_PATHS.Auth.PasswordSetupPage.path && location.pathname !== "/organization/none") { + if ( + !data.organizationId && + location.pathname !== ROUTE_PATHS.Auth.PasswordSetupPage.path && + location.pathname !== "/organization/none" + ) { throw redirect({ to: "/login/select-organization" }); } - const user = await context.queryClient.ensureQueryData({ - queryKey: userKeys.getUser, - queryFn: fetchUserDetails - }); + const user = await context.queryClient + .ensureQueryData({ + queryKey: userKeys.getUser, + queryFn: fetchUserDetails + }) + .catch(async (error) => { + const err = error as AxiosError; + if (err.response?.status === 403) { + // (dangtony98): this edge-case can occur if the user's token corresponds to an organization + // that has been deleted for which we must clear the refresh token in http-only cookie + clearSession(true); + await logoutUser(); + throw redirect({ + to: "/login" + }); + } + }); return { organizationId: data.organizationId as string, isAuthenticated: true, user }; } From 012f265363784a9c9eecc436ea4bf8ce047c4177 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 18 Feb 2025 18:42:39 -0800 Subject: [PATCH 3/4] Fix lint issues --- backend/src/server/routes/v1/auth-router.ts | 1 + .../services/auth-token/auth-token-service.ts | 72 +++++++++---------- backend/src/services/auth/auth-fns.ts | 1 + .../src/services/kms/kms-root-config-dal.ts | 3 +- frontend/src/hooks/api/users/queries.tsx | 4 +- .../src/pages/middlewares/authenticate.tsx | 5 +- .../pages/organization/NoOrgPage/route.tsx | 10 +-- frontend/src/pages/organization/layout.tsx | 12 ++-- 8 files changed, 54 insertions(+), 54 deletions(-) diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index 4465053ea..04ca958c6 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -1,5 +1,6 @@ import jwt from "jsonwebtoken"; import { z } from "zod"; + import { getConfig } from "@app/lib/config/env"; import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 67920acf1..2468e3c8a 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; -import jwt from "jsonwebtoken"; import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; import { Knex } from "knex"; import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; @@ -101,40 +101,6 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu return token; }; - const validateRefreshToken = async (refreshToken?: string) => { - const appCfg = getConfig(); - if (!refreshToken) - throw new NotFoundError({ - name: "AuthTokenNotFound", - message: "Failed to find refresh token" - }); - - const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload; - - if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) - throw new UnauthorizedError({ - message: "The token provided is not a refresh token", - name: "InvalidToken" - }); - - const tokenVersion = await getUserTokenSessionById(decodedToken.tokenVersionId, decodedToken.userId); - - if (!tokenVersion) - throw new UnauthorizedError({ - message: "Valid token version not found", - name: "InvalidToken" - }); - - if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) { - throw new UnauthorizedError({ - message: "Token version mismatch", - name: "InvalidToken" - }); - } - - return { decodedToken, tokenVersion }; - }; - const validateTokenForUser = async ({ type, userId, @@ -185,6 +151,40 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId }); + const validateRefreshToken = async (refreshToken?: string) => { + const appCfg = getConfig(); + if (!refreshToken) + throw new NotFoundError({ + name: "AuthTokenNotFound", + message: "Failed to find refresh token" + }); + + const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload; + + if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) + throw new UnauthorizedError({ + message: "The token provided is not a refresh token", + name: "InvalidToken" + }); + + const tokenVersion = await getUserTokenSessionById(decodedToken.tokenVersionId, decodedToken.userId); + + if (!tokenVersion) + throw new UnauthorizedError({ + message: "Valid token version not found", + name: "InvalidToken" + }); + + if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) { + throw new UnauthorizedError({ + message: "Token version mismatch", + name: "InvalidToken" + }); + } + + return { decodedToken, tokenVersion }; + }; + // to parse jwt identity in inject identity plugin const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => { const session = await tokenDAL.findOneTokenSession({ @@ -218,12 +218,12 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu return { createTokenForUser, - validateRefreshToken, validateTokenForUser, getUserTokenSession, clearTokenSessionById, getTokenSessionByUser, revokeAllMySessions, + validateRefreshToken, fnValidateJwtIdentity, getUserTokenSessionById }; diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index c73b32c1e..5f7aca812 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -2,6 +2,7 @@ import jwt from "jsonwebtoken"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; + import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type"; export const validateProviderAuthToken = (providerToken: string, username?: string) => { diff --git a/backend/src/services/kms/kms-root-config-dal.ts b/backend/src/services/kms/kms-root-config-dal.ts index 8745d286e..31826b79d 100644 --- a/backend/src/services/kms/kms-root-config-dal.ts +++ b/backend/src/services/kms/kms-root-config-dal.ts @@ -1,8 +1,9 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; -import { Knex } from "knex"; export type TKmsRootConfigDALFactory = ReturnType; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 6b85af9eb..06cde4d34 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -3,6 +3,7 @@ import { AxiosError } from "axios"; import { apiRequest } from "@app/config/request"; import { SessionStorageKeys } from "@app/const"; +import { queryClient as qc } from "@app/hooks/api/reactQuery"; import { APIKeyDataV2 } from "../apiKeys/types"; import { MfaMethod } from "../auth/types"; @@ -24,7 +25,6 @@ import { User, UserEnc } from "./types"; -import { queryClient } from "@app/hooks/api/reactQuery"; export const fetchUserDetails = async () => { const { data } = await apiRequest.get<{ user: User & UserEnc }>("/api/v1/user"); @@ -297,7 +297,7 @@ export const clearSession = (keepQueryClient?: boolean) => { sessionStorage.removeItem(SessionStorageKeys.CLI_TERMINAL_TOKEN); if (!keepQueryClient) { - queryClient.clear(); // Clear React Query cache + qc.clear(); // Clear React Query cache } }; diff --git a/frontend/src/pages/middlewares/authenticate.tsx b/frontend/src/pages/middlewares/authenticate.tsx index 93462a095..2b1e79a7b 100644 --- a/frontend/src/pages/middlewares/authenticate.tsx +++ b/frontend/src/pages/middlewares/authenticate.tsx @@ -1,12 +1,11 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; +import { AxiosError } from "axios"; import { createNotification } from "@app/components/notifications"; import { ROUTE_PATHS } from "@app/const/routes"; import { userKeys } from "@app/hooks/api"; import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries"; -import { fetchUserDetails } from "@app/hooks/api/users/queries"; -import { AxiosError } from "axios"; -import { clearSession, logoutUser } from "@app/hooks/api/users/queries"; +import { clearSession, fetchUserDetails, logoutUser } from "@app/hooks/api/users/queries"; export const Route = createFileRoute("/_authenticate")({ beforeLoad: async ({ context, location }) => { diff --git a/frontend/src/pages/organization/NoOrgPage/route.tsx b/frontend/src/pages/organization/NoOrgPage/route.tsx index d89f77c1c..5d34fed93 100644 --- a/frontend/src/pages/organization/NoOrgPage/route.tsx +++ b/frontend/src/pages/organization/NoOrgPage/route.tsx @@ -1,7 +1,7 @@ -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute } from "@tanstack/react-router"; -import { NoOrgPage } from './NoOrgPage' +import { NoOrgPage } from "./NoOrgPage"; -export const Route = createFileRoute('/_authenticate/organization/none')({ - component: NoOrgPage, -}) +export const Route = createFileRoute("/_authenticate/organization/none")({ + component: NoOrgPage +}); diff --git a/frontend/src/pages/organization/layout.tsx b/frontend/src/pages/organization/layout.tsx index 5da0757be..79bdbf216 100644 --- a/frontend/src/pages/organization/layout.tsx +++ b/frontend/src/pages/organization/layout.tsx @@ -1,9 +1,7 @@ -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute } from "@tanstack/react-router"; -import { OrganizationLayout } from '@app/layouts/OrganizationLayout' +import { OrganizationLayout } from "@app/layouts/OrganizationLayout"; -export const Route = createFileRoute( - '/_authenticate/_inject-org-details/_org-layout', -)({ - component: OrganizationLayout, -}) +export const Route = createFileRoute("/_authenticate/_inject-org-details/_org-layout")({ + component: OrganizationLayout +}); From 86b6d23f34ee982d1cd1e9ace03d2e016e0b0f61 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 19 Feb 2025 10:27:55 -0800 Subject: [PATCH 4/4] Fix lint issue --- frontend/src/pages/middlewares/authenticate.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/middlewares/authenticate.tsx b/frontend/src/pages/middlewares/authenticate.tsx index 2b1e79a7b..615d03b60 100644 --- a/frontend/src/pages/middlewares/authenticate.tsx +++ b/frontend/src/pages/middlewares/authenticate.tsx @@ -47,12 +47,18 @@ export const Route = createFileRoute("/_authenticate")({ if (err.response?.status === 403) { // (dangtony98): this edge-case can occur if the user's token corresponds to an organization // that has been deleted for which we must clear the refresh token in http-only cookie - clearSession(true); - await logoutUser(); - throw redirect({ - to: "/login" + createNotification({ + type: "error", + title: "Access Denied", + text: "Something went wrong with your session. Please log in again." }); } + clearSession(true); + await logoutUser(); + + throw redirect({ + to: "/login" + }); }); return { organizationId: data.organizationId as string, isAuthenticated: true, user };