Rails API + JWT auth + VueJS SPA: Part 2, Roles

Julija Alieckaja
6 min readJun 25, 2018

--

In the previous article we’ve built a simple Todos application. Users of this application are able to sign up, sign in, sign out and to manage their own lists of todos.

This time we’re going to extend the functionality and add a few more features.

Acceptance criteria:
— User model should allow 3 types of roles: Admin, Manager and User (default).
— Admins and Managers should have an access to an admin panel.
— Within the admin panel Admins and Managers should be able to view the list of all app users.
— Within the admin panel only Admin users should be able to view a todo list of a selected user.

The application itself can be found on GitHub.

The Backend

  1. First, we’ll specify allowed roles list within User model.

2. Add a role attribute to User model

$ rails generate migration add_role_to_users role:string

3. Set user role as the default value within the db migration.

4. Run rake db:migrate

5. Now we can add roles to the tokens payload. The general downside of using HTTP-only cookies as a token store — we cannot read the token’s payload on the frontend. We still can read the payload on the backend though, when receiving auth token from the web clients and thus prevent some db hits, as we can fetch and validate the authorization data from the token itself.
JWT standard provides a set of standard claims that can be used for token verification:

iss — Issuer. Identifies principal that issued the JWT.
sub — Subject. Identifies the subject of the JWT.
aud — Audience. Identifies the recipients that the JWT is intended for. This one can be used to specify the set of accepted roles.
exp — Expiration time.
nbf — Not before. Identifies the time on which the JWT will start to be accepted for processing.
iat — Issued at. Identifies the time at which the JWT was issued.
jti — JWT ID. Case sensitive unique identifier of the token even among different issuers.

If we’re talking about role-granted permissions verification, although we can use totally custom keys in the payload f.e. role key and verify it via Pundit , ActionPolicy or any other authorization solution, in this tutorial I’d like to show how to work with the standard JWT claims. But just for the sake of the interest later in the article I’ll show really quick the Pundit way as well.
Let’s update signin and signup controllers and extended the payload to include aud claim with the user’s role — payload = { user_id: user.id, aud: [user.role] }.

4. As the frontend cannot read the data from the payload, we must pass user data explicitly from the backend. Let’s build a separate endpoint for this matter (in real life I’d use a serializer in the controller, but I’m too lazy to build it within this example).

And a simple spec to ensure it works.

5. Now we can add users controller to the admin space of the API. We must specify token_claims method in order to let JWT know which claims we’d like to verify. On default it checks only the expiration claim. We’re going to add list of allowed roles — Admin and Manager, and verify them within aud claim.
The extremely cool part of it — all the authorization flow is performed without a single database query.

With the roles support we should handle 403 errors within the application controller.

Specs. Btw, those are demonstration specs, no need to test auth on each controller within the application.

6. And the last one — todos controller, only users with Admin role are allowed to view todos.

Specs.

The Frontend

1. Let’s add Vuex and Vuex-Persistedstate in order to manage the SPA state in a more convenient way.

$ npm install vuex --save
$ npm install vuex-persistedstate

2. Now we should fetch and store current user within the Vue store. Once we have the store, we can go ahead and move all the auth data to the store as well for the sake of consistance. But first, let’s create the store.

$ tree src
src
├── App.vue
├── backend
│ └── axios
│ └── index.js
├── components
│ ├── Signin.vue
│ ├── Signup.vue
│ └── todos
│ └── List.vue
├── main.js
├── router
│ └── index.js
└── store.js

3. Update routers to use the data from the store.

4. Now let’s add current user fetching to Signin/Signup components and adjust the rest of the code to use the data from the store.

5. Extract sign out link into a header component and add a link to the admin space, visible to Admins and Managers only.
To achieve it we’ll add isAdmin and isManager getters to the store. Vue allows to use more agile solutions to manage roles and permissions, but as it’s not the main goal of the article we’ll not focus on it too much.

$ tree src/componentssrc/components
├── AppHeader.vue
├── Signin.vue
├── Signup.vue
└── todos
└── List.vue

Phew, a lot of work is done and all is to be able to show a tiny Admin link in the header for the specific users.

6. The link itself isn’t even pointing anywhere, so let’s fix that and add admin space component with users list.

$ tree src/componentssrc/components
├── AppHeader.vue
├── Signin.vue
├── Signup.vue
├── admin
│ └── users
│ └── List.vue
└── todos
└── List.vue

And while we’re here let’s also add Home link to the header.

Yay, a fresh admin component is here. Note, the right column with todos icon is visible for Admin users only.

7. And finally, the last one component — todos per user.

$ tree src/components/adminsrc/components/admin
└── users
├── List.vue
└── todos
└── List.vue

Visualisation.

And done! All acceptance criteria are fulfilled.
Here are few fancy gifs showing interactions with the app on behalf of users with different roles.

Admin

Visiting todos page, creating a new todo and deleting it, then visiting admin space, viewing the list of users and their todos.

Manager

Visiting todos page, creating a new todo and deleting it, then visiting admin space, viewing the list of users.

User

Visiting todos page, creating a new todo, then editing this todo and deleting it.

And as being promised, here’s a quick sample of Pundit policy using JWT payload for authorization. In the example we’re replacing pundit_user with the payload hash.
Payload is customizable, so it can contain specific permissions like { can_view_secret_resource: true}, which otherwise would require heavy db queries.

In the next part, hopefully final, I’ll show how to edit user roles and reset forgotten passwords, how to properly reset tokens and sessions in case of a role change or user deletion.
Thanks for reading!

UPD: 3rd Part of the series.

--

--