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:
- It is really simple to implement
- 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 < 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
Then we send a POST request to the sessions controller in “http://localhost:3000/sessions”
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”
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.