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:
- *Gatsby on the frontend to create the user admin dashboard. For the dashboard I use client-only routes in Gatsby, which I have written about here.
- Auth0 Authentication API for frontend user authentication. Here I use the Auth0 React SDK for Single Page Apps to make things a little easier for myself.
- Gatsby Functions (serverless functions) on the backend. These are Node applications running on the server that contact the Auth0 Management API to create, update, or delete users.
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:
This is a simplified diagram showing how this works:
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` })
}
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
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:
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:
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.
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:
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.
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).
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).
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);
}
}
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' />
);
}
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: [],
},
});
}
.
.
.
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 useauth0.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 withgetRoles
. - 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!
]
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:
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'],
},
],
},
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,
},
});
}
}
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
Top comments (0)