Implementing Role Based Access Control in SvelteKit
Tags: svelte, sveltekit, tutorial, security
Published: 2024-05-30 11:43:46

### Table Of Contents
* <a href="#understanding-role-based-access-control-rbac">Understanding Role-Based Access Control (RBAC)</a>
* <a href="#setting-up-the-sveltekit-project">Setting Up the SvelteKit Project</a>
* <a href="#integrating-rbac-configurations-and-utilities">Integrating RBAC Configurations and Utilities</a>
    * <a href="#defining-roles-and-permissions">Defining Roles and Permissions</a>
    * <a href="#implementing-rbac-utility-functions">Implementing RBAC Utility Functions</a>
* <a href="#dynamic-navigation-based-on-user-roles">Dynamic Navigation Based on User Roles</a>
* <a href="#securing-routes-based-on-user-roles">Securing Routes Based on User Roles</a>
* <a href="#managing-permissions-within-components">Managing Permissions Within Components</a>
* <a href="#conclusion">Conclusion</a>

### **Prerequisites**

Before you begin, you'll need to have the following prerequisites:

* **Basic knowledge of SvelteKit**: Familiarity with SvelteKit's core concepts, like routing, components, and stores, will be helpful.
* **Node.js and npm:** You'll need to have Node.js and npm (Node Package Manager) installed on your system. If you haven't already, you can download and install them from the official [Node.js website](https://nodejs.org/).
* **Git:** Ensure you have Git installed on your machine. If not, you can download it from the [Git website](https://git-scm.com/).

[![Github Banner](https://github.com/Permify/permify/assets/34595361/440284b3-a8d6-4c6b-a17f-116c49e72a58)](https://github.com/Permify/permify/)

<h2 id="understanding-role-based-access-control-rbac">Understanding Role-Based Access Control (RBAC)</h2>

RBAC is a security model that organizes user permissions based on their roles within an application. 

Each role is associated with a set of permissions, and users are assigned specific roles to determine what they can access.

### **Roles and Permissions**

Roles represent a collection of related permissions that define what a user can do within an application. 

For instance, an application might have roles such as `Admin`, `Editor`, and `Viewer` each with different sets of permissions. 

Permissions, on the other hand, are specific actions or operations that a user with a particular role can perform. 

These can range from viewing and editing data to managing user accounts or system configurations.

<h2 id="setting-up-the-sveltekit-project">Setting Up the SvelteKit Project</h2>

Before we start implementing RBAC, we need a SvelteKit project ready to work with. 

To keep things simple, we'll use a demo e-commerce project as our starting template. The complete code for this project can be found in this [GitHub repository](https://github.com/TropicolX/svelte-rbac-demo).

To get the starter template, run the following commands:

```bash
git clone https://github.com/TropicolX/svelte-rbac-demo.git
cd svelte-rbac-demo
git checkout starter
npm install
```

The project structure should look like the one below:

![Screenshot of project structure](https://res.cloudinary.com/tropicolx/image/upload/v1715948411/Implementing%20Role-Based%20Access%20Control%20in%20SvelteKit/962ef8aa-a41c-467c-957a-2ef9a394771b_rrhaoa.png?align="center")

Next, let's go over the essential files and directories we'll be working with for the project:

* `/lib`: This directory will contain all our utilities and components.
* `/routes`: Contains all the routes in our application.  
    * `/(protected)`: This folder contains all our protected routes. Only signed-in users may be able to navigate these routes.     
    * `/login`: This is the route to the login page.    
    * `/unauthorized`: This is the route we redirect users to when they are unauthorized in accessing a page.    
    * `+layout.svelte`: This contains the UI layout and logic that applies to every page.     
    * `+page.svelte`: This is our home page route.     
* `hooks.server.js`: This file contains all the server-side logic we need to authenticate users when they navigate to a protected route and to redirect them when necessary. It also fetches the user data on the server and stores it in the [`event.locals`](https://kit.svelte.dev/docs/hooks#server-hooks-handle) object.
    

### **Project Overview**

To start the project, run the following command in the root directory:

```bash
npm run dev
```

You should see a page like the one below:

![Screenshot of e-commerce project home page](https://res.cloudinary.com/tropicolx/image/upload/v1715948453/Implementing%20Role-Based%20Access%20Control%20in%20SvelteKit/62341754-6934-42db-a036-f0af349c3547_t34a4d.png?align="center")

Next, let's sign in to the app. Click the "Sign in" button at the top right corner of the page.

![Sign in page](https://res.cloudinary.com/tropicolx/image/upload/v1715948502/Implementing%20Role-Based%20Access%20Control%20in%20SvelteKit/eb66d3a3-bbd9-4043-8b75-81d6771574e8_mqbx5k.png?align="center")

You can sign in as either an admin or a customer using the following credentials:

* **Admin:**
    * **Email:** ``admin@gmail.com``
    * **Password:** ``admin123``
        
* **Customer:**
    * **Email:** ``john@mail.com``
    * **Password:** ``changeme``
        

After signing in, you will be redirected to the home page and should able to see two new links on the navbar:

* Admin link
* Profile link
    

![Initial home page when signed in](https://res.cloudinary.com/tropicolx/image/upload/v1715948530/Implementing%20Role-Based%20Access%20Control%20in%20SvelteKit/f3fbe006-582e-46da-a3fe-c537619ebd72_vyxsai.png?align="center")

This brings us to the current problem with our e-commerce app: We need to ensure that only admins can access the admin page, and similarly, only customers can access the profile page.

Additionally, we need to ensure that only admins and customers with the required permissions can perform specific actions within their respective pages. 

For instance, only an admin with permission to create products should be able to do so on the admin page.

<h2 id="integrating-rbac-configurations-and-utilities">Integrating RBAC Configurations and Utilities</h2>

Now that we've set up and familiarized ourselves with the Svelekit project let's begin implementing RBAC in our app. We'll start by defining the roles and permissions within the app.

<h3 id="defining-roles-and-permissions">Defining Roles and Permissions</h3>

In the SvelteKit project, create a new file called `constants.js` in the `src` directory and add the following code:

```javascript
// src/constants.js
export const ROLES = {
	ADMIN: "admin",
	CUSTOMER: "customer",
};

export const PERMISSIONS = {
	CREATE_PRODUCTS: "create:products",
	UPDATE_PRODUCTS: "update:products",
	DELETE_PRODUCTS: "delete:products",
	READ_PROFILE: "read:profile",
	UPDATE_PROFILE: "update:profile",
};
```

In the code above, we defined our app's roles and permission constants.

Next, we need to define the permissions each role has. Navigate to the `lib` directory and create a `rolePermissions.js` file with the following code:

```javascript
// src/lib/rolePermissions.js
import { PERMISSIONS, ROLES } from "../constants";

export const rolePermissions = {
	[ROLES.ADMIN]: [
		PERMISSIONS.CREATE_PRODUCTS,
		PERMISSIONS.UPDATE_PRODUCTS,
		PERMISSIONS.DELETE_PRODUCTS,
	],
	[ROLES.CUSTOMER]: [PERMISSIONS.READ_PROFILE, PERMISSIONS.UPDATE_PROFILE],
};
```

The code above sets up each of the roles with specific permissions. We want admin users to be able to create, update, and delete products. 

On the other hand, we want customers to be able to read and update their profile details.

<h3 id="implementing-rbac-utility-functions">Implementing RBAC Utility Functions</h3>

Next, let's create some utility functions to check if a user has a certain role or permission. These functions will enforce access control throughout the app.

In the `lib` directory, create a `rbacUtils.js` file and add the following code:

```javascript
// src/lib/rbacUtils.js
import { rolePermissions } from "./rolePermissions";

// Function to check if a user has a specific role
export function checkRole(user, requiredRole) {
	if (!user) {
		return false;
	}

	return user.role === requiredRole;
}

// Function to check if a user has specific permissions
export function checkPermissions(user, requiredPermissions) {
	if (!user) {
		return false;
	}

	const userPermissions = rolePermissions[user.role];
	return userPermissions?.includes(requiredPermissions);
}

// Function to check if a user has both a specific role and permissions
export function checkRoleAndPermissions(
	user,
	requiredRole,
	requiredPermissions
) {
	return (
		checkRole(user, requiredRole) &&
		checkPermissions(user, requiredPermissions)
	);
}
```

With these configurations in place, we can start integrating RBAC within our app.

<h2 id="dynamic-navigation-based-on-user-roles">Dynamic Navigation Based on User Roles</h2>

The navigation UI is the first area in our app where we'll implement RBAC. We need to ensure that users see only the navigation links relevant to their roles.

Firstly, head over to the `+layout.svelte` file in the `routes` folder and add the following code:

```javascript
<!-- src/routes/+layout.svelte -->
<script>
    ...
	import Loading from "$lib/components/Loading.svelte";
	import { checkRole } from "$lib/rbacUtils";
	import { ROLES } from "../constants";
	import { user, cart, products } from "../stores";
	import "../app.css";

	...
	$: isAdmin = checkRole($user, ROLES.ADMIN);
	$: isCustomer = checkRole($user, ROLES.CUSTOMER);
</script>

...
```

Here, we defined two variables, `isAdmin` and `isCustomer`, which check if the user is an admin or a customer, respectively. We also used reactive declarations to define the variables so that any changes to the user data, e.g., if the user logs out, will update their values.

Next, let's update our navigation links:

```javascript
<!-- src/routes/+layout.svelte -->
...

<header
	class="bg-gray-900 text-white py-4 px-6 md:px-12 flex items-center justify-between"
>
	...
	<nav class="hidden md:flex items-center space-x-6">
		{#if isAdmin}
			<a class="hover:text-gray-300" href="/admin">Admin</a>
		{/if}
		{#if isCustomer}
			<a class="hover:text-gray-300" href="/user">Profile</a>
		{/if}
	</nav>
    ...
</header>

...
```

![Demo of dynamic navigation implementation](https://res.cloudinary.com/tropicolx/image/upload/v1715948598/Implementing%20Role-Based%20Access%20Control%20in%20SvelteKit/51ff4981-2756-4d54-8b91-0311b149b3c6_xptqub.gif?align="center")

Now, the navigation links for the admin and profile pages will be displayed conditionally based on the user's role.

<h2 id="securing-routes-based-on-user-roles">Securing Routes Based on User Roles</h2>

In the previous section, we added dynamic client-side navigation based on user roles. However, users may still be able to navigate to unauthorized routes by using the browser navigation. To prevent this, we need to secure the routes directly by adding role-based protection.

We can do this in Svelte by adding a `+layout.server.js` file in each route to run a server-side `load` function. The load function will run whenever the user loads the page and will check whether the user has access to the route based on their role. If they are not authorized, they will be redirected to the `/unauthorized` page.

Let's start by implementing this on the admin page. Head over to the `admin` directory in the `(protected)` folder, create a `+layout.server.js` file with the following code:

```javascript
// src/routes/(protected)/admin/+layout.server.js
import { redirect } from "@sveltejs/kit";

import { checkRole } from "$lib/rbacUtils";
import { ROLES } from "../../../constants";

/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
	const user = locals.user;
	const isAdmin = checkRole(user, ROLES.ADMIN);

	if (!isAdmin) {
		redirect(307, "/unauthorized");
	}
}
```

![Demo for securing admin route](https://res.cloudinary.com/tropicolx/image/upload/v1715948663/Implementing%20Role-Based%20Access%20Control%20in%20SvelteKit/a2d604ab-c976-42fc-9fcb-e4ae936c11bd_sa4ozo.gif?align="center")

Now, if a non-admin user attempts to access the admin route, they will be redirected appropriately.

Next, let's secure our `user` route. Within the `(protected)` folder, navigate to the `user` directory and create a `+layout.server.js` file with the following code:

```javascript
// src/routes/(protected)/user/+layout.server.js
import { redirect } from "@sveltejs/kit";

import { checkRoleAndPermissions } from "$lib/rbacUtils";
import { PERMISSIONS, ROLES } from "../../../constants";

/** @type {import('./$types').LayoutServerLoad} */
export function load({ locals }) {
	const user = locals.user;
	const isCustomerAndCanViewProfile = checkRoleAndPermissions(
		user,
		ROLES.CUSTOMER,
		PERMISSIONS.READ_PROFILE
	);

	if (!isCustomerAndCanViewProfile) {
		redirect(307, "/unauthorized");
	}
}
```

![Demo for securing user route](https://res.cloudinary.com/tropicolx/image/upload/v1715948720/Implementing%20Role-Based%20Access%20Control%20in%20SvelteKit/c3a93ae8-6ec6-496f-ac40-ce6492780265_rqkhwf.gif?align="center")

Here, we're doing things a bit differently by using the `checkRoleAndPermissions` function to ensure the user is both a customer and has permission to read a profile before being able to access the profile page. 

This function can be helpful in more complex scenarios where a role might have additional or custom permissions different from the default permissions.

Next let's look at how to manage role permissions within components.

<h2 id="managing-permissions-within-components">Managing Permissions Within Components</h2>

Now that we've secured our routes, the next step is to safeguard actions and features within those pages to ensure only users with the necessary permissions can access them. 

For instance, you can take three main actions on the admin page:

* Create a product
* Edit a product
* Delete a product
    

![Screenshot of admin dashboard](https://res.cloudinary.com/tropicolx/image/upload/v1715948760/Implementing%20Role-Based%20Access%20Control%20in%20SvelteKit/2ebbcadd-ee6a-43a6-b646-a06a24e08ed5_tqtu2y.png?align="center")

Currently, an admin has the permission to perform all these actions by default. But suppose later in development, a change was made to revoke the `create:products` permission from the default admin role and to only be given to a select few admins. 

In that case, ensuring only those specific admins can create a product becomes crucial.

We can check permissions in our project using the `checkPermissions` utility function. 

For example, in the `+page.svelte` file in the `admin` directory, we can add the following code to ensure only admins with `create:product` permission can access the "Add new product" button:

```javascript
<!-- src/routes/(protected)/admin/+page.svelte -->
<script>
	import { checkPermissions } from "$lib/rbacUtils";
	import { PERMISSIONS } from "../../../constants";
	import { user, products } from "../../../stores";

	$: canCreateProducts = checkPermissions($user, PERMISSIONS.CREATE_PRODUCTS);
</script>

<div class="container mx-auto px-4 md:px-8 py-12">
	<div class="flex flex-wrap items-center justify-between mb-8">
		<h2 class="text-3xl md:text-4xl font-bold">Admin Dashboard</h2>
		{#if canCreateProducts}
			<button
				class="mt-4 min-[457px]:mt-0 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2 bg-gray-900 text-white hover:bg-gray-800"
			>
				Add new product
			</button>
		{/if}
	</div>
	...
</div>
```

If we remove `PERMISSIONS.CREATE_PRODUCTS` from the admin permissions in the `rolePermissions.js` file, we can see the change taking effect:

```javascript
// src/lib/rolePermissions.js
...

export const rolePermissions = {
	[ROLES.ADMIN]: [
		PERMISSIONS.UPDATE_PRODUCTS,
		PERMISSIONS.DELETE_PRODUCTS,
	],
	...
};
```

![Screenshot of admin dashboard with no new product button](https://res.cloudinary.com/tropicolx/image/upload/v1715948810/Implementing%20Role-Based%20Access%20Control%20in%20SvelteKit/4e6442ee-dba7-4b2c-b6a0-de03ce0ade0c_dgigiy.png?align="center")

Let's take things a step further by securing the edit and delete feature as well:

```javascript
<!-- src/routes/(protected)/admin/+page.svelte -->
<script>
	...
	$: canDeleteProducts = checkPermissions($user, PERMISSIONS.DELETE_PRODUCTS);
	$: canUpdateProducts = checkPermissions($user, PERMISSIONS.UPDATE_PRODUCTS);
</script>

...
<tbody class="[&amp;_tr:last-child]:border-0">
	{#each $products as product (product.id)}
		<tr
			class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"
		>
			...
			<td class="p-4 align-middle [&amp;:has([role=checkbox])]:pr-0">
				<div class="flex items-center space-x-2">
					{#if canUpdateProducts}
						<!-- Update button -->
						<button
							class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 w-10"
						>
							<svg
								xmlns="http://www.w3.org/2000/svg"
								width="24"
								height="24"
								viewBox="0 0 24 24"
								fill="none"
								stroke="currentColor"
								stroke-width="2"
								stroke-linecap="round"
								stroke-linejoin="round"
								class="h-4 w-4"
							>
								<path
									d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"
								></path>
							</svg>
						</button>
					{/if}

					{#if canDeleteProducts}
						<!-- Delete button -->
						<button
							class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 w-10"
						>
							<svg
								xmlns="http://www.w3.org/2000/svg"
								width="24"
								height="24"
								viewBox="0 0 24 24"
								fill="none"
								stroke="currentColor"
								stroke-width="2"
								stroke-linecap="round"
								stroke-linejoin="round"
								class="h-4 w-4"
							>
								<path d="M3 6h18"></path>
								<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"
								></path>
								<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"
								></path>
							</svg>
						</button>
					{/if}
				</div>
			</td>
		</tr>
	{/each}
</tbody>
...
```

Next, let's implement the same policy on our profile page. Navigate to the `+page.svelte` file in the `user` directory and add the following code:

```javascript
<!-- src/routes/(protected)/user/+page.svelte -->
<script>
	import { checkPermissions } from "$lib/rbacUtils";
	import { PERMISSIONS } from "../../../constants";
	import { user } from "../../../stores";

	$: canEditProfile = checkPermissions($user, PERMISSIONS.UPDATE_PROFILE);

	let name = $user.name;
	let email = $user.email;
</script>

<div class="h-[calc(100svh-64px)] w-full flex items-center justify-center">
	<div
		class="rounded-lg border bg-card text-card-foreground shadow-sm w-[28rem] max-w-md"
	>
		...
		<div class="p-6 space-y-4">
			<div class="space-y-2">
				...
				<input
					...
					disabled={!canEditProfile}
				/>
			</div>
			<div class="space-y-2">
				...
				<input
					...
					disabled={!canEditProfile}
				/>
			</div>
			<div class="space-y-2">
				...
				<input
					...
					disabled={!canEditProfile}
				/>
			</div>
			<div class="space-y-2">
				...
				<input
					...
					disabled={!canEditProfile}
				/>
			</div>
		</div>
		<div class="flex items-center p-6">
			<button
				...
				disabled={!canEditProfile}
			>
				Save
			</button>
		</div>
	</div>
</div>
```

Here, we use the `canEditProfile` variable to determine whether the input fields and save button will be disabled. If the value is `false`, the input fields and submit button will be disabled and vice versa.

And with that, we should have a fully secured e-commerce web app using RBAC!

![Final RBAC demo](https://res.cloudinary.com/tropicolx/image/upload/v1715948845/Implementing%20Role-Based%20Access%20Control%20in%20SvelteKit/d5b4fa53-3b48-4c98-b47b-c821793d927a_cqtq0a.gif?align="center")

<h2 id="conclusion">Conclusion</h2>

In this tutorial, we looked at how to implement Role-Based Access Control (RBAC) in Sveltekit applications. 

We began by learning about the fundamentals of RBAC, such as roles and permissions and their importance in web development. 

We then examined its implementation, which included configuring custom roles and permissions, integrating RBAC utilities, securing routes, and managing permissions within components.

As you continue developing your SvelteKit apps, remember to regularly review and update your RBAC configuration to adapt to changing requirements and mitigate potential security risks. 

Additionally, prioritize continual monitoring, logging, and testing to maintain the integrity and effectiveness of your RBAC implementation.

You can find the complete project code in this [GitHub Repo](https://github.com/TropicolX/svelte-rbac-demo).