Skip to content
This repository has been archived by the owner on Sep 17, 2024. It is now read-only.

Commit

Permalink
feat(Auth): handle auth errors. Only the one available in params url.
Browse files Browse the repository at this point in the history
Signed-off-by: reslene <reslene@numary.com>
  • Loading branch information
reslene committed May 2, 2023
1 parent 97d24b5 commit 4e85cc0
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 74 deletions.
170 changes: 113 additions & 57 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';

Expand All @@ -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,
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -102,74 +112,101 @@ const renderError = (
navigate: NavigateFunction,
t: any,
message?: string,
description?: string
): ReactElement => (
<Backdrop
sx={{
zIndex: (theme) => theme.zIndex.drawer + 1,
}}
open={true}
>
<Box
display="flex"
justifyContent="space-evenly"
description?: string,
type?: Errors
): ReactElement => {
const isAuthError = (type && type == Errors.AUTH) || false;

return (
<Backdrop
sx={{
width: '100%',
height: '100%',
background: ({ palette }) => palette.neutral[0],
zIndex: (theme) => theme.zIndex.drawer + 1,
}}
open={true}
>
<Box
display="flex"
justifyContent="space-evenly"
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
alignSelf: 'center',
width: '100%',
height: '100%',
background: ({ palette }) => palette.neutral[0],
}}
>
<Typography variant="large2x" mb={0}>
{t('common.boundaries.title')}
</Typography>
<Typography variant="h2" mt={1}>
{message || t('common.boundaries.errorState.error.title')}
</Typography>
<Box
sx={{
mt: 4,
p: 4,
borderRadius: 2,
color: ({ palette }) => palette.neutral[0],
backgroundColor: ({ palette }) => palette.neutral[900],
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
alignSelf: 'center',
background: ({ palette }) => palette.neutral[0],
}}
>
<Typography variant="body2">
{description || t('common.boundaries.errorState.error.description')}
<Typography variant="large2x" mb={0}>
{t('common.boundaries.title', {
type: isAuthError
? t(`common.boundaries.${Errors.AUTH}`)
: undefined,
})}
</Typography>
</Box>
<Box sx={{ display: 'flex' }}>
<LoadingButton
id="go-back-home"
content="Back to main page"
variant="stroke"
onClick={() => navigate(getRoute(OVERVIEW_ROUTE))}
sx={{ mt: 5, mr: 1 }}
/>
<LoadingButton
id="logout"
content={t('topbar.logout')}
variant="stroke"
startIcon={<Logout />}
onClick={() => {
window.location.href = `${window.origin}/auth/redirect-logout`;
<Typography variant="h2" mt={1}>
{message || t('common.boundaries.errorState.error.title')}
</Typography>
<Box
sx={{
mt: 4,
p: 4,
borderRadius: 2,
color: ({ palette }) => palette.neutral[0],
backgroundColor: ({ palette }) => palette.neutral[900],
}}
sx={{ mt: 5 }}
/>
>
<Typography variant="body2">
{description ||
t('common.boundaries.errorState.error.description')}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
{!isAuthError && (
<LoadingButton
id="go-back-home"
content={t('common.boundaries.buttons.overview')}
variant="stroke"
onClick={() => navigate(getRoute(OVERVIEW_ROUTE))}
sx={{ mt: 5 }}
/>
)}
{!isAuthError && (
<LoadingButton
id="logout"
content={t('topbar.logout')}
variant="stroke"
startIcon={<Logout />}
onClick={() => {
window.location.href = `${window.origin}/auth/redirect-logout`;
}}
sx={{ mt: 5 }}
/>
)}
<LoadingButton
id="get-help"
content={t('common.boundaries.buttons.getHelp')}
variant="stroke"
startIcon={<Forum />}
onClick={() => {
window.open(
'https://formance-community.slack.com/ssb/redirect',
'_blank'
);
}}
sx={{ mt: 5 }}
/>
</Box>
</Box>
</Box>
</Box>
</Backdrop>
);
</Backdrop>
);
};

const Document = withEmotionCache(
({ children, title }: DocumentProps, emotionCache) => {
Expand Down Expand Up @@ -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 (
<Document title="Error!">
<Layout>{renderError(navigate, t)}</Layout>
<Layout>
{renderError(
navigate,
t,
errorParsed.error,
errorParsed.description,
errorParsed.error_type
)}
</Layout>
</Document>
);
}
Expand All @@ -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]));
Expand Down
14 changes: 8 additions & 6 deletions app/routes/auth/login.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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')}`
);
};
18 changes: 11 additions & 7 deletions app/routes/auth/redirect-logout.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,14 +14,17 @@ import {
export const loader: LoaderFunction = async ({ request }): Promise<any> => {
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`);
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/Layout/components/Topbar/Topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const Topbar: FunctionComponent<TopbarProps> = ({ resized, onResize }) => {
});
}
} catch (e) {
console.info('Current user could not be retrieved');
console.info('Current user could not be retrieved yet');
}
};

Expand Down
7 changes: 6 additions & 1 deletion app/src/translations/en/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,11 @@ export default {
last: 'Last {{value}} {{kind}}',
},
boundaries: {
auth: 'authentication',
buttons: {
overview: 'Go back home',
getHelp: 'Get help',
},
errorState: {
error: {
title:
Expand Down Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions app/src/types/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ export enum Errors {
FORBIDDEN = 'forbidden',
SERVICE_DOWN = 'service_down',
ERROR = 'error',
AUTH = 'auth',
OTHER = 'other',
}
4 changes: 2 additions & 2 deletions app/src/utils/auth.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ export const getOpenIdConfig = async (): Promise<OpenIdConfiguration> => {
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 = (
Expand Down

0 comments on commit 4e85cc0

Please sign in to comment.