Authentication with Rails, JWT and ReactJS

13 Mar 2015 Development, Ruby On Rails, React

Andrea Pavoni

6 mins
Jwt zoomed logo

Here at Nebulab we love to hack with new exciting technologies, that's why we dedicate 20% of our work time for study and open source contribution. In this post we'll cover authentication made with Rails, JSON Web Tokens and ReactJS.

How JSON Web Token works

JSON Web Token (aka JWT) is a useful standard becoming more prevalent, it allows you to sign information with a signature that can be verified at a later time with a secret signing key. It is a perfect solution for single page apps, avoiding the use of sessions to allow communication between a client (not necessarily a browser) and a server. In its simplest form, JWT is composed by three URL encoded parts:

  • Header: it represents the metadata for the token and contains at least the type of the signature and/or encryption algorithm
  • Claims: is the information you want to sign
  • JSON Web Signature (JWS): it is composed by the header and the claims digitally signed using the algorithm specified in the header

Here is a basic example of how to generate a JWT with plain Javascript:

// Header
var header = {
  "alg": "HS256", // algorithm used for the  signature
  "typ": "JWT"    // the type of token
};

// Claims
var claims = {
  "id": "1234567890",
  "name": "John Doe",
  "email": "[email protected]"
};

// Signature
var payload = base64UrlEncode(header) + "." + base64UrlEncode(claims);
var signature = HMACSHA256(payload, 'my_secret');

// JWT
var myJWT = payload + "." + signature;

The server side

We're using Rails for the server side part, so we installed the jwt gem and wrote some code to integrate it inside our app:

# lib/json_web_token.rb

class JsonWebToken
  class << self
    def encode(payload, exp = 24.hours.from_now)
      payload[:exp] = exp.to_i
      JWT.encode(payload, Rails.application.secrets.secret_key_base)
    end

    def decode(token)
      body = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
      HashWithIndifferentAccess.new body
    rescue
      # we don't need to trow errors, just return nil if JWT is invalid or expired
      nil
    end
  end
end

Authenticating the user with email and password

At this point, we needed two workflows: the first one only happens the first time the user authenticates. Once the user's identity is verified, the JWT is created. Note: We used simple_command gem to manage this part, it helped a lot in keeping Rails controllers and models very slim.

# app/commands/authenticate_user.rb

class AuthenticateUser
  prepend SimpleCommand

  def initialize(email, password)
    @email = email
    @password = password
  end

  def call
    JsonWebToken.encode(user_id: user.id) if user
  end

  private

  attr_accessor :email, :password

  def user
    user = User.by_email(email)
    return user if user && user.authenticate(password)

    errors.add :user_authentication, 'invalid credentials'
    nil
  end
end

Followed by the controller that uses it:

# app/controllers/authentication_controller.rb

class AuthenticationController < ApplicationController
  skip_before_action :authenticate_request

  def authenticate
    command = AuthenticateUser.call(params[:email], params[:password])

    if command.success?
      render json: { auth_token: command.result }
    else
      render json: { error: command.errors }, status: :unauthorized
    end
  end
end
Authentication on following requests

The second part is related to authenticating the requests from client using the JWT we generated during user authentication:

# app/commands/authenticate_api_request.rb

class AuthenticateApiRequest
  prepend SimpleCommand

  def initialize(headers = {})
    @headers = headers
  end

  def call
    user
  end

  private

  attr_reader :headers

  def user
    @user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
    @user || errors.add(:token, 'Invalid token') && nil
  end

  def decoded_auth_token
    @decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
  end

  def http_auth_header
    if headers['Authorization'].present?
      return headers['Authorization'].split(' ').last
    else
      errors.add :token, 'Missing token'
    end
    nil
  end
end

And again, the controller part:

class ApplicationController < ActionController::API
  before_action :authenticate_request

  attr_reader :current_user
  helper_method :current_user

  private

  def authenticate_request
    @current_user = AuthenticateApiRequest.call(request.headers).result

    render json: { error: 'Not Authorized' }, status: 401 unless @current_user
  end
end

As you can see it is very simple and similar to session based authentication. The only difference is the fact that we are dealing with a token instead of a cookie.

The client side

Of course, the browser doesn't know how to deal with JWTs and thus, it needs to store the JWT for its subsequent requests. To make this possible, we relied on a relatively recent feature of HTML5 called localStorage that allows to store key/value pairs on the browser. For the Javascript part we chose ReactJS, but keep in mind that this is possible using plain Javascript and we just wrapped the necessary code inside our app.

Setup

Before proceedeing it's important to highlight the fact that we skipped the default Rails asset pipeline and relied on a stack based on NodeJS, Browserify and Gulp with a bunch of plugins. The main reason is the fact that the NodeJS (and npm) stack is easier, faster and kept updated, without having to deal with wrapper gems. To adapt your rails app is quite easy, just edit config/application.rb and disable the javascript asset generator:

# ...

module JWTReactApp
  class Application < Rails::Application
    # more configs...

    config.generators do |generate|
      # DISABLE ASSET GENERATORS
      generate.javascript_engine false
      # more generator configs...
    end
  end
end

Setup the NodeJS packages we're going to use through package.json:

{
  "name": "my_react_app",
  "version": "0.0.0",
  "description": "ReactJS app",
  "main": "index.js",
  "scripts": {},
  "author": "Andrea Pavoni",
  "license": "MIT",
  "homepage": "",
  "dependencies": {
    "browserify": "^8.1.3",
    "coffee-react": "^2.4.1",
    "coffee-reactify": "^2.1.0",
    "coffee-script": "^1.9.1",
    "gulp": "^3.8.11",
    "gulp-browserify": "^0.5.1",
    "gulp-rename": "^1.2.0",
    "gulp-size": "^1.2.1",
    "gulp-util": "^3.0.4",
    "jquery": "^2.1.3",
    "react": "^0.12.2"
  }
}

Finally a basic a gulpfile.coffee to manage the tasks. These will work like rake in the Ruby world. In our case, it exposes build and watch, which respectively bundles all the javascripts in one file and watches the filesystem for changes to javascripts to rebuild:

gulp = require("gulp")
gutil = require("gulp-util")
browserify = require("gulp-browserify")
rename = require("gulp-rename")
size = require("gulp-size")

JS_SRC='./app/assets/javascripts'
DEST='public'
JS_BUNDLE='app'

gulp.task 'build', ->
  gulp.src("#{JS_SRC}/application.coffee", { read: false })
    .pipe(browserify(
      transform: ['coffee-reactify']
      extensions: ['.coffee']
    ))
    .on("error", gutil.log)
    .pipe(rename("#{JS_BUNDLE}.js"))
    .pipe gulp.dest("#{DEST}/js")
    .pipe(size showFiles: true, title: "Plain JS")

gulp.task "watch", ->
  gulp.watch "#{JS_SRC}/**/*.coffee", ['build']

gulp.task "default", ["build"]

NOTE: For those not confident with CoffeeScript, you can easily translate it into plain Javascript with js2coffee online tool.

The client side, for real

Our initial implementation started with a simple utility library to manage the authentication for the client side:

# app/assets/javascripts/lib/auth.coffee

authenticateUser = (email, password, callback) ->
  $.ajax '/authenticate',
    type: 'POST'
    data: {email: email, password: password}
    success: (resp) ->
      callback(authenticated: true, token: resp.auth_token)
    error: (resp) ->
      callback(authenticated: false)

module.exports =
  login: (email, pass, callback) ->
    if @loggedIn()
      callback(true) if callback
      @onChange true
      return

    authenticateUser email, pass, (res) =>
      authenticated = false
      if res.authenticated
        localStorage.token = res.token
        authenticated = true

      callback(authenticated) if callback
      @onChange authenticated

  getToken: ->
    localStorage.token

  logout: (callback) ->
    delete localStorage.token
    callback() if callback
    @onChange(false)

  loggedIn: ->
    !!localStorage.token

  onChange: ->

As you can see, it is very simple, we have a Javascript object which exposes some methods and helpers. The main one, login, makes an AJAX request with user credentials to the server endpoint. If credentials are correct, it will store the resulting JWT from the server response and stores it on localStorage. The JWT will then be used to make authenticated requests to the server.

Finally we integrated it inside the main application component:

# app/assets/javascripts/application.coffee

React = require 'react'
Auth = require './lib/auth'
# other requires...

App = React.createClass
  getInitialState: ->
    loggedIn: Auth.loggedIn()

  setStateOnAuth: (loggedIn) ->
    @setState loggedIn: loggedIn

  componentWillMount: ->
    Auth.onChange = @setStateOnAuth

  render: ->
    # React's render ...

And here's how we use it for the authenticated requests:

# app/assets/javascripts/notifications_list.coffee

React = require 'react'
Auth = require '../lib/auth'
# other requires...

module.exports = React.createClass
  getInitialState: ->
    return {results: []}

  componentDidMount: ->
    $.ajax '/notifications',
      headers:
        'Authorization': "Bearer #{Auth.getToken()}"
      success: (data) =>
        @setState({results: data.notifications})
  render: ->
    # React's render ...

As you can see, we need to set an authorization token header for our requests. The code is simple for didactic purposes, in a real scenario it might be useful to create a custom helper to append the authentication header token in each request by default.

Conclusions

We've only scratched the surface, but the intent of this article was to offer a gentle introduction. We're happy to listen to your feedbacks and questions, if you want some more in-depth article, please let us know :-)

You may also like

Let’s redefine
eCommerce together.