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": "johndoe@example.com"
};
// 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 :-)