From 4e85cc0e73e74eca3a1c8a5cce40652a947f7c2a Mon Sep 17 00:00:00 2001 From: reslene Date: Fri, 28 Apr 2023 15:35:58 +0200 Subject: [PATCH] feat(Auth): handle auth errors. Only the one available in params url. Signed-off-by: reslene --- app/root.tsx | 170 ++++++++++++------ app/routes/auth/login.ts | 14 +- app/routes/auth/redirect-logout.ts | 18 +- .../Layout/components/Topbar/Topbar.tsx | 2 +- app/src/translations/en/translations.ts | 7 +- app/src/types/generic.ts | 2 + app/src/utils/auth.server.ts | 4 +- 7 files changed, 143 insertions(+), 74 deletions(-) diff --git a/app/root.tsx b/app/root.tsx index 0b1bd461..377cadde 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { ReactElement, useEffect, useState } from 'react'; import { withEmotionCache } from '@emotion/react'; -import { Logout } from '@mui/icons-material'; +import { Forum, Logout } from '@mui/icons-material'; import { Backdrop, Box, @@ -23,7 +23,7 @@ import { useLoaderData, } from '@remix-run/react'; import { LinksFunction, LoaderFunction } from '@remix-run/server-runtime'; -import { camelCase, get } from 'lodash'; +import { camelCase, get, noop } from 'lodash'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -36,6 +36,7 @@ import Layout from '~/src/components/Layout'; import { getRoute, OVERVIEW_ROUTE } from '~/src/components/Layout/routes'; import ClientStyleContext from '~/src/contexts/clientStyleContext'; import { ServiceContext } from '~/src/contexts/service'; +import { Errors } from '~/src/types/generic'; import { Authentication, CurrentUser, @@ -71,8 +72,17 @@ export const loader: LoaderFunction = async ({ request }) => { const buff = new Buffer(JSON.stringify(stateObject)); const stateAsBase64 = buff.toString('base64'); const openIdConfig = await getOpenIdConfig(); - - if (!cookie) { + const error = url.searchParams.get('error'); + if (error) { + throw Error( + JSON.stringify({ + error, + description: url.searchParams.get('error_description'), + error_type: Errors.AUTH, + }) + ); + } + if (!cookie && !error) { // TODO add method on auth.server with URL params to be more elegant return redirect( `${openIdConfig.authorization_endpoint}?client_id=${process.env.CLIENT_ID}&redirect_uri=${REDIRECT_URI}${AUTH_CALLBACK_ROUTE}&state=${stateAsBase64}&response_type=code&scope=openid email offline_access` @@ -102,74 +112,101 @@ const renderError = ( navigate: NavigateFunction, t: any, message?: string, - description?: string -): ReactElement => ( - theme.zIndex.drawer + 1, - }} - open={true} - > - { + const isAuthError = (type && type == Errors.AUTH) || false; + + return ( + palette.neutral[0], + zIndex: (theme) => theme.zIndex.drawer + 1, }} + open={true} > palette.neutral[0], }} > - - {t('common.boundaries.title')} - - - {message || t('common.boundaries.errorState.error.title')} - palette.neutral[0], - backgroundColor: ({ palette }) => palette.neutral[900], + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + alignSelf: 'center', + background: ({ palette }) => palette.neutral[0], }} > - - {description || t('common.boundaries.errorState.error.description')} + + {t('common.boundaries.title', { + type: isAuthError + ? t(`common.boundaries.${Errors.AUTH}`) + : undefined, + })} - - - navigate(getRoute(OVERVIEW_ROUTE))} - sx={{ mt: 5, mr: 1 }} - /> - } - onClick={() => { - window.location.href = `${window.origin}/auth/redirect-logout`; + + {message || t('common.boundaries.errorState.error.title')} + + palette.neutral[0], + backgroundColor: ({ palette }) => palette.neutral[900], }} - sx={{ mt: 5 }} - /> + > + + {description || + t('common.boundaries.errorState.error.description')} + + + + {!isAuthError && ( + navigate(getRoute(OVERVIEW_ROUTE))} + sx={{ mt: 5 }} + /> + )} + {!isAuthError && ( + } + onClick={() => { + window.location.href = `${window.origin}/auth/redirect-logout`; + }} + sx={{ mt: 5 }} + /> + )} + } + onClick={() => { + window.open( + 'https://formance-community.slack.com/ssb/redirect', + '_blank' + ); + }} + sx={{ mt: 5 }} + /> + - - -); + + ); +}; const Document = withEmotionCache( ({ children, title }: DocumentProps, emotionCache) => { @@ -319,10 +356,28 @@ export function ErrorBoundary({ error }: { error: Error }) { const navigate = useNavigate(); const { t } = useTranslation(); logger(error, 'app/root', undefined); + let errorParsed = { + error: undefined, + description: undefined, + error_type: undefined, + }; + try { + errorParsed = JSON.parse(error.message); + } catch { + noop(); + } return ( - {renderError(navigate, t)} + + {renderError( + navigate, + t, + errorParsed.error, + errorParsed.description, + errorParsed.error_type + )} + ); } @@ -331,6 +386,7 @@ export function CatchBoundary() { const caught = useCatch(); const navigate = useNavigate(); const { t } = useTranslation(); + console.log('cccc', caught); logger(caught, 'app/root/CatchBoundary'); const error = camelCase(get(errorsMap, caught.status, errorsMap[422])); diff --git a/app/routes/auth/login.ts b/app/routes/auth/login.ts index 5f367f8b..da26ea15 100644 --- a/app/routes/auth/login.ts +++ b/app/routes/auth/login.ts @@ -1,6 +1,7 @@ -import { json, redirect } from '@remix-run/node'; +import { redirect } from '@remix-run/node'; import { LoaderFunction, TypedResponse } from '@remix-run/server-runtime'; +import { Errors } from '~/src/types/generic'; import { commitSession, COOKIE_NAME, @@ -34,10 +35,11 @@ export const loader: LoaderFunction = async ({ }); } - return json( - {}, - { - status: 401, - } + return redirect( + `${process.env.REDIRECT_URI}?error_type=${ + Errors.AUTH + }&error=${url.searchParams.get( + 'error' + )}&error_description=${url.searchParams.get('error_description')}` ); }; diff --git a/app/routes/auth/redirect-logout.ts b/app/routes/auth/redirect-logout.ts index 65672b26..fb747a4e 100644 --- a/app/routes/auth/redirect-logout.ts +++ b/app/routes/auth/redirect-logout.ts @@ -1,6 +1,7 @@ import { redirect } from '@remix-run/node'; import { LoaderFunction } from '@remix-run/server-runtime'; +import { Errors } from '~/src/types/generic'; import { Authentication } from '~/src/utils/api'; import { getOpenIdConfig, @@ -13,14 +14,17 @@ import { export const loader: LoaderFunction = async ({ request }): Promise => { const session = await getSession(request.headers.get('Cookie')); const sessionHolder: Authentication = parseSessionHolder(session); - const openIdConfig = await getOpenIdConfig(); + try { + const openIdConfig = await getOpenIdConfig(); + const intro = await introspect(openIdConfig, sessionHolder.access_token); - const intro = await introspect(openIdConfig, sessionHolder.access_token); - - if (intro && intro.active) { - return redirect( - `${openIdConfig.end_session_endpoint}?id_token_hint=${sessionHolder.access_token}&client_id=${process.env.CLIENT_ID}&post_logout_redirect_uri=${REDIRECT_URI}/auth/logout` - ); + if (intro && intro.active) { + return redirect( + `${openIdConfig.end_session_endpoint}?id_token_hint=${sessionHolder.access_token}&client_id=${process.env.CLIENT_ID}&post_logout_redirect_uri=${REDIRECT_URI}/auth/logout` + ); + } + } catch { + return redirect(`${process.env.REDIRECT_URI}?error_type=${Errors.AUTH}`); } return redirect(`${REDIRECT_URI}/auth/logout`); diff --git a/app/src/components/Layout/components/Topbar/Topbar.tsx b/app/src/components/Layout/components/Topbar/Topbar.tsx index 00c60d11..a96bb709 100644 --- a/app/src/components/Layout/components/Topbar/Topbar.tsx +++ b/app/src/components/Layout/components/Topbar/Topbar.tsx @@ -89,7 +89,7 @@ const Topbar: FunctionComponent = ({ resized, onResize }) => { }); } } catch (e) { - console.info('Current user could not be retrieved'); + console.info('Current user could not be retrieved yet'); } }; diff --git a/app/src/translations/en/translations.ts b/app/src/translations/en/translations.ts index 80520bd1..4a509331 100644 --- a/app/src/translations/en/translations.ts +++ b/app/src/translations/en/translations.ts @@ -633,6 +633,11 @@ export default { last: 'Last {{value}} {{kind}}', }, boundaries: { + auth: 'authentication', + buttons: { + overview: 'Go back home', + getHelp: 'Get help', + }, errorState: { error: { title: @@ -662,7 +667,7 @@ export default { button: 'Go back home', }, }, - title: "That's an error", + title: "That's an {{type}} error", }, soon: 'Soon!', title: 'Formance', diff --git a/app/src/types/generic.ts b/app/src/types/generic.ts index 97dccf4a..5ef01249 100644 --- a/app/src/types/generic.ts +++ b/app/src/types/generic.ts @@ -21,4 +21,6 @@ export enum Errors { FORBIDDEN = 'forbidden', SERVICE_DOWN = 'service_down', ERROR = 'error', + AUTH = 'auth', + OTHER = 'other', } diff --git a/app/src/utils/auth.server.ts b/app/src/utils/auth.server.ts index 38720f36..b98e5ea4 100644 --- a/app/src/utils/auth.server.ts +++ b/app/src/utils/auth.server.ts @@ -83,10 +83,10 @@ export const getOpenIdConfig = async (): Promise => { const uri = `${process.env.API_URL}/api/${API_AUTH}/.well-known/openid-configuration`; return fetch(uri) + .then(async (response) => response.json()) .catch(() => { throw new Error('Error while fetching openid config'); - }) - .then(async (response) => response.json()); + }); }; export const getJwtPayload = (