Role-Based User Authorization in JavaScript with CASL

Written by
Written by

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'

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

‍exportconst MODEL_NAMES = { POST: 'Post' }
exportfunctiondefineAbilitiesForAdmin() {  
  const { rules, can } = AbilityBuilder.extract()    
  can(PERMISSIONS.MANAGE, MODEL_NAMES.POST)  

  returnnew Ability(rules)}
exportfunctiondefineAbilitiesForPleb() {  
  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')  

  returnnew 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
exportfunctiongetRoleAbilityForUser({ 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'

‍classPostController {  
  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" */   
    }  
  }
}
‍
‍exportdefault 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'

‍classUserController {    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)    
    }  
  }}

‍exportdefault 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()
exportfunctionuseAbility() {    asyncfunctionfetchUserAbility() {    
    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'
functionCreatePostButton() {  
  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>
      
  )
}

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!

Using techniques like what is listed above, we have had the opportunity to address our clients’ concerns and they love it! If you are interested in joining our team, please visit our Careers page.

---
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.

Frequently Asked Questions