FullStack Labs

Please Upgrade Your Browser.

Unfortunately, Internet Explorer is an outdated browser and we do not currently support it. To have the best browsing experience, please upgrade to Microsoft Edge, Google Chrome or Safari.
Upgrade

Role-Based User Authorization in JavaScript with CASL

Written by 
Decker Brower
,
Senior Software Engineer
Role-Based User Authorization in JavaScript with CASL
blog post background
A Day in The Life of a Software Engineer at FullStack Labs
2020 Software Development Price Guide & Hourly Rate Comparison
How Company Culture Attracts Top IT Talent in Colombia

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.

-- CODE language-javascript keep-markup --
/* 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.

-- CODE language-javascript keep-markup --
/* 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.

-- CODE language-javascript keep-markup --
/* 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.

-- CODE language-javascript keep-markup --
/* 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.

-- CODE language-javascript keep-markup --
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.

-- CODE language-javascript keep-markup --
/* 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.

-- CODE language-javascript keep-markup --
/* 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.

Decker Brower
Written by
Decker Brower
Decker Brower

As a Senior Software Engineer at FullStack Labs I'm focused on building custom software applications using a variety of technologies, including React.js, Ember.js, Ruby on Rails and Node.js. Over the course of my career I've held a variety of roles, including Senior Software Developer at Techmonster, and Web Developer at Markit On Demand.

FullStack Labs Icon

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 $50,000.

company name
name
email
phone
Type of project
Reason for contact
How did you hear about us?
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.