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.