Part 5: Making a user admin dashboard with Gatsby Functions and Auth0

klekanger

Kurt Lekanger

Posted on September 12, 2021

Part 5: Making a user admin dashboard with Gatsby Functions and Auth0

In a series of articles, I have shown how I created a new website for the condominium association where I live using Gatsby and with Auth0 user authentication. Read part 1 here: How I built our condos's new web pages with Gatsby and Chakra UI

When the new website was launched, all user administration was done via a technical and complicated user interface at Auth0. For the condominium's website to be a full-fledged solution that can be handed over to non-technical users, a more user-friendly dashboard was needed. It should be possible for non-technical users to create, update or delete users and do all the admin tasks without contacting me.

This is how I built the user admin solution:

You can find the source code for the site at https://github.com/klekanger/gartnerihagen, but in this article I want to go through how I have structured everything - without going into all the details (that would make a book!).

Securing everything

Everything on the client (i.e. in the browser) can be manipulated. Building a user administration dashboard requires a high level of security, and authenticating users and verifying that the user has permission to create, delete or update other users should therefore be done on a server - not on the client.

This is how my solution works:

  • The user logs in to the client and receives an access token from Auth0
  • When the user visits the user admin dashboard, the access token is sent to a serverless function at Netlify which 1) checks that it is a valid access token, 2) contacts Auth0 and checks that the access token belongs to a user with the necessary permissions to do whatever she or he tries to do
  • If the user has all required permissions, the serverless function contacts Auth0's Management API which for example returns a list of all users.

To access the user admin dashboard on the web page, the user must have the role "admin". I use Auth0's role-based access control (RBAC) to define three different roles: "user", "editor" and "admin". Depending on the role, the logged in user will see buttons for user administration or content editing:

Buttons for user admin and content editing will appear on the user's
Buttons for user admin and content editing will appear on the user's "My page" if the user has the required roles.

This is a simplified diagram showing how this works:
Diagram showing how the frontend requests an access token from Auth0. The access token is then passed on to the user admin API made with Gatsby Functions, and verified.

Gatsby Functions makes it easy to create APIs

When I began creating the user admin dashboard, I started creating the APIs to retrieve, update or create users using Netlify Functions. But then Gatsby announced Gatsby Functions, so I decided to convert my Netlify functions into Gatsby Functions (which was quite easy, they are not that different). With built-in support for serverless functions in Gatsby, my job became even easier. This is something Next.js has had for a long time, so it was about time, Gatsby!

Creating a Gatsby Function is as simple as creating a JavaScript or TypeScript file in the src/api folder and exporting a handler function that takes two parameters - req (request) and res (response). For those who have used the Node framework Express, Gatsby Functions is pretty similar.

The Hello World example in Gatsby's official documentation illustrates how easy it is to make a serverless function API with Gatsby Functions:

// src/api/hello-world.js

export default function handler(req, res) {
  res.status(200).json({ hello: `world` })
}
Enter fullscreen mode Exit fullscreen mode

If you make a request to the URL /api/hello-world the serverless function will return { hello: 'world' } and the HTTP status code 200 (which means everything is OK).

Four APIs

I decided that I needed four API-s to create my user admin dashboard. Each API is one servierless function:

src
├── api
│   └── admin-users
│       ├── create-user.ts
│       ├── delete-user.ts
│       ├── get-users-in-role.ts
        └── update-user.ts
Enter fullscreen mode Exit fullscreen mode

When the user visits the user admin web page via "My page", we call the API admin-users/get-users-in-role. If the user have the required permissions the API returns a list over every user, including the role of each user. Each user is displayed as a "user card" in the user admin dashboard, with buttons for changing the user, deleting a user, or changing the user's password:

A search field and a dropdown menu let's you filter out the users you want to see.
A search field and a dropdown menu let's you filter out the users you want to see.

Auth0 configuration

Before I could create my own backend APIs for user administration with Gatsby Functions, I had to configure some things in Auth0.

First I had to create a new so-called machine-to-machine application at Auth0. These are applications that will not communicate with clients, but with another server you trust (like the serverless functions I will create for user administration).

When I log in to manage.auth0.com and go to Applications, I have these two applications:

Screenshot showing two applications on Auth0: Backend and Boligsameiet Gartnerihagen.

The one named Boligsameiet Gartnerihagen takes care of authentication for users who are logged in to the website. The one called Backend is the machine-to-machine application to be used by our serverless Gatsby function running on Netlify's servers.

To set up role-based access control (RBAC), we must create a new API at Auth0 where we define all the permissions (scopes) we want to be able to give users based on which roles the user has. These are the permissions the Auth0 Management API requires to be able to perform various operations, and which we can later choose from when we create the various roles for the users (in our case admin, user or editor).

I called my API Useradmin, and entered the various permissions I would need to update users and roles. Auth0 has a more detailed description of how this works.

Screenshot showing the Useradmin API and all the permissions we have set up.

Then I gave the machine-to-machine application Backend access to both the Auth0 Management API and the new Useradmin API that I just created:

Screenshot of the Auth0 backend application, giving permissions to access the necessary APIs.

However, this is not enough. You also have to click the small down arrow on the right hand side of each API, and give the Backend application the necessary permissions to the APIs. Jeg checked all the checkboxes with the permissions I created for the Useradmin API.

Screenshot, setting up permissions.

Then I had to configure the different user roles by selecting User Management from Auth0s main menu and then choose Roles. I created three roles: admin, editor and user. Then, for each role, I chose Add permissions and selected which API I wanted to add permissions from (in my case, the Useradmin API).

Setting up user roles.

I gave the admin user all permissions defined in the Useradmin API. The roles user and editor don't need any permissions, as they should not be able to do anything "dangerous". I only check on the client if the user is a member of these roles to decide whether I should show buttons for editing content on the web site or not. Only users with an admin role will be allowed by my Gatsby Function to contact the Auth0 Management API (which also double-checks that the user that connects to it has the right permissions).

Defining the permissions for the admin user.

To avoid unnecessary API calls and simplify the code on the client side, I also wanted to make it possible to see what roles a user has when the user logs in. This is to be able to display roles on My Page, and for displaying buttons for user administration and content editing only when the user have the right roles. By default, the access token will only contain all the permissions the user has received (through its role). However, the name of the role will not be in the metadata of the access token. We have to fix that.

Auth0 has something called Flows and Actions that makes it possible to perform various operations when, for example, a user logs in. I selected the "flow" called Login, and then chose to add an "action" that runs right after the user logs in, but before the access token is sent.

When you create a new action, you will get an editor where you can enter your code. I entered the following code snippet, which adds all the roles of the user to the accesstoken before it is sent to the client:

/**
 * @param {Event} event - Details about the user and the context in which they are logging in.
 * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
 */
exports.onExecutePostLogin = async (event, api) => {
  const namespace = 'https:/gartnerihagen-askim.no';
  if (event.authorization) {
    api.idToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
    api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
  }
}
Enter fullscreen mode Exit fullscreen mode

In Auth0s docs you can find a description of this, and more examples of what you can do with Auth0 Actions.

Fetch a list of all users

Finally, we can start creating the user admin dashboard for the web page. Let's start with the main page, the one that shows all registered users. In the next article, I will show how to make the components for editing users and deleting users.

I created a userAdminPage.tsx component that returns the user interface with a box at the top with information about who is logged in, a text field to filter / search for users, and a drop-down menu for selecting whether you want to display all users or only administrators or editors. Creating this was pretty straight forward , thanks to a great component library in Chakra UI.

I then created a custom hook (useGetAllUsers.js) that contacts the get-users-in-role API and passes along the access token of the logged in user. The custom hook returns the variables data, loading and error, as well as the getToken function that should be called if Auth0 needs the logged in user's permission for Auth0 to access the user account. This is something new users will see the first time they use the application.

If loading = true, I display my own custom <LoadingSpinner> component with loading message.

const { data, loading, error, getToken } = useGetAllUsers();

if (loading) {
  return (
    <LoadingSpinner spinnerMessage='Kobler til brukerkonto-administrasjon' />
  );
}
Enter fullscreen mode Exit fullscreen mode

When the get-users-in-role API has finished fetching all the users, we find all the users in data.body.users. I use the array method .filter to filter out only the users I want to display, based on what I have entered in the search field. And then I sort all the names with .sort before I use .map to present each user in the array as a "user card" on the screen.

However, before we get to this point, some backend magic has happened in the Gatsby function get-users-in-role. First, we use the @serverless-jwt/jwt-verifier library to read the access token that the client sent when it made a GET request to get-users-in-role. This is the access token of the user who is logged in on the client, and is available in the request header. We use jwt.verifyAccessToken to check that the access token is valid. Then we verify the permissions included in the token, and that those permissions are the ones the user should have to be able to fetch user data from Auth0s Management API. The permissions the user must have to perform various operations are well described in the documentation for Auth0's Management API and in the documentation for the ManagementClient SDK I use to make everything a bit easier for myself.

Here is the first part of the code for the serverless function, the part of the code that checks permissions etc.:

// api/admin-users/get-users-in-role.ts

import { GatsbyFunctionRequest, GatsbyFunctionResponse } from 'gatsby';
const ManagementClient = require('auth0').ManagementClient;
const {
  JwtVerifier,
  JwtVerifierError,
  getTokenFromHeader,
} = require('@serverless-jwt/jwt-verifier');

const jwt = new JwtVerifier({
  issuer: `https://${process.env.GATSBY_AUTH0_DOMAIN}/`,
  audience: `https://${process.env.AUTH0_USERADMIN_AUDIENCE}`,
});

export default async function handler(
  req: GatsbyFunctionRequest,
  res: GatsbyFunctionResponse
) {
  let claims, permissions
  const token = getTokenFromHeader(req.headers.authorization);

  if (req.method !== `GET`) {
    return res.status(405).json({
      error: 'method not allowed',
      error_description: 'You should do a GET request to access this',
    });
  }

  // Verify access token
  try {
    claims = await jwt.verifyAccessToken(token);
    permissions = claims.permissions || [];
  } catch (err) {
    if (err instanceof JwtVerifierError) {
      return res.status(403).json({
        error: `Something went wrong. ${err.code}`,
        error_description: `${err.message}`,
      });
    }
  }

  // check if user should have access at all
  if (!claims || !claims.scope) {
    return res.status(403).json({
      error: 'access denied',
      error_description: 'You do not have access to this',
    });
  }

  // Check the permissions
  if (!permissions.includes('read:roles')) {
    return res.status(403).json({
      error: 'no read access',
      status_code: res.statusCode,
      error_description:
        'Du må ha admin-tilgang for å administrere brukere. Ta kontakt med styret.',
      body: {
        data: [],
      },
    });
  }
.
.
.
Enter fullscreen mode Exit fullscreen mode

The way roles in Auth0 works, is that you first define the roles you want (in our case "user", "editor", "administrator"). Then you define what permissions each role should have. Finally, you assign one or more roles to the users.

Auth0 used to store roles in a separate app_metadata field in the access token for each user, but they now have a new solution for role-based authentication where we no longer get the role names included with the data for each individual user. This made fetching all users and the roles for each user much more cumbersome. I ended up building the following get-users-in-role API:

  • Use the Auth0 ManagementClient SDK to create a new ManagementClient that we call auth0.
  • Now that we have a ManagementClient called auth0, we can use auth0.getRoles() to fetch all available roles we have defined in Auth0. We then get an array with the roles user, admin and editor (we could of course hardcode this, but by using the getRoles method the solution is flexible and will still work if we later decide to create new roles with Auth0.
  • We use .map to create another array that contains all the users within each role. We do this with auth0.[getUsersInRole](https://auth0.github.io/node-auth0/module-management.ManagementClient.html#getUsersInRole) where we as a parameter uses the ID of each of the roles we retrieved with getRoles.
  • We now have a new array called userRoles that contains all three roles, with all users within each role. If a user has two roles (eg is both editor and admin), the user will excist several places.
[
        {
            "role": "admin",
            "users": [
                {
                    "user_id": "auth0|xxx",
                    "email": "kurt@lekanger.no",
                    "name": "Kurt Lekanger"
                }
            ]
        },
        {
            "role": "editor",
            "users": [
                {
                    "user_id": "auth0|xxx",
                    "email": "kurt@lekanger.no",                    
                    "name": "Kurt Lekanger"
                },
                {
                    "user_id": "auth0|yyy",
                    "email": "kurt@testesen.xx",                    
                    "name": "Kurt Testesen"
                },
                        ]
                }
... and so on!
]
Enter fullscreen mode Exit fullscreen mode

This is not exactly what we need. We want an array with all users, where each user excists only once as an object containing an array with all the roles. Therefore, we need to build a new array - I have called it userListWithRoles. First I retrieve all users registered in the Auth0 database with const userList = await auth0.getUsers(). Then I use forEach with a nested for-loop inside to iterate over each user and check whether the user exists in the user list for this role. If a user has a role, that role is added to that user's roles array.

A diagram illustrating how it works and the ManagementClient SDK methods used:

Diagram showing how user roles are added to a roles array for each user.

Finally, I return userListWithRoles from the API and HTTP status code 200 to indicate that everything worked as expected. This is a shortened example of what is returned from the API. Note that each user now has a roles array:

  body: {
    users: [
      {
        name: 'Kurt Lekanger',
        email: "kurt@lekanger.no",
        user_id: 'auth0|xxxx',
        roles: ['admin', 'editor', 'user'],
      },
      {
        name: 'Kurt Testesen',
                email: "kurt@testesen.xx",
        user_id: 'auth0|yyyy',
        roles: ['editor', 'user'],
      },
    ],
  },
Enter fullscreen mode Exit fullscreen mode

In reality, each user object in the userListWithRoles array also contains a lot of other metadata from Auth0, such as when the user last logged in, email address, whether the email has been verified, etc.

Here is the rest of the source code for the get-users-in-role API:

// // api/admin-users/get-users-in-role.ts 
.
.
.
  const auth0 = new ManagementClient({
    domain: `${process.env.GATSBY_AUTH0_DOMAIN}`,
    clientId: `${process.env.AUTH0_BACKEND_CLIENT_ID}`,
    clientSecret: `${process.env.AUTH0_BACKEND_CLIENT_SECRET}`,
    scope: 'read:users read:roles read:role_members',
  });

  try {
    const roles: string[] | undefined = await auth0.getRoles();
    const allUsersInRoles = await roles.map(async (role: any) => {
      const usersInRole = await auth0.getUsersInRole({ id: role.id });
      return { role: role.name, users: usersInRole };
    });

    const userRoles = await Promise.all(allUsersInRoles); // Get a list of all the roles and the users within each of them,
    const userList = await auth0.getUsers(); // and a list of every registered user

    let userListWithRoles = [];
    userList.forEach((user) => {
      for (let i = 0; i < userRoles.length; i++) {
        if (
          userRoles[i].users.find((element) => element.user_id === user.user_id)
        ) {
          const existingUserToModify = userListWithRoles.find(
            (element) => element.user_id === user.user_id
          );
          if (existingUserToModify) {
            existingUserToModify.roles = [
              ...existingUserToModify.roles,
              userRoles[i].role, 
            ];
          } else {
            userListWithRoles.push({
              ...user,
              roles: [userRoles[i].role],
            });
          }
        }
      }
    });

    res.status(200).json({
      body: {
        users: userListWithRoles,
      },
    });
  } catch (error) {
    res.status(error.statusCode || 500).json({
      body: {
        error: error.name,
        status_code: error.statusCode || 500,
        error_description: error.message,
      },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Next step: Useradmin with Gatsby Functions. Update, create and delete users

Feel free to take a look at the finished website here: https://gartnerihagen-askim.no

The project is open source, you can find the source code at my Github.

Here's a video showing the live site with the login protected pages and the user admin dashboard:

This is a translation, the original article in Norwegian is here: Slik lagde jeg et brukeradmin-panel med Gatsby Functions og Auth0

💖 💪 🙅 🚩
klekanger
Kurt Lekanger

Posted on September 12, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related