Step-By-Step Tutorial: Frontend Authorization with Next.js and CASL
- Share:
If you’ve worked on a frontend app before, you probably encountered the need to show or hide features based on the current user logged into your app. That’s called Feature Toggling.
Often users have different permission levels, and we as developers need to control what they should be able to access, but also what they can interact with inside the app.
In this tutorial, we will talk about combining the very popular Next.js Framework with a feature toggling library by Permit.io called permit-fe-sdk
- which utilizes the Permit SaaS no-code solution for managing authorization and CASL (an isomorphic authorization JavaScript library).
We will create a very basic application modeling a supermarket loyalty program, where users will get product promotion recommendations based on their latest shopping list.
đź’ˇ No Authentication Implementation
In this tutorial, we'll be using predefined, static users to achieve the necessary outputs, bypassing the implementation of an authentication system. However, for practical applications, you should integrate a proper user login mechanism using your preferred authentication service.
Setting Up Your Environment
Ensure you have Node.js installed on your system. Next.js requires Node.js 12.22.0
or later. You can check your Node.js version by running node -v
in your terminal. If you need to install or update Node.js, visit the official Node.js website.
Creating a Next.js Project
Open your terminal and run the following command to create a new Next.js app. Replace my-next-app
with your project name.
npx create-next-app@latest my-next-app
This command generates a new Next.js project in a directory named my-next-app and installs all the necessary dependencies.
Then you need to navigate into the repository folder:
cd my-next-app
Installing permit dependencies
We need to install two sets of dependencies. One set is to allow permissions control via Permit and its frontend SDK.
npm install @casl/ability @casl/react permit-fe-sdk permitio
The other set is to allow us to create a nicer UI.
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
Building the app
Step 1: Define Users and Permissions
Let's start by defining a simple user management system and permissions. For this example, we will statically define two users and simulate a login mechanism by selecting a user based on an ID.
Create a new file under src/userManagement.js
:
export const users = [
{
id: 'user1',
role: 'standard_user',
permissions: [
{ action: 'view', resource: 'basic_content' },
],
},
{
id: 'user2',
role: 'premium_user',
permissions: [
{ action: 'view', resource: 'basic_content' },
{ action: 'view', resource: 'premium_content' },
],
},
];
export const getUserById = (userId) => users.find(user => user.id === userId);
Step 2: Creating our backend route
We will check permissions as soon as the user logs in, but just before the content loads onto the screen. Thus, we need to direct all our requests to a backend route. That will initialize a connection with Permit, send all the relevant information to the Permit PDP (microservice for authorization), and then reply back with a list of permitState
permissions. These can be utilized to render exactly what we want to the screen based on the current user and their rules inside of the policy we will configure.
import { Permit } from "permitio";
const permit = new Permit({
token: "YOUR_PERMIT_API_KEY",
pdp: "<http://localhost:7766>",
});
export default async function handler(req, res) {
try {
const { resourcesAndActions } = req.body;
const { user: userId } = req.query;
if (!userId) {
return res.status(400).json({ error: "No userId provided." });
}
const checkPermissions = async (resourceAndAction) => {
const { resource, action, userAttributes, resourceAttributes } =
resourceAndAction;
const allowed = permit.check(
{
key: userId,
attributes: userAttributes,
},
action,
{
type: resource,
attributes: resourceAttributes,
tenant: "default",
}
);
return allowed;
};
const permittedList = await Promise.all(
resourcesAndActions.map(checkPermissions)
);
console.log(permittedList);
return res.status(200).json({ permittedList });
} catch (error) {
console.error(error);
return res.status(500).json({ error: "Internal Server Error" });
}
}
Step 3: Create a Context for User and Permissions
Create a new file under src/AbilityContext.js
for the ability context and provider:
"use client";
import React, { createContext, useState, useEffect } from "react";
import { Ability } from "@casl/ability";
import { getUserById } from "./userManagement";
import { Permit, permitState } from "permit-fe-sdk";
export const AbilityContext = createContext();
export const AbilityProvider = ({ userId, children }) => {
const [ability, setAbility] = useState(new Ability());
useEffect(() => {
const user = getUserById(userId);
const getAbility = async () => {
const permit = Permit({
loggedInUser: user,
backendUrl: "/api/checkPermissions",
});
const rules = user.permissions.map(({ action, resource }) => ({
action,
subject: resource,
}));
const caslConfig = permitState.getCaslJson();
return caslConfig && caslConfig.length
? new Ability(caslConfig)
: undefined;
};
if (user) {
getAbility(user.id).then((caslAbility) => {
setAbility(caslAbility);
});
}
}, [userId]);
return (
<AbilityContext.Provider value={ability}>
{children}
</AbilityContext.Provider>
);
};
Step 4: Integrate the AbilityProvider into Your Application
Assuming you want to select the user based on a static ID for this example, integrate the AbilityProvider in your RootLayout component in src/layout.js
. You will need to pass a user ID to the AbilityProvider.
For the sake of this example, we'll arbitrarily choose user1
or user2
.
import React from "react";
import { AbilityProvider } from "./AbilityContext";
export const metadata = {
title: "Next.js",
description: "Generated by Next.js",
};
export default function RootLayout({ children }) {
const userId = "user1";
return (
<AbilityProvider userId={userId}>
<html lang="en">
<body>{children}</body>
</html>
</AbilityProvider>
);
}
Step 5: Utilizing Abilities in the Home Component
Finally, modify the Home component in src/page.js
to render content based on the user's abilities. This example will conditionally render premium content.
"use client";
import React, { useContext } from "react";
import { AbilityContext } from "./AbilityContext";
export default function Home() {
const ability = useContext(AbilityContext);
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
{permitState?.check("view", "basic_content") && (
<div>Basic Content for All Users</div>
)}
{permitState?.check("view", "premium_content") && (
<div>Premium Content for Premium Users</div>
)}
</main>
);
}
Step 6: Building a basic UI
It’s time to build out the page.js
component, which will be the landing page for our users to access their accounts. The idea is to use the data that we have in our userManagement.js
file to render the relevant information, making sure the user has the ability to visualize this information. We will get into the enforcement of this in just a second, but first, let's build a working UI.
We will need a first page that loads two buttons: One for the standard user
, and another for the premium user
. This is us mocking the authentication solution and mocking the confirmation of that user's identity.
Once the user clicks each button, they will access the relevant information for that profile.
Let’s create some mock data for that information to ease the building of our UI.
const products = [
{
id: 1,
name: "Apples",
description: "Fresh green apples",
imageUrl: "<https://source.unsplash.com/1600x900/?apples>",
},
{
id: 2,
name: "Oranges",
description: "Juicy sweet oranges",
imageUrl: "<https://source.unsplash.com/1600x900/?oranges>",
},
{
id: 3,
name: "Milk",
description: "Organic whole milk",
imageUrl: "<https://source.unsplash.com/1600x900/?milk>",
},
];
const discountVouchers = [
{
id: "voucher1",
title: "10% OFF",
description: "Get 10% off on your next purchase",
},
{
id: "voucher2",
title: "20% OFF",
description: "Exclusive 20% discount on premium products",
},
];
With this information in place, let’s build out the code and have a semi-working solution. By this, I mean a solution that renders a specific UI without the Permit Authorization enforcement taking part just yet.
"use client";
import React, { useContext, useState } from "react";
import { users } from "./userManagement";
import { AbilityContext } from "./AbilityContext";
import { permitState } from "permit-fe-sdk";
import Card from "@mui/material/Card";
import CardMedia from "@mui/material/CardMedia";
import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import Grid from "@mui/material/Grid";
import Container from "@mui/material/Container";
import Box from "@mui/material/Box";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import StarIcon from "@mui/icons-material/Star";
const products = [
{
id: 1,
name: "Apples",
description: "Fresh green apples",
imageUrl: "<https://source.unsplash.com/1600x900/?apples>",
},
{
id: 2,
name: "Oranges",
description: "Juicy sweet oranges",
imageUrl: "<https://source.unsplash.com/1600x900/?oranges>",
},
{
id: 3,
name: "Milk",
description: "Organic whole milk",
imageUrl: "<https://source.unsplash.com/1600x900/?milk>",
},
];
const discountVouchers = [
{
id: "voucher1",
title: "10% OFF",
description: "Get 10% off on your next purchase",
},
{
id: "voucher2",
title: "20% OFF",
description: "Exclusive 20% discount on premium products",
},
];
export default function Home() {
const ability = useContext(AbilityContext);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [userRole, setUserRole] = useState("");
const handleLogin = (userId) => {
const user = users.find((user) => user.id === userId);
if (user) {
setIsLoggedIn(true);
setUserRole(user.role); // Set user role based on login
}
};
return (
<>
<AppBar position="static">
<Toolbar className="bg-blue-500">
<Typography variant="h6" color="inherit" component="div">
Supermarket Loyalty Program
</Typography>
</Toolbar>
</AppBar>
<Box mt={4}>
<Container>
{!isLoggedIn ? (
<Box className="flex flex-col items-center gap-2">
{/* Buttons to simulate login for different user roles */}
<Button
variant="contained"
color="primary"
onClick={() => handleLogin("user1")}
style={{ marginRight: "20px" }}
>
Login as Standard User
</Button>
<Button
variant="contained"
color="secondary"
onClick={() => handleLogin("user2")}
style={{ marginRight: "20px" }}
>
Login as Premium User
</Button>
</Box>
) : (
<>
<Grid container spacing={2}>
{products.map((product) => (
<Grid item xs={12} sm={6} md={4} key={product.id}>
<Card>
<CardMedia
component="img"
height="300"
image={product.imageUrl}
alt={product.name}
/>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{product.name}
</Typography>
<Typography variant="body2" color="text.secondary">
{product.description}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
{userRole === "premium_user" && (
<Box mt={4}>
<Typography variant="h5" gutterBottom>
Exclusive Discounts for You
</Typography>
<Grid container spacing={2}>
{discountVouchers.map((voucher) => (
<Grid item xs={12} sm={6} md={4} key={voucher.id}>
<Card raised>
<CardContent>
<Typography
variant="h5"
component="div"
gutterBottom
>
{voucher.title} <StarIcon color="secondary" />
</Typography>
<Typography variant="body1">
{voucher.description}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
)}
</>
)}
</Container>
</Box>
</>
);
}
Here is what this app will now render:
Step 7: Creating out basic policy in the Permit no-code UI
To get started, make an account with Permit here. Permit is a full-stack authorization service that allows us to manage the policies we assign to each role with a no-code UI.
As you log in, you will get asked to create an organization, and then you will be in the App!
The first thing you will need to do is retrieve the Permit API Key and pass it into the Permit instance object. Make sure to add this to your .env.local
file. You can find your API key here:
You can also follow this simple Quickstart guide if you prefer.
Once we have everything set up, we can start to configure our policies to enforce access and make sure that only the users we assigned specific roles get shown specific components in the UI.
1. Setting up a basic policy in Permit’s no-code dashboard
Navigate to the Policy page - you can find it on the left navigation bar.
As we have no roles yet, as soon as we click “Manage Roles” we will be prompted to create our first role. Let’s do that!
We only need two roles, let’s call them Basic Content Viewer
and Premium Content Viewer
roles. We can add a short description to each role if we want to. For this example, the naming is self-explanatory, so a description is unnecessary.
We created two roles! Now we need to add a resource, something we will be performing an action on — and thus testing if we have the permission to do so.
Navigate to the Resources tab at the top, and let’s create four new resources. In this case, each component of the page that we conditionally want to render per each user should be its own resource.
Let’s create:
- Item A
- Item B
- Item C
- Item D
As we create our resource we need to specify the actions on that resource. Because we are working with just viewing components, we should have a specific action for that component, and we can call it view
.
đź’ˇ We can have other actions for each component if your functionality is more complicated or you want a user to interact with that component in a restricted way. For this demo example, we will just work with one action.
And here is all our resource created:
Great! We are all good to go to enforce the policy in our code.
2. Working with the Policy Editor
Now that we have our policy ready, let’s check some actions, let’s change between users, and see how the UI changes!
3. Implementing guards in our code to only render specific UI components
The products
and vouchers
are now conditionally rendered based on the permitState
that comes back from our policy engine. Below are the two implementations of the guards:
{permitState?.check("view", "basic_content")
? products.map((product) => (
// Rest of code
))
: null}
{permitState?.check("view", "premium_content") ? (
// Rest of voucher code
) : null}
The last thing that needs to be done now is to run the Permit policy engine and connect your application.
- Pull the docker image
docker pull permitio/pdp-v2:latest
- Run the docker image with the Environment API key. In this case, this is the API key we are storing in our
.local.env
file.
docker run -it -p 7766:7000 --env PDP_DEBUG=True --env PDP_API_KEY=<YOUR_API_KEY> permitio/pdp-v2:latest
And hurray! You have a working application!
Concluding
Managing permissions and implementing feature flagging can initially seem complex. Yet, with the integration of specialized tools and frameworks, these tasks become significantly more manageable. This approach is essential for parts of the user interface that require strict control to prevent unauthorized access.
Utilizing Permit.io and CASL together facilitates a straightforward and effective method for handling permissions, ensuring both ease of use and top-tier security. These solutions are designed to demystify the process of permissions management, making it accessible to developers who need to enforce detailed access controls without sacrificing security.
For those seeking deeper insights or needing guidance on utilizing Permit.io and CASL, our Slack channel offers a supportive community where questions are welcomed and knowledge is freely shared. Additionally, our detailed documentation provides a comprehensive overview of managing granular permissions, offering valuable resources for those looking to understand and implement these controls effectively.
Written by
Filip Grebowski
Developer Advocate at Permit.io, Software Engineer, and YouTube Creator