Role-Based User Authorization in JavaScript with CASL

Written by Decker Brower, Senior Software Engineer

I was recently tasked with implementing role-based user authorization in a client's application and after some research, decided to try a new library that I had never worked with before called CASL.


I enjoyed working with it and would like to show you just how easy it is to set up and start using it in your projects.



What is CASL?


https://github.com/stalniy/casl



CASL (pronounced /ˈkæsəl/, like castle) is an isomorphic authorization JavaScript library which restricts what resources a given user is allowed to access. All permissions are defined in a single location (the Ability class) and not duplicated across UI components, API services, and database queries.


CASL, because of its isomorphic nature, can be used together with any data layer, any HTTP framework, and even any frontend framework.


Let's Do This.


For this example, we'll be defining two roles for our users, Admin and Pleb.


An Admin user will be able to "manage" posts. This means that they can perform any action on the given resource.


A Pleb will be able to read and update posts, but not create or delete them.


Server Side


First, we define a function to create and return an ability for each role.

    
//roles.js
import { AbilityBuilder, Ability } from '@casl/ability'

export const PERMISSIONS = {
  MANAGE: 'manage',
  CREATE: 'create',
  READ: 'read',
  UPDATE: 'update',
  DELETE: 'delete'
}

export const MODEL_NAMES = { POST: 'Post' }

export function defineAbilitiesForAdmin() {
  const { rules, can } = AbilityBuilder.extract()

  can(PERMISSIONS.MANAGE, MODEL_NAMES.POST)

  return new Ability(rules)
}

export function defineAbilitiesForPleb() {
  const { rules, can, cannot } = AbilityBuilder.extract()

  can(PERMISSIONS.MANAGE, MODEL_NAMES.POST) //start with full permissions

  cannot(PERMISSIONS.CREATE, MODEL_NAMES.POST)
    .because('Only Admins can create Posts')
  cannot(PERMISSIONS.DELETE, MODEL_NAMES.POST)
    .because('Only Admins can delete Posts')


  return new Ability(rules)
}
    
  

We could pass some data to our functions to conditionally add/remove abilities for the role but let's keep it simple for now.


Next, let's add a function to fetch the abilities for a user based upon their role.


We'll define a default role with no permissions in case the user doesn't have a role set yet.

    
//utils.js
import { AbilityBuilder, Ability } from '@casl/ability'
import {
  defineAbilitiesForAdmin,
  defineAbilitiesForPleb
} from './roles'

const USER_ROLES = {
  ADMIN: 1,
  PLEB: 2
}

const DEFAULT_ABILITIES = new Ability() //defaults to no permissions

export function getRoleAbilityForUser({ user = {} }) {
  let ability
  switch (user.role) {
    case USER_ROLES.ADMIN:
      ability = defineAbilitiesForAdmin()
      break
    case USER_ROLES.PLEB:
      ability = defineAbilitiesForPleb()
      break
    default:
      ability = DEFAULT_ABILITIES
      break
  }

  return ability
}
    
  

Great! Now we have both our Admin and Pleb roles defined and we have a function to fetch the abilities for a given user.


Let's put them to use by checking if the user has permission to create a post in a controller action.


CASL has a nice ForbiddenError helper that we can use to throw an error with a helpful message if the user doesn't have the correct permissions.

    
//PostController.js
import { ForbiddenError } from '@casl/ability'
import { PERMISSIONS, MODEL_NAMES } from './roles'
import { getRoleAbilityForUser } from './utils'

class PostController {
  async createPost(req, res) {
    const { user = {} } = req

    try {
      const ability = getRoleAbilityForUser({ user })
      ForbiddenError.from(ability)
        .throwUnlessCan(PERMISSIONS.CREATE, MODEL_NAMES.POST)
      //create the post!
    } catch (error) {
      console.log(error.message) // "Only Admins can create Posts"
    }
  }
}

export default PostController
    
  

Finally, let's add a controller action to fetch the ability for the current user so we can check their permissions client-side.


CASL has some helper functions to pack/unpack the rules for the abilities to reduce the size for storage in a jwt token. We'll skip the token part for now but keep the optimization.

    
//controller.js
import { packRules } from '@casl/ability/extra'
import { getRoleAbilityForUser } from './utils'

class UserController {
  getUserRoleAbility(req, res) {
    const { user = {} } = req

    try {
      const ability = getRoleAbilityForUser({ user })
      const packedRules = packRules(ability.rules)
      return res.status(200).send(packedRules)
    } catch (error) {
      //handle the error
      res.status(501).send(error)
    }
  }
}

export default UserController
    
  

Client-Side


We'll use React in this example but we can just as easily use CASL by itself as we did on the server.

    
import { Ability } from '@casl/ability'

const ability = new Ability() //defaults to no permissions

ability.can('create', 'Post') //returns false
    
  

Notice that we are using the same @casl/ability packages on the client as we did on the server!


There are also complementary libraries for other major frontend frameworks which makes integration of CASL super easy in your application.


First, let's add a hook to define and update our users' abilities.

    
//useAbility.js
import { Ability } from '@casl/ability'
import { unpackRules } from '@casl/ability/extra'
import { UserApi } from './api'

const userAbility = new Ability()

export function useAbility() {
  async function fetchUserAbility() {
    try {
      const { data: packedRules } = await UserApi.fetchAbility()
      userAbility.update(unpackRules(packedRules))
    } catch (error) {
      //handle the error
    }

    return userAbility
  }

  return {
    fetchUserAbility,
    userAbility
  }
}
    
  

Now, let's take a look at how to conditionally render a button if the user has the correct permissions to create a new post.

    
//CreatePostButton.js
import { Can } from '@casl/react'
import { useAbility, usePost } from './hooks'

function CreatePostButton() {
  const { fetchUserAbility, userAbility } = useAbility()
  const { createPost } = usePost()
  const [fetched, setFetched] = useState(false)

  useEffect(() => {
    if (!fetched) {
      fetchUserAbility()
      setFetched(true)
    }
  }, [fetched, setFetched, fetchUserAbility])

  //shown for admins, hidden by default and for plebs
  return (
    <Can I="create" a="Post" ability={userAbility}>
      <button onClick={createPost}>Create Post</button>
    </Can>
  )
}
    
  

That's it! Simple and very non-intimidating, right?


Check out the CASL documentation for a deeper dive.


https://github.com/stalniy/casl


Thanks for reading!


---
At FullStack Labs, we are consistently asked for ways to speed up time-to-market and improve project maintainability. We pride ourselves on our ability to push the capabilities of these cutting-edge libraries. Interested in learning more about speeding up development time on your next form project, or improving an existing codebase with forms? Contact us.

Let’s Talk!

We’d love to learn more about your project. Contact us below for a free consultation with our CEO.
Projects start at $25,000.

FullStack Labs
This field is required
This field is required
Type of project
Reason for contact:
How did you hear about us? This field is required