Feedback for an architecture with three applications

Hi,

I would like to receive feedback from the community or from the engineers behind this great tool that is Auth0.

I present you my scenario and the steps I have followed to build a system that consists of three applications. It is the following:

The system consists of three applications:

A landing page made with NextJS that is rendered mainly on the server side. (from now on, Landing).

A Frontend application also made with NextJS that is rendered mainly on the client side (from now on, Frontend)

A Backend application that acts as a Node server with Express. (from now on, Backend)

The authentication system I was interested in having was as follows:

To be able to authenticate myself from the Frontend to access the pages and content of that application in addition to being able to make HTTP requests to my Backend in an authorized way. Additionally, to be able to have a record of users both in Auth0’s own database as well as in my own database with some additional information.

How did I build it? In the following way:

In the Frontend application I have integrated the NextJS SDK using the following code:

import { Auth0Client } from '@auth0/nextjs-auth0/server';

/**
 * Auth0 client
 */
export const auth0 = new Auth0Client({
    domain: process.env.AUTH0_DOMAIN,
    clientId: process.env.AUTH0_CLIENT_ID,
    clientSecret: process.env.AUTH0_CLIENT_SECRET,
    appBaseUrl: process.env.FRONTEND_BASE_URL,
    secret: process.env.AUTH0_SECRET,
    session: {
        cookie: {
            name: 'my_session',
        },
    },
    authorizationParameters: {
        audience: process.env.AUTH0_AUDIENCE,
        scope: 'openid profile email',
    },
});

The values of the environment variables, I have extracted them by creating a Regular Web application.

With this, I can authenticate the users in Frontend and, additionally, through the getSession method in RootLayout, check if they are authenticated. If they are, perfect, if not, they are expelled.

export default async function RootLayout({ children }: Readonly<RootLayoutProps>) {
    const session = await auth0.getSession();

    if (!session) {
        redirect(process.env.LANDING_BASE_URL!);
    }
...

When from Frontend I want to make a request to the Backend API, I add the access_token in the headers of the request:

import { getAccessToken } from '@auth0/nextjs-auth0';

const currentUserApiRepository = (): CurrentUserRepository => ({
    getCurrentUser: async (): Promise<User> => {
        const token = await getAccessToken();

        const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_BASE_URL}/current-user`, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                Authorization: `Bearer ${token}`,
            },
        });

        return response.json();
    },
});

On the Backend side, in the main.ts file, I initialize the express-oauth2-jwt-bearer library provided by the system to check that the authorization tokens are correct:

import { auth } from 'express-oauth2-jwt-bearer';

const authClient = auth({
    issuerBaseURL: process.env.AUTH0_ISSUER,
    audience: process.env.AUTH0_AUDIENCE,
    tokenSigningAlg: 'RS256',
});
...
app.use(authClient);

With this, what I get is to approve or deny the requests sent to me by Frontend.

Additionally, in Frontend I have a component that makes a request to the Backend to the path /current-user.

This endpoint, when processed by Backend through a controller, does the following:

const getCurrentUserController: RequestHandler = async (req, res) => {
    try {
        // Get "sub" property from request authorization headers
        const auth0Id = getCurrentUserIdUseCase(req);
        // Search in my own DB for a user with this "sub". If doesn't exists, create it
        let userFromDatabase = await getUserByAuth0UseCase(usersTypeOrmDatabaseRepository, auth0Id);

        if (!userFromDatabase) {
            userFromDatabase = await createUserUseCase(usersTypeOrmDatabaseRepository, auth0Id);
        }
        // Get more user information through the “sub” using ManagementClient
        const idpUser = await getUserByIdUseCase(usersAuth0IdpRepository, auth0Id);

        const authenticatedUser: User = {
            id: userFromDatabase.id,
            name: idpUser.name,
        };

        res.json(authenticatedUser);
    } catch (error) {
        if (error instanceof Error) {
            res.status(StatusCodes.UNAUTHORIZED).json({ message: error.message });
        } else {
            res.status(StatusCodes.UNAUTHORIZED).json({ message: 'Unknown error' });
        }
    }
};

With this, in Frontend we receive a user that is built from the data obtained by the ManagementClient API as well as data that comes from my own database.

The last aspect that I wanted to highlight is that in the Landing application, I made an exact same installation of the Auth0 SDK to know if the user is authenticated, and since the configuration parameters of that SDK are the same as in Frontend, I can know if there is session or not, to show one content or another.

What do you think about my approach? Is it coherent, does it have security flaws? Is it susceptible to be improved?

Thank you very much in advance.

Hi @mcfdez, and welcome to the Auth0 Community!

Your architecture and implementation look great and are following good practices. :clap:

Here are a couple of things to consider if you haven’t already:

  1. In your try-catch on the backend, you return a 401 for any errors that are caught. However there are scenarios where more appropriate status codes exist. For example, if your try block fails because of a database connection, a 500 would be a better status code to return. Returning appropriate responses will help you debug should a problem occur.
  2. Where you call the idp endpoint to obtain more information:
  // Get more user information through the “sub” using ManagementClient
        const idpUser = await getUserByIdUseCase(usersAuth0IdpRepository, auth0Id);

        const authenticatedUser: User = {
            id: userFromDatabase.id,
            name: idpUser.name,
        };

If this call fails, you will end up with a created user that doesn’t have the name or other information you may have wanted to add to your database record. If this is a problem for your use case, you should consider moving the request to obtain the additional information from the idp before creating the user in your database (and abort the operation if necessary).

1 Like

Hi,

Thank you very much for your feedback, it is very helpful!

I will implement your recommendations which will give an additional layer of robustness to the code.

Thank you!