Rails API + JWT auth + VueJS SPA

Julija Alieckaja
8 min readJun 17, 2018

This is a quick guide designed to help newbie developers to build VueJS SPA with authenticated API calls.

We’ll start with building an API-first REST-ful Rails backend. API-first means that the same API endpoints can be used by different Web/JS clients, mobile applications, 3rd party APIs, and ideally all of them should use a unified auth flow and JWT is a good fit for this goal. While in this article we’ll consider the creation of a single VueJS client, the API itself can be easily adapted to be reused by other API-consumers.

Tools for the backend:
Rails 5.2.0,
Ruby 2.4.4,
gem bcrypt 1.3.12,
gem jwt_sessions 2.1.0,
gem redis 4.0.1,
gem rack-cors 1.0.2

We are not tied up with this exact versions, I’m simply listing the ones I have installed on my local environment. Now, let’s build the classic todo app.

The Backend

  1. Create a rails app rails new silver-octo-invention --api -T. The fancy project name is auto-generated by GitHub.-T option excludes Minitest, the default testing framework. We’ll be using RSpec .
  2. Adjust Gemfile to look somewhat like this

3. Run bundle install

4. Run rails generate rspec:install . This command generates

.rspec
spec/spec_helper.rb
spec/rails_helper.rb

5. Now, let’s create the User model. We’ll start with the minimum required model fields rails g model user email:string password_digest:string

6. Add null: false settings to the migration strings.

7. Run rails db:migrate

8. Add has_secure_password method call to the User model.

9. Now, let’s create todos. Run rails g model todo title:string user:references && rails db:migrate

10. Let’s build the controllers. First, we’ll need to include JWTSessions::RailsAuthorization into ApplicationController. The module provides authorization actions which will protect secure endpoints.

11. We’ll need exception handlings for unauthorized requests, so let’s add it right away.

12. By the way, in order to use JWT we need to specify the initial configuration. JWTSessions gem by default it uses HS256 algorithm, and it needs an encryption key provided.
Also, on default the gem uses redis as a token store, so you’ll need a working redis-server instance. However, there’s a possibility to select memory as a token store (can be useful in test env). More info about specific settings can be found in the README.

13. Now, let’s create a signup endpoint. With token-based sessions and SPA we have 2 most common options of where to store the tokens on the client — cookies and localStorage. It’s up to developer to decide where they are going to store the tokens. While making the decision, keep in mind — cookies are vulnerable to CSRF and localStorage is vulnerable to XSS attacks. CSRF vulnerability is solvable - I usually prefer http-only cookies as the most secure token store.
jwt_sessions gem itself provides the set of tokens — access, refresh, and CSRF for the cases when cookies are chosen as the token store. With this being said, let’s use cookies together with the CSRF token provided by the gem (the gem automatically manages the CSRF validations when JWT is passed by request cookies).
The session within the gem is represented as a pair of tokens — access and refresh. Access token has a short life span (default 1 hour), and refresh has a relatively long life span (2 weeks). Expiration times are configurable. Refresh token is used to renew the access once it’s expired.
While it makes sense to pass refresh token to external API services or mobile applications — JS clients are usually not secure enough to store the precious refresh token. It’s up to developer to decide which info to pass or not to pass to JS. The jwt_sessions gem provides the possibility to issue a new access token by passing the old expired one, so we can avoid passing the refresh token to JS client. As both refresh and access tokens are linked to each other it will be easy to detect if the access has been stolen from the JS client and flush the leaked session (2 users —the original user and the attacker eventually will have 2 different access tokens pointing to the same refresh token).
Now for real, let’s create the signup endpoint. The endpoint must create users, assemble the JWT payload, and pass it via cookies with the response as well as the CSRF token via response body.

Specs to ensure the signup works.

14. Now we can build a sign in controller.

And specs for signin.

15. Here goes the refresh endpoint. As we’re building an endpoint for web client — we’ll be renewing a new access with the old expired one. Later we can create a separate set of endpoints to be used by other API-consumers (mobile/etc) which will operate via refresh tokens, but for this case we’re not going to risk it and to show the refresh token to the cruel outer world.
We’re expecting only expired access tokens to be used for refresh, so within the refresh_by_access_payload method a block is passed with unauth exception. Optionally, within the block we can notify the support team, flush the session or skip the block and ignore this kind of activity.
JWT library automatically checks expiration claims, and to avoid the exception for an expired access token we’ll be using claimless_payload method.

Refresh specs:

16. Almost there. This is the time to build the todos controller.

To make it work we’ll also need current_user, so let’s add it.
After the token is authorized we can dig into the payload and fetch whatever we decided to store within. In our case it’s user_id .

Generic todos specs:

17. We’re almost set with the API, it’s possible to sign up, to sign in, to refresh an access_token and to manage todos. But while we’re here, let’s also add ability to log out.

18. To allow JS client to send requests to the API we’ll need to set up CORS.

Routes configuration:

The Frontend

In this guide we’ll develop a stand alone front end application.

Tools for the frontend:

Node.js
Node.js Version Manager — NWM
Node.js Package Manager — NPM
VueJS CLI
Axios

$ node -v
v10.4.1
$ npm -v
6.1.0
  1. Install the Vue CLI.
$ npm install --global vue-cli

2. Initialize the application.

$ vue init webpack todos-vue

This command is going to ask us a couple of questions.

? Project name todos-vue
? Project description Todos Vue.js application
? Author Yulia Oletskaya <yulia.oletskaya@gmail.com>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner karma
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm

3. Start the application

$ cd todos-vue
$ npm run dev

And now it’s live on http://localhost:8080

VueJS HelloWorld component

4. Delete auto-generated HelloWorld component and create a new Signin.vue . The app directory should look similar to this one.

$ tree . -I "node_modules"
.
├── README.md
├── build
│ ├── build.js
│ ├── check-versions.js
│ ├── logo.png
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ ├── webpack.prod.conf.js
│ └── webpack.test.conf.js
├── config
│ ├── dev.env.js
│ ├── index.js
│ ├── prod.env.js
│ └── test.env.js
├── index.html
├── package-lock.json
├── package.json
├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── Signin.vue
│ ├── main.js
│ └── router
│ └── index.js
├── static
└── test
└── unit
├── index.js
├── karma.conf.js
└── specs
└── Signin.spec.js

5. Let’s add stylesheet links to /index.html

6. Install axios within todos-vue dir.

$ npm install --save axios vue-axios

Prepare configuration files.

$ tree src/backendsrc/backend
└── axios
└── index.js

Define configuration and make Vue use the axios config.

7. Build Signin component. We are receiving cookies with an access token from the server response and they will be handled by the browser. In the response body we’ll receive CSRF. The CSRF token can be stored right in the localStorage, as we don’t really care even in case it’ll be stolen by a sudden XSS attack.

Visualisation:

8. To manage all the access/refresh auth we’ll need to build a custom wrapper for axios. The desired flow is following:

  • JWT is stored within the HTTP-only cookie;
  • For all requests except OPTIONS and GET we must add CSRF token to the request headers;
  • Once JWT is expired the next API request with this cookie returns 401;
  • At this step we should handle Unauthorized response code and make a refresh request with the expired access token for a new cookie;
  • If the request is successful we must retry the original request, otherwise throw up the 401;

Actually, we’ll need to build even 2 axios wrappers. First one will use interceptors to handle 401s (secure axios instance) and the second one won’t have any special retry listeners (plain axios instance), but will perform refresh and retry requests — it’s needed in order to avoid endless loops.
As you might be noticed we’re already using the plain one on Signin as we do not need to retry requests on failed signins/signups:

this.$http.plain.post(‘/signin’, { email: this.email, password: this.password })

The router configuration:

As the cookie is HTTP-only it’s not easy to detect by JS if it’s present or not. Instead, we’ll be using a simple signedIn flag. Some people might say that the flag can be easily changed in the browser console and so unlogged user will have an access to “secure” URLs. Indeed, the flag can be modified manually. But since it’s only used to prevent redirects to login page, it doesn’t break security anyhow as in case of an attempt to retrieve/update a secure resource without a valid cookie, user will be redirected to the login page anyway.

9. Now, let’s build Signup component. It’s very similar to Signin.

A bit of visual representation

10. Finally, we can build a todos component. The implementation is pretty naive, the main goal here is to demonstrate the auth flow with basic CRUD.

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

Routes.

Visualisation.

Fancy trash icon appears on hover, it’s important, and requires an additional screenshot.

11. And the last thing — sign out button. Let’s do this.

Visualisation

Wohoo, that’s it, now we have a completely functional secure CRUD VueJS SPA application.

Thanks for reading!
The application itself is available on GitHub.
I plan to continue the series, add admin panel, user roles, extend usage of JWT claims, add permissions to the payload - stay tuned.
UPD:
The 2nd part of the series.
The 3rd part of the series.

📝 Read this story later in Journal.

🗞 Wake up every Sunday morning to the week’s most noteworthy Tech stories, opinions, and news waiting in your inbox: Get the noteworthy newsletter >

--

--