Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix failing redirect to create new organization page on no organizations #3125

Merged
merged 6 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 7 additions & 37 deletions backend/src/server/routes/v1/auth-router.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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" };
}
});
Expand Down Expand Up @@ -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(
{
Expand Down
38 changes: 37 additions & 1 deletion backend/src/services/auth-token/auth-token-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import crypto from "node:crypto";
import jwt from "jsonwebtoken";

import bcrypt from "bcrypt";
import { Knex } from "knex";
Expand All @@ -8,7 +9,7 @@
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";
Expand Down Expand Up @@ -100,6 +101,40 @@
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);

Check failure on line 120 in backend/src/services/auth-token/auth-token-service.ts

View workflow job for this annotation

GitHub Actions / Check TS and Lint

'getUserTokenSessionById' was used before it was defined

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,
Expand Down Expand Up @@ -183,6 +218,7 @@

return {
createTokenForUser,
validateRefreshToken,
validateTokenForUser,
getUserTokenSession,
clearTokenSessionById,
Expand Down
1 change: 0 additions & 1 deletion backend/src/services/auth/auth-fns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
49 changes: 26 additions & 23 deletions frontend/src/hooks/api/users/queries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down Expand Up @@ -278,30 +278,33 @@ export const useRegisterUserAction = () => {
});
};

export const logoutUser = async () => {
await apiRequest.post("/api/v1/auth/logout");
};

// 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) => {
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);

if (!keepQueryClient) {
queryClient.clear();
}
}
mutationFn: logoutUser,
onSuccess: () => clearSession(keepQueryClient)
});
};

Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/auth/LoginPage/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const LoginPage = () => {
console.log("Error - Not logged in yet");
}
};

if (isLoggedIn()) {
handleRedirects();
}
Expand Down
30 changes: 17 additions & 13 deletions frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,20 +250,24 @@ export const SignupInvitePage = () => {
setStep(2);
} else {
const redirectExistingUser = async () => {
const { token: mfaToken, isMfaEnabled } = await selectOrganization({
organizationId
});

if (isMfaEnabled) {
SecurityClient.setMfaToken(mfaToken);
toggleShowMfa.on();
setMfaSuccessCallback(() => redirectExistingUser);
return;
try {
const { token: mfaToken, isMfaEnabled } = await selectOrganization({
organizationId
});

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();
Expand Down
30 changes: 25 additions & 5 deletions frontend/src/pages/middlewares/authenticate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -27,14 +30,31 @@ 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" });
}

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 };
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/organization/NoOrgPage/NoOrgPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const NoOrgPage = () => {
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="min-h-screen bg-bunker-800">
<CreateOrgModal
isOpen={popUp.createOrg.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
Expand Down
12 changes: 5 additions & 7 deletions frontend/src/pages/organization/NoOrgPage/route.tsx
Original file line number Diff line number Diff line change
@@ -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,
})
12 changes: 7 additions & 5 deletions frontend/src/pages/organization/layout.tsx
Original file line number Diff line number Diff line change
@@ -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,
})
Loading
Loading