Managing Frontend Authorization in Vue with CASL
- Share:
Vue.js is a powerful front-end framework for building fast and interactive web applications. While Vue provides a great development experience, managing user permissions can be challenging—especially when different users need access to different features.
Ensuring a smooth and secure user experience requires frontend authorization that dynamically adjusts the UI based on a user's role and permissions.
This guide will walk through how to implement UI-level authorization in Vue using CASL.
This article is a direct continuation of our previous guide, "How to Implement Role-Based Access Control (RBAC) in Vue.js", where we covered enforcing permissions at the backend level using Permit.io.
In this tutorial, we will focus entirely on frontend UI authorization using CASL, a JavaScript permission management library that lets you declaratively define and enforce access control in Vue applications.
We’ll walk through setting up roles, defining permissions, integrating CASL in Vue, and dynamically controlling UI elements in a food delivery app example.
To streamline permission management, we’ll also use Permit.io to handle role assignments and resource definitions through its no-code dashboard. While CASL ensures that restricted UI elements remain hidden or disabled, Permit.io can centralize role and permission management for a complete access control system.
By the end of this tutorial, you'll have a Vue app with fine-grained UI authorization, where users only see and interact with the features they’re allowed to access.
What is CASL?
CASL is a JavaScript authorization library designed to control what users can see and do in an application. It allows developers to define fine-grained authorization rules declaratively and apply them directly to the UI.
Unlike backend authorization solutions that enforce permissions at the API level, CASL is focused on frontend enforcement. It ensures that restricted UI elements—such as buttons, forms, and navigation links—are hidden or disabled based on the user’s role and permissions.
How CASL Works
CASL defines permissions using an "ability" object, which determines whether a user can perform specific actions on resources. For example, a rider in a food delivery app should be able to see orders but not fulfill them. We can express this rule with CASL like this:
import { defineAbility } from '@casl/ability';
const ability = defineAbility((can, cannot) => {
can('read', 'Order'); // All users can read orders
cannot('fulfill', 'Order'); // Riders cannot fulfill orders
});
This ability object can then be used throughout the Vue app to dynamically control UI elements. In the next sections, we'll set up CASL in a Vue project and demonstrate how to enforce permissions effectively.
Setting Up a Vue Project with CASL
Before we start integrating CASL into a Vue application, let’s set up our project and install the necessary dependencies.
Prerequisites
To follow along with this tutorial, you’ll need:
- Node.js (16+) installed on your machine
- A Permit.io account with an RBAC policy already set up (as covered in "How to Implement Role-Based Access Control (RBAC) in Vue.js")
⚠️ Important: This guide uses roles and resources defined in the previous article on enforcing RBAC permissions in Vue on the backend using Permit.io. This blog will cover the code implementation of it, so you can either go back to the previous guide for the UI setup, or just go along with this one and implement it as code - up to you.
A working version of the Vue codebase for food delivery is available here
Installing CASL in Vue
If you need to create a new Vue project with Vite, run:
npm create vite@latest vue-casl-demo --template vue
cd vue-casl-demo
npm install
Then, install Vue Router, Pinia, and Tailwind if it’s not included:
npm install vue-router pinia tailwind
With our setup ready, let's plan our frontend authorization rules and define user permissions using CASL.
Planning Our Frontend Authorization Rules
Before implementing CASL in Vue, we need a clear plan for how permissions will be structured. Since this guide builds on our previous article on RBAC in Vue.js, we'll continue using a food delivery service example.
User Roles & Permissions
In our food delivery app, users belong to one of four roles:
Role | Can Create Orders | Can Fulfill Orders | Can Assign Riders | Can Deliver Orders |
---|---|---|---|---|
Customer | ✅ | ❌ | ❌ | ❌ |
Vendor | ❌ | ✅ | ❌ | ❌ |
Admin | ❌ | ✅ | ✅ | ✅ |
Rider | ❌ | ❌ | ❌ | ✅ |
These roles determine what actions users can perform on resources (Meals
and Orders
). Our goal is to enforce these rules dynamically in the UI using CASL for frontend authorization while Permit.io handles role and permission management in the backend.
Using CASL with Permit.io
Rather than hardcoding permissions, we’ll integrate CASL with Permit.io to dynamically fetch and apply access rules. Permit.io will store roles, resources, and permissions in a centralized system, and CASL will use these definitions to enforce frontend restrictions.
How CASL & Permit.io Work Together
- Permit.io maintains the policy table, defining which roles can perform which actions.
- A backend API fetches permissions from Permit.io in bulk.
- CASL reads the permissions from the backend and applies them to Vue components, hiding or disabling UI elements accordingly.
To achieve this, we’ll:
- Use a backend API to handle permission checks.
- Store permissions in a Pinia store for frontend access control.
- Use CASL to show or hide UI elements dynamically.
Next, let’s start implementing these permissions by setting up CASL in Vue.
Defining Permissions in CASL
Now that we've outlined our user roles and permissions, it's time to implement them in CASL. This section will focus on defining and managing permissions in the frontend while integrating them with Permit.io for dynamic access control.
Setting Up CASL in Vue
Before we define permissions, make sure CASL and the Permit.io frontend SDK are installed. If you haven’t already, install them using:
npm install @casl/ability @casl/vue permit-fe-sdk
The installation has been done for you in the example tutorial codebase, so you don’t need to reinstall if you are coding along.
Since we are integrating CASL with Permit.io, we also need a backend endpoint to fetch permissions for the signed-in user. The example codebase already provides this API in /backend/src/abilities.ts
.
Backend API for Fetching Permissions
Permit.io requires a backend API to fetch and validate permissions. This API checks which actions a user can perform on which resources.
The following Express route handles this in /backend/src/abilities.ts
. We will be looping through resourcesAndActions
and checking permissions for the provided userId
as seen below:
import 'dotenv/config';
import { Request, Response } from 'express';
import { Permit } from 'permitio';
const permit = new Permit({
pdp: '<https://cloudpdp.api.permit.io>',
token: process.env.PERMIT_TOKEN
});
export const getAbilities = async (req: Request, res: Response) => {
try {
const { resourcesAndActions } = req.body;
const { user: userId } = req.query;
if (!userId) {
res.status(400).json({ error: 'No userId provided.' });
return;
}
const checkPermissions = async (resourceAndAction) => {
const { resource, action, userAttributes, resourceAttributes } = resourceAndAction;
return permit.check(
{ key: userId as string, attributes: userAttributes },
action,
{ type: resource, attributes: resourceAttributes, tenant: 'default' }
);
};
const permittedList = await Promise.all(
resourcesAndActions.map(checkPermissions)
);
res.status(200).json({ permittedList });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
}
};
For the above code to work, provide the Permit token in a .env
file in the backend directory. This token can be obtained from the Permit UI.
We are also using Permit.io’s generally available public cloud PDP (Policy Decision Point).
⚠️ Note: the cloud PDP doesn’t support complex authorization policies like Attribute-based access control (ABAC) or Relationship-based Access Control (ReBAC). If you have ABAC and/or ReBAC set up, you will have to run a local PDP (with Docker) or a custom cloud-hosted PDP.
How This Works:
When a Vue component requests permission data, it sends a request to the backend API. This API endpoint:
- Receives a user ID and a list of
resourcesAndActions
. - Calls
permit.check()
to verify if the user can perform each action. - Returns a permission list for the frontend to use.
Storing Permissions in a Pinia Store
With the bulk backend API and the frontend packages installed, how do you achieve a CASL integration with Permit.io in Vue?
The trick is to use a Pinia store to bulk-load the permissions for the signed-in user. This store will:
- Load permissions from the backend.
- Expose CASL’s
$can
and$cannot
helpers for UI authorization. - Keep permissions updated when the user changes.
You can see an example of this in the src/stores/abilities.ts
file:
// In src/stores/abilities.ts
import { PureAbility } from '@casl/ability';
import { Permit } from 'permit-fe-sdk';
import { defineStore } from 'pinia';
import { onMounted, ref, watch } from 'vue';
import { useUserStore } from './user';
export const useAbilitiesStore = defineStore('abilities', () => {
const current = ref(new PureAbility([]));
const user = useUserStore();
const permit = ref(
Permit({
loggedInUser: user.current,
backendUrl: '<http://localhost:3000/abilities>'
})
);
const setAbility = async () => {
permit.value = Permit({
loggedInUser: user.current,
backendUrl: '<http://localhost:3000/abilities>'
});
permit.value.reset();
await permit.value.loadLocalStateBulk([
{ action: 'create', resource: 'Meal' },
{ action: 'read', resource: 'Meal' },
{ action: 'delete', resource: 'Meal' },
{ action: 'create', resource: 'Order' },
{ action: 'read', resource: 'Order' },
{ action: 'fulfill', resource: 'Order' },
{ action: 'assign-rider', resource: 'Order' },
{ action: 'deliver', resource: 'Order' }
]);
const caslConfig = permit.value.getCaslJson();
current.value = new PureAbility(
caslConfig && caslConfig.length ? caslConfig : []
);
};
onMounted(async () => {
await setAbility();
watch(
() => user.current,
async (_) => await setAbility()
);
document.addEventListener(
'visibilitychange',
async (_) => await setAbility()
);
});
return { current, permit };
});
What is happening here?
This Pinia store plays a crucial role in managing frontend authorization. Let’s go over its key components:
The
setAbility
function is the core of this store. Its job is to:- Initialize a new Permit instance whenever a user logs in or changes.
- Reset and reload policies to ensure the permissions remain up-to-date.
- Fetch the allowed actions and resources from the backend API and store them in CASL.
Every time a new user logs in,
setAbility
ensures that the Vue app loads only the permissions relevant to that user.The actions and resources listed in
setAbility
correspond to what the food delivery app’s UI requires.Automatic permission synchronization:
- In the
onMounted
lifecycle hook,setAbility
is called:- When a user logs in or changes roles – ensuring they see the correct UI elements.
- When the browser tab regains focus – ensuring permissions stay updated if they were modified while the user was away.
This means users don’t need to refresh the page to see permission updates—an important usability improvement.
Once permissions are loaded, any Vue component can access them via:
- The
permit
instance for advanced permission checks (permit.check()
supports additional context like attributes). - CASL’s
$can
and$cannot
helpers for quick UI toggling in templates.
As we’ll see in the next sections, these tools allow Vue components to dynamically hide, disable, or restrict UI elements based on the user’s role and permissions.
Enforcing Permissions in Vue Components
Now that we have CASL and Permit.io integrated, let’s apply permissions to hide or disable UI elements dynamically:
Option 1: Using CASL’s $can
in Vue Templates
CASL provides $can
and $cannot
directives, allowing us to show or hide elements dynamically.
<template v-if="$can('create', 'Order')">
<button @click="createOrder()">Create Order</button>
</template>
Option 2: Using the Permit Instance for More Control
Instead of using CASL’s $can
, we recommend using permit.check()
for greater flexibility:
<template v-if="permit.check('create', 'Meal', {})">
<button @click="createMeal()">Create Meal</button>
</template>
Why use permit.check()
instead of $can
?
- The Permit instance provides a richer authorization context.
- It supports ABAC (Attribute-Based Access Control) and ReBAC (Relationship-Based Access Control). For more about using ABAC and ReBAC in CASL, check out the Permit.io docs.
- It handles permission updates more efficiently.
Applying Permissions to Action Buttons
The food delivery example app dynamically enforces permissions on buttons.
In the src/components/ManageMeals.vue
component, multiple permission checks ensure that:
- Only authorized users can submit the form to create a meal.
- Only authorized users can see the meal creation form.
- Only authorized users can view the delete button for meals.
<script setup lang="ts">
// ...
const onFormSubmit = async (e: FormSubmitEvent) => {
if (e.valid && permit.value.check('create', 'Meal', {})) {
// ...
}
};
</script>
<template>
<Form
v-slot="$form"
:initialMealValues
:resolver
@submit="onFormSubmit"
v-if="permit.check('create', 'Meal', {})"
>
<!-- ... -->
</Form>
<div>
<!-- ... -->
<Button
v-if="permit.check('delete', 'Meal', {})"
severity="contrast"
variant="text"
icon="pi pi-trash"
class="text-red-500 px-2 py-0 -mr-3"
@click="meals.remove(id)"
/>
</div>
</template>
The OrdersDisplay.vue
component is used across all user roles in the application. However, certain actions—such as fulfilling, delivering, and assigning riders to orders—must be restricted based on the user’s permissions.
The following code ensures that only authorized users can:
- Fulfill an order (
fulfill
action) - Assign a rider to an order (
assign-rider
action) - Deliver an order (
deliver
action)
<script setup lang="ts">
// ...
const fulfill = async (orderId: number) => {
if (permit.value.check('fulfill', 'Order', {})) {
// ...
}
};
const assignRider = async (e: FormSubmitEvent, orderId: number) => {
if (e.valid && permit.value.check('assign-rider', 'Order', {})) {
// ...
}
};
const deliver = async (orderId: number) => {
if (permit.value.check('deliver', 'Order', {})) {
// ...
}
};
</script>
<template>
<!-- ... --->
<Button
type="submit"
label="Fulfill"
v-if="!order.fulfilledTime && permit.check('fulfill', 'Order', {})"
@click="fulfill(order.id)"
:loading="isFulfilling"
/>
<Button
type="submit"
label="Deliver"
v-if="!order.deliveredTime && permit.check('deliver', 'Order', {})"
@click="deliver(order.id)"
:loading="isDelivering"
/>
<Form
v-slot="$form"
:initialRider
:resolver
@submit="(e) => assignRider(e, order.id)"
v-if="
permit.check('assign-rider', 'Order', {}) &&
!order.riderAssignedTime
"
class="flex gap-4 items-end"
>
<!-- ... --->
</Form>
<!-- ... --->
</template>
Customizing for Your Application
The approach shown here can be extended or simplified depending on your application’s needs. Some apps may require additional attribute-based conditions (ABAC) or relationship-based policies (ReBAC), which Permit.io supports.
The app’s navigation allows you to toggle the current user ID and test allows or denies. For this to work, you should have set up the example user IDs and assigned their roles in your Permit UI dashboard.
Summary
In this guide, we explored how to implement frontend authorization in Vue using CASL, ensuring that UI elements dynamically adapt based on user permissions. Unlike backend authorization, which restricts API access, CASL enforces access control directly in Vue components—hiding or disabling UI elements based on a user’s role and permissions.
We covered:
- Planning frontend authorization rules by defining roles, actions, and resources.
- Setting up CASL in Vue and integrating it with Permit.io for real-time permission checks.
- Fetching permissions dynamically from the backend using a Permit.io-powered API.
- Managing permissions in a Pinia store, ensuring they update when users switch roles.
- Using CASL and
permit.check()
to control UI elements, restricting actions like creating, fulfilling, and delivering orders.
By combining CASL for UI-level control with Permit.io for backend policy management, we created a scalable, flexible authorization system that enforces permissions at both the frontend and API levels.
Next Steps & Further Reading
To continue refining your authorization system, check out:
- How to Implement Role-Based Access Control (RBAC) in Vue.js (Covers backend RBAC enforcement with Permit.io)
- CASL Official Docs (Learn more about CASL’s capabilities)
For questions or further guidance, join our community or check out the GitHub repo for the example food delivery app.
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker