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

[QF-625] introduced proxy to backend with redirect to auth #2191

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
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 @@ -81,6 +81,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>({
osamasayed marked this conversation as resolved.
Show resolved Hide resolved
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 (
osamasayed marked this conversation as resolved.
Show resolved Hide resolved
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 => {
osamasayed marked this conversation as resolved.
Show resolved Hide resolved
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 => {
osamasayed marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -5862,6 +5862,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 @@ -9391,6 +9398,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 @@ -9804,16 +9816,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 @@ -10495,6 +10507,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 @@ -10930,6 +10971,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