Skip to content

Rodrigo Urubatan – About Code

Helping ruby developers to use the best tools for each job so they can solve hard problems, with less bugs and have more free time.

Menu
  • Home
  • My last presentations
  • About
  • Privacy Policy
Menu

5 easy steps to implement JWT (Javascript Web Token) authentication for your API Only Rails apps

Posted on 2018-07-22 by Rodrigo Urubatan

If you are writing an API only rails app, your clients will need to authenticate in some way to use your API, you have of course many options to choose from:

  • You can build your own authentication
  • You can Use a plugin like Devise
  • You can use OAuth both accepting other provider or implementing your own
  • You can use JWT

Currently my choice is to use JWT for two main reasons:

  1. It is really simple to implement
  2. The authentication token is already CSRF safe (even better if you are running your API over SSL as you should)

You can start from our last post about writing an API only Rails app, using that same “empty” app, I’ll create a User model, and a sessions controller as bellow:

 

1 – Create User Model and auth controller

rails g model user password_digest:string username:string
rails g controller sessions

 

2 – Configure your Gemfile

Then we’ll add the ‘jwt’ gem to you Gemfile and uncomment “bcrypt” gem the like this:

source 'https://rubygems.org'
 
git_source(:github) do |repo_name|
  repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
  "https://github.com/#{repo_name}.git"
end
 
 
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.1.5'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
# Use Puma as the app server
gem 'puma', '~> 3.7'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
# gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
 
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
 
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
# gem 'rack-cors'
 
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end
 
group :development do
  gem 'listen', '>= 3.0.5', '< 3.2'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end
 
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
gem 'jwt'

3 – Adjust routes

We’ll adjust the authentication route in the routes.rb

Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  resources :sessions
end

4 – implement your “authenticate” method

Then we’ll add the “has_secure_password” and validate the presence of the username field for the User model:

class User < ApplicationRecord
  has_secure_password
  validates :username, presence: true
end

5 – Write the actual JWT code for authentication and token validation

Now we can write the actual login code adding a “create” method to the sessions_controller.rb

class SessionsController < ApplicationController
  skip_before_action :require_user
 
  def create
    @user = User.find_by(username: params[:username]).try(:authenticate, params[:password])
    if @user
      token = JWT.encode({user_id: @user.id, authentication_date: Time.now}, Rails.application.secrets.secret_key_base)
      render json: {success: true, token: token}
    else
      render json: {success: false}
    end
  end
end

As you can see, the JWT library has an encode method that receives a hash and a secret, we can add any information we want in the hash, and we can also save the current token for the user, now allowing two concurrent tokens, …

The possibilities are endless. depending on our application requirements.

To save some work, we already added the “skip_before_action” for the before_action we didn’t create yet, but lets fix this, and create the authentication filter in the application_controller.rb

class ApplicationController < ActionController::API                                                  
  before_action :require_user                                                                        
 
  def current_user                                                                                   
    @user                                                                                            
  end                                                                                                
 
  def require_user                                                                                   
    token = request.headers['jwt-token']                                                             
    hash = JWT.decode(token, Rails.application.secrets.secret_key_base)[0] rescue nil                
    if hash && is_valid(hash)                                                                        
      @user = User.find hash["user_id"]                                                              
      render status: :unauthorized unless @user                                                      
    else                                                                                             
      render status: :unauthorized                                                                   
    end                                                                                              
  end                                                                                                
 
  def is_valid(hash)                                                                                 
    puts hash.inspect                                                                                
    hash["authentication_date"] > 3.days.ago                                                         
  end                                                                                                
end

In the application controller, the only validation we added for the token was checking if it was created in the last 3 days.

What I usually do in production applications is to save the tokens in the database, allowing the invalidation of a token.

How many “active” tokens one user can have is up to your business logic to decide.

Now you can test it!

After this, we can create a “users” controller to list the registered users and test our app with this command:

rails g controller users index

And this completely unsecure code

class UsersController &lt; ApplicationController
  def index
    render json: User.all
  end
end

And if you create a user using rails console, and remember to run these simple commands:

bundle install
rails db:migrate
rails s

You can now use something like Advanced REST Client to test your newly created app supporting JWT for authentication.

As you can see, in your first request to “http://localhost:3000/users/index” you get an “Unauthorized” response

Unauthorized request without authentication token

 

Then we send a POST request to the sessions controller in “http://localhost:3000/sessions”

Login request to the sessions controller

 

We get the token from the last response and add it as a header named “jwt-token” in the first call, and now it works”

Successful request sending the jwt-token

Summary

As you can see, you can add JWT support to your application very easily, the token replaces the CSRF token, and you can embed any kind of information to validate the authenticity of the token.

The client does not need to know your encryption secret, only the server that generated the token can decrypt it, so it is really secure.

But never forget, that any API should run through HTTPS preventing any sniffer from hijhacking your token from the requests.

And just to finish, if you have any questions about this, or any comment about this implementation, please leave a comment bellow or contact me by email.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recent posts

  • I see Dead Jobs everywhere (sidekiq DeadSet)
  • Quick tips that help: rails notes
  • Ruby 3.2.0 released with WASI, YJIT, RegExp improvements, …
  • Rubyconf Thailand quick summary and slides
  • Testing download contents with Cucumber+Capybara and chromedriver

Comments

  1. When Kubernetes is not the right choice? | Rodrigo Urubatan - About Code on Rails from “zero” to kubernetes – first pod
  2. When Kubernetes is not the right choice? | Rodrigo Urubatan - About Code on How to use docker to have an uniform development environment for your rails project
  3. Rails from "zero" to kubernetes – ingress reverse proxy | Rodrigo Urubatan - About Code on Rails from “zero” to kubernetes – a service to access your pod
  4. Rails from "zero" to kubernetes – horizontal autoscaling | Rodrigo Urubatan - About Code on Rails from “zero” to kubernetes – a service to access your pod
  5. Jeronimo on 6 Lessons From CrossFit That will help your developer career (Or any other career in the matter of fact)

Arquives

  • February 2023
  • January 2023
  • December 2022
  • June 2021
  • March 2020
  • January 2020
  • July 2019
  • June 2019
  • May 2019
  • October 2018
  • September 2018
  • August 2018
  • July 2018
  • June 2018
  • May 2018
  • February 2018
  • January 2018
  • November 2017
  • August 2015
  • August 2014
  • July 2014
  • August 2007

Categories

  • articles
  • cfp
  • firebase
  • gems
  • git
  • opinion
  • presentations
  • projects
  • rails6
  • ruby
  • Sem categoria
  • server-api
  • Uncategorized
© 2023 Rodrigo Urubatan – About Code | Powered by Minimalist Blog WordPress Theme