Knock JWT Auth for Rails API + Create React App

This tutorial picks up where my first Rails API + Create React App article left off. Get the code here: https://github.com/Nick-the-BinaryTree/BMS/tree/Part2

I would like to begin by apologizing for all the buzzwords in the title. Seriously, the buzzword to normal word ratio is literally 8 to 2. And I even counted the plus symbol as a normal word.

So you have a Rails API, and it feels great — lightweight and reliably pumping out JSON. However, you don’t just want anyone to get that JSON. You want users and authentication.

First, let’s take a look at the traditional way to do this.

  1. A user logs in.
  2. We create a session in our database for that user.
  3. While the user’s session is active, they can access resources we would otherwise restrict.

This is a good strategy for web apps that need to keep track of state, and there’s a great tutorial on how to create sessions here.

However, keeping track of sessions and who’s who might be a bit much for our Rails API. We want to keep it as simple and stateless as possible: essentially a JSON vending machine.

And what kind of coins do you put into a JSON vending machine? JSON Web Tokens, of course (I’ll stop pushing the analogy now).

Let’s rework the traditional steps to show how JWTs work:

  1. A user logs in.
  2. We send the user a token.
  3. We only send JSON to users who include a valid token in their requests.

Our database doesn’t need to keep track of logged-in users; it just needs to do a quick check to see if their token is fine.

For more info/indoctrination, check out this site.

I think this is an excellent system for authenticating access to our bananas, and knock is the gem for the task.


Let’s start by creating some users.

rails g scaffold user email password_digest admin:boolean

Go check the model this made: app/models/user.rb

Add a line to implement password hashing:

class User < ApplicationRecord
has_secure_password
end

Oh, and to do this, we’ll need the bcrypt gem. To get it, uncomment this line in your Gemfile:

gem 'bcrypt', '~> 3.1.7'

Now run bundle to download the gem (if you get an error, make sure you saved the Gemfile after uncommenting the line — I didn’t haha).

Great, so now we have a user model who’s ready for an encrypted password.

Go ahead, and find the user migration file: db/migrate/###_create_users.rb

Change the line that let’s us know if the user is an admin to be false by default:

t.boolean :admin, default: false

Now run rails db:migrate to create the table.

Awesome, now let’s create some test users. Under, db/seeds.rb add:

admin = User.new
admin.email = 'admin@bananas.com'
admin.password = 'bananaKing'
admin.password_confirmation = 'bananaKing'
admin.admin = true
admin.save
user = User.new
user.email = 'user@bananas.com'
user.password = 'bananaBro'
user.password_confirmation = 'bananaBro'
user.save

Hold on, doesn’t our user table only have a password_digest column? What is this password and password_confirmation business?

Well, that’s where the magic of bcrypt and has_secure_password comes in. It takes those two entries and turns them into a secure, encrypted password_digest.

Run rails db:seed to add these test users to our database. You could also have done this with the rails console method we used to add the bananas, but I like having the info for my fake users displayed in a file.

Okay, now that we actually have some users to authenticate, let’s add authentication.

Add the knock gem to your Gemfile: gem 'knock'

Run bundle, and then set up knock’s user authentication with the following commands:

rails g knock:install

rails g knock:token_controller user

Great, that added a line to our config/routes.rb file that says, post 'user_token' => 'user_token#create'

This means that if a POST request is sent to the /user_token address, it will sent to the user_token controller knock created. If the credentials in the POST request are valid (email and password), knock will use its create method to give us a token.

But there’s an issue. All our routes to this API server are behind /api

No worries, just move the route under the API scope. Also, while we’re here, we probably don’t want exposed routes for our user JSON floating around. So let’s delete resources :users. Your code should look like this:

Rails.application.routes.draw do
scope '/api' do
resources :bananas
post 'user_token' => 'user_token#create'
end
end

Fantastic, now that we have an access point to get a token, let’s start restricting access to resources. Change your app/controllers/application_controller.rb file to give us access to knock:

class ApplicationController < ActionController::API
include Knock::Authenticable
end

Since our resource controllers (for bananas and users) inherit the application controller, they’ll all have access to knock’s methods.

Let’s put that to use.

First, let’s only allow users in our database to see bananas. Add a line to the top of app/controllers/bananas_controller.rb , so it becomes:

class BananasController < ApplicationController
before_action :authenticate_user
before_action :set_banana, only: [:show, :update, :destroy]

:authenticate_user will make sure people making requests to these banana addresses have a valid JSON web token.

Okay, now let’s go back to our front-end and see what it feels like to be forcefully separated from our bananas.

foreman start

I hit “Get Bananas,” and nothing happened.

This is unacceptable.


We need to revamp our front-end and get our bananas back.

Change the render function in client/src/App.js to add a login field:

render() {
return (
<div className="App">
<h1 style={{marginTop: "20vh", marginBottom: "5vh"}}>
Banana Management System
</h1>
<form>
<label htmlFor="email">Email: </label>
<br />
<input
name="email"
id="email"
type="email"
/>
<br /><br />
<label htmlFor="password">Password:</label>
<br />
<input
name="password"
id="password"
type="password"
/>
</form>
<br />
<button
onClick={this.login}
>
Login
</button>
<br />
<button
onClick={this.getBananas}
style={{marginTop: "10vh"}}
>
Get Bananas
</button>
<p>{this.state.bananasReceived}</p>
</div>
);
}

Let’s make that login function send us a token. Add the following method to your App class:

login () {
const email = $("#email").val()
const password = $("#password").val()
const request = {"auth": {"email": email, "password": password}}
console.log(request)
$.ajax({
url: "http://localhost:3000/api/user_token",
type: "POST",
data: request,
dataType: "json",
success: function (result) {
console.log(result)
localStorage.setItem("jwt", result.jwt)
}
})
}

We format our request JSON for knock and then save the token it sends back in our browser’s local storage for later use.

You can check your browser’s developer console to see the token itself. I would probably remove the console.log() statements afterwards.

Now, when we type in the correct credentials for one of our users in the db/seeds.rb file, logging in should send us back a token.

Let’s give it to our broken getBananas method:

getBananas() {
let token = "Bearer " + localStorage.getItem("jwt")
console.log(token)
$.ajax({
url: "http://localhost:3000/api/bananas",
type: "GET",
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', token)},
context: this, // Allows us to use this.setState inside success
success: function (result) {
console.log(result)
this.setState({bananasReceived: JSON.stringify(result)})
}
})
}

We take our token from our browser’s local storage and, using beforesend in the ajax method, modify our GET request’s header to include it as authorization for knock.

Now, if we log in and get a token, our “Get Bananas” button will work again.

But hey. Have you seen all that banana JSON? That’s a lot for a mere user. Maybe we should restrict the button to admins.

Head back to your app/controllers/bananas_controller.rb

Let’s take advantage of the current_user object knock creates for us when we authenticated before with this line: before_action :authenticate_user

Change your index method to use current_user and the built in admin? check Rails gives us when we create a model with an admin boolean:

def index
if current_user.admin?
@bananas = Banana.all
render json: @bananas
end
end

I apologize for the weird formatting in that code block.

Anyway, now if you log in as our normal user (check the db/seeds.rb), you should not be able to hit the “Get Bananas” button.

But that’s a little cruel, and we are a benevolent banana admin, so let’s allow our users to have a single banana. We’ll add a button to get one banana in our client/src/App.js file:

import React, { Component } from 'react'
import './App.css'
// $ is a shortcut for jQuery methods
import $ from 'jquery'
class App extends Component {
constructor(props) {
super(props)
this.state = {bananasReceived: ""}
this.getBananas = this.getBananas.bind(this)
}
login () {
const email = $("#email").val()
const password = $("#password").val()
const request = {"auth": {"email": email, "password": password}}
console.log(request)
$.ajax({
url: "http://localhost:3000/api/user_token",
type: "POST",
data: request,
dataType: "json",
success: function (result) {
console.log(result)
localStorage.setItem("jwt", result.jwt)
}
})
}
getBananas(admin) {
let token = "Bearer " + localStorage.getItem("jwt")
let url = ""
url = admin ? "http://localhost:3000/api/bananas" : "http://localhost:3000/api/bananas/1"
console.log(token)
$.ajax({
url: url,
type: "GET",
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', token)},
context: this, // Allows us to use this.setState inside success
success: function (result) {
console.log(result)
this.setState({bananasReceived: JSON.stringify(result)})
}
})
}
render() {
return (
<div className="App">
<h1 style={{marginTop: "20vh", marginBottom: "5vh"}}>
Banana Management System
</h1>
<form>
<label htmlFor="email">Email: </label>
<br />
<input
name="email"
id="email"
type="email"
/>
<br /><br />
<label htmlFor="password">Password:</label>
<br />
<input
name="password"
id="password"
type="password"
/>
</form>
<br />
<button
onClick={this.login}
>
Login
</button>
<br />
<button
onClick={() => { this.getBananas(false) }}
style={{marginTop: "10vh"}}
>
Get One Banana
</button>
<br />
<button
onClick={() => { this.getBananas(true) }}
style={{marginTop: "2vh"}}
>
Get Bananas
</button>
<p>{this.state.bananasReceived}</p>
</div>
);
}
}
export default App

A few notes:

  1. What’s this? url = admin ? http://localhost:3000/api/bananas : http://localhost:3000/api/bananas/1 A ternary operator is basically a shortened version of an if else statement with ? : syntax. If the admin boolean we pass into our banana request function is true, we go to the URL to get all the bananas. Otherwise, we go to the URL to get just the banana with the id of one.
  2. Why do we use ES6 arrow functions in our button click methods? Well, before we just had this.getBananas, which is fine if you have no parameters; it just points to the function. However, if we changed this to this.getBananas(true), the method would instantly execute and execute again every time the React App component renders (aka constantly). We prevent this by putting a buffer between this.getBananas(true) and onClick=. If onClick points to an arrow function, it won’t immediately execute. Consequently, if we put the desired this.getBananas(true) function call inside our arrow function, it will only execute when clicked.

And there we go. Try logging in as a user; you can only get one banana. Try logging in as an admin; all the bananas are yours.


Now you know how to secure your Rails 5 API and interact with it from the front-end. Excellent work using cutting-edge technology to drive the banana industry forward.

Hope you have a wonderful week.