Skip to content

Commit

Permalink
[QF-625] introduced proxy to backend with redirect to auth (#2191)
Browse files Browse the repository at this point in the history
  • Loading branch information
mohsinayoob authored and osamasayed committed Nov 18, 2024
1 parent 7609f1e commit e0651f6
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 8 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ NEXT_PUBLIC_NOVU_APP_ID=

NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SERVER_SENTRY_ENABLED=false
NEXT_PUBLIC_CLIENT_SENTRY_ENABLED=true
NEXT_PUBLIC_CLIENT_SENTRY_ENABLED=true
NODE_TLS_REJECT_UNAUTHORIZED=0 #set this only when SSL is self signed
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"firebase": "^9.10.0",
"fuse.js": "^6.6.2",
"groq": "^3.4.0",
"http-proxy-middleware": "^3.0.0",
"humps": "^2.0.1",
"js-cookie": "^3.0.1",
"lodash": "^4.17.21",
Expand Down
58 changes: 58 additions & 0 deletions src/pages/api/proxy/[...path].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { EventEmitter } from 'events';

import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware';
import { NextApiRequest, NextApiResponse } from 'next';

// Define error messages in a constant object
const ERROR_MESSAGES = {
PROXY_ERROR: 'Proxy error',
PROXY_HANDLER_ERROR: 'Proxy handler error',
};

// This line increases the default maximum number of event listeners for the EventEmitter to a better number like 20.
// It is necessary to prevent memory leak warnings when multiple listeners are added,
// which can occur in a proxy setup like this where multiple requests are handled concurrently.
EventEmitter.defaultMaxListeners = Number(process.env.PROXY_DEFAULT_MAX_LISTENERS) || 100;

// This file sets up a proxy middleware for API requests. It is needed to forward requests from the frontend
// to the backend server, allowing for features like cookie handling and request body fixing, which are essential
// for maintaining session state and ensuring correct request formatting while in a cross domain env.
const apiProxy = createProxyMiddleware<NextApiRequest, NextApiResponse>({
target: process.env.NEXT_PUBLIC_AUTH_BASE_URL,
changeOrigin: true,
pathRewrite: { '^/api/proxy': '' }, // eslint-disable-line @typescript-eslint/naming-convention
secure: process.env.NEXT_PUBLIC_VERCEL_ENV === 'production', // Disable SSL verification to avoid UNABLE_TO_VERIFY_LEAF_SIGNATURE error for dev
logger: console,

on: {
proxyReq: (proxyReq, req) => {
// Attach cookies from the request to the proxy request
if (req.headers.cookie) {
proxyReq.setHeader('Cookie', req.headers.cookie);
}

// Fix the request body if bodyParser is involved
fixRequestBody(proxyReq, req);
},

proxyRes: (proxyRes, req, res) => {
// Set cookies from the proxy response to the original response
const proxyCookies = proxyRes.headers['set-cookie'];
if (proxyCookies) {
res.setHeader('Set-Cookie', proxyCookies);
}
},

error: (err, req, res) => {
res.end(() => ({ error: ERROR_MESSAGES.PROXY_ERROR, message: err.message }));
},
},
});

export default function handler(req: NextApiRequest, res: NextApiResponse) {
apiProxy(req, res, (err) => {
if (err) {
res.status(500).json({ error: ERROR_MESSAGES.PROXY_HANDLER_ERROR, message: err.message });
}
});
}
145 changes: 145 additions & 0 deletions src/pages/auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { useEffect } from 'react';

import { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
import { useRouter } from 'next/router';
import useTranslation from 'next-translate/useTranslation';

import { ToastStatus, useToast } from '@/dls/Toast/Toast';
import AuthError from '@/types/AuthError';
import { makeRedirectTokenUrl } from '@/utils/auth/apiPaths';

interface AuthProps {
error?: string;
}

const Auth: React.FC<AuthProps> = ({ error }) => {
const router = useRouter();
const toast = useToast();
const { t } = useTranslation('login');

useEffect(() => {
if (error) {
const errorMessage = t(`login-error.${error}`);
toast(errorMessage, {
status: ToastStatus.Error,
});
router.replace('/');
}
}, [error, toast, t, router]);

return null;
};

/**
* Handles the redirection process based on the provided token.
* It fetches the token from the server, sets the necessary cookies,
* and redirects the user to the specified URL.
*
* @param {GetServerSidePropsContext} context - The context object containing request and response information.
* @param {string} token - The token used for authentication and redirection.
* @param {string} redirectUrl - The URL to redirect the user to after successful token handling.
* @returns {Promise<GetServerSidePropsResult<any>>} - A promise that resolves to the server-side props result,
* which includes either a redirection or an error message.
*/
const handleTokenRedirection = async (
context: GetServerSidePropsContext,
token: string,
redirectUrl: string,
): Promise<GetServerSidePropsResult<any>> => {
try {
const baseUrl = getBaseUrl(context);
const response = await fetchToken(baseUrl, token, context);

if (!response.ok) {
throw new Error('Network response was not ok');
}

setProxyCookies(response, context);

return {
props: {},
redirect: {
destination: redirectUrl,
permanent: false,
},
};
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error during token redirection:', error);
return {
props: {
error: AuthError.AuthenticationError,
},
};
}
};

/**
* Constructs the base URL from the request headers in the given context.
*
* @param {GetServerSidePropsContext} context - The context object containing request and response information.
* @returns {string} - The constructed base URL using the protocol and host from the request headers.
*/
const getBaseUrl = (context: GetServerSidePropsContext): string => {
return `${context.req.headers['x-forwarded-proto'] || 'https'}://${context.req.headers.host}`;
};

/**
* Fetches a token from the server using the provided base URL and token.
*
* @param {string} baseUrl - The base URL to use for the request.
* @param {string} token - The token to be included in the request URL.
* @param {GetServerSidePropsContext} context - The context object containing request and response information.
* @returns {Promise<Response>} - A promise that resolves to the response from the fetch request.
*/
const fetchToken = async (
baseUrl: string,
token: string,
context: GetServerSidePropsContext,
): Promise<Response> => {
return fetch(`${baseUrl}${makeRedirectTokenUrl(token)}`, {
method: 'GET',
headers: {
cookie: context.req.headers.cookie || '',
},
credentials: 'include',
});
};

/**
* Sets cookies from the proxy response to the server-side response.
*
* This function extracts the 'set-cookie' header from the proxy response,
* splits it into individual cookies, and sets them in the server-side response
* headers. This is necessary to ensure that cookies set by the proxy are
* correctly forwarded to the client.
*
* @param {Response} response - The response object from the proxy request.
* @param {GetServerSidePropsContext} context - The context object containing request and response information.
*/
const setProxyCookies = (response: Response, context: GetServerSidePropsContext): void => {
const proxyCookies = response.headers.get('set-cookie');
if (proxyCookies) {
const cookiesArray = proxyCookies.split(/,(?=\s*\w+=)/).map((cookie) => cookie.trim());
context.res.setHeader('Set-Cookie', cookiesArray);
}
};

export const getServerSideProps: GetServerSideProps = async (context) => {
const { r, token } = context.query;
const redirectUrl = (r || '/') as string;

if (token) {
return handleTokenRedirection(context, token as string, redirectUrl);
}

return {
props: {},
redirect: {
destination: redirectUrl,
permanent: false,
},
};
};

export default Auth;
2 changes: 2 additions & 0 deletions src/utils/auth/apiPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ export const makeLogoutUrl = () => makeUrl('auth/logout');

export const makeRefreshTokenUrl = () => makeUrl('tokens/refreshToken');

export const makeRedirectTokenUrl = (token: string) => makeUrl('tokens/redirectToken', { token });

export const makeGenerateMediaFileUrl = () => makeUrl('media/generate');

export const makeGetMediaFileProgressUrl = (renderId: string) =>
Expand Down
3 changes: 1 addition & 2 deletions src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,4 @@ export const getBasePath = (): string =>
* @param {string} path
* @returns {string}
*/
export const getAuthApiPath = (path: string): string =>
`${process.env.NEXT_PUBLIC_AUTH_BASE_URL}/${path}`;
export const getAuthApiPath = (path: string): string => `/api/proxy/${path}`;
56 changes: 51 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6267,6 +6267,13 @@
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==

"@types/http-proxy@^1.17.10":
version "1.17.15"
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36"
integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==
dependencies:
"@types/node" "*"

"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7"
Expand Down Expand Up @@ -9855,6 +9862,11 @@ event-target-shim@^5.0.0:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==

eventemitter3@^4.0.0:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==

eventemitter3@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4"
Expand Down Expand Up @@ -10268,16 +10280,16 @@ flow-parser@0.*:
resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.230.0.tgz#f0e54bdac58a20553bb81ef26bdc8a616360f1cd"
integrity sha512-ZAfKaarESYYcP/RoLdM91vX0u/1RR7jI5TJaFLnxwRlC2mp0o+Rw7ipIY7J6qpIpQYtAobWb/J6S0XPeu0gO8g==

follow-redirects@^1.0.0, follow-redirects@^1.15.6:
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==

follow-redirects@^1.15.4:
version "1.15.5"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020"
integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==

follow-redirects@^1.15.6:
version "1.15.6"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==

for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
Expand Down Expand Up @@ -10959,6 +10971,35 @@ http-proxy-agent@^5.0.0:
agent-base "6"
debug "4"

http-proxy-middleware@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-3.0.0.tgz#550790357d6f92a9b82ab2d63e07343a791cf26b"
integrity sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==
dependencies:
"@types/http-proxy" "^1.17.10"
debug "^4.3.4"
http-proxy "^1.18.1"
is-glob "^4.0.1"
is-plain-obj "^3.0.0"
micromatch "^4.0.5"

http-proxy@^1.18.1:
version "1.18.1"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
dependencies:
eventemitter3 "^4.0.0"
follow-redirects "^1.0.0"
requires-port "^1.0.0"

http2-wrapper@^1.0.0-beta.5.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d"
integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==
dependencies:
quick-lru "^5.1.1"
resolve-alpn "^1.0.0"

https-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
Expand Down Expand Up @@ -11417,6 +11458,11 @@ is-plain-obj@^1.1.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==

is-plain-obj@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==

is-plain-obj@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
Expand Down

0 comments on commit e0651f6

Please sign in to comment.