Adding an authentication token system can be a bit confusing the first time around if you are only familiar with session based authentication. In this post I am going to walkthrough adding token authentication to a Rails API using JWT.
JWT (JSON Web Token) is an industry standard method for representing claims to be transmitted between parties. In the case of our Rails API, the back end will generate a JWT to issue to the client front end. The token will then be used to allow access to the API endpoints.
To start we will setup our gemfile and models. First we need to add JWT and bcrypt to our gemfile.
# gemfile
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
# JWT for Auth
gem 'jwt'
Make sure to run bundle afterwards.
Since we don’t have any user model yet I generate a model.
$ rails generate model User email:string password_digest:string
And in the user model add has_secure_password
.
Continuing on with the JWT integration, we create a class Auth that will create and decode tokens using the ‘jwt’ library gem. I put it in the app/services folder.
class Auth
ALGORITHM = 'HS256'
@auth_secret = ENV['AUTH_SECRET']
def self.issue(payload)
JWT.encode(
payload,
@auth_secret,
ALGORITHM
)
end
def self.decode(token)
JWT.decode(token,
@auth_secret,
true,
{ algorithm: ALGORITHM }).first
end
end
ALGORITHM is the type of algorithm you want JWT to use, in this case we are using HS256 which is HMAC SHA-256 (the default). The @auth_secret value is a secret string we have stored in the environment. Running rails secret
in the terminal is a good way of generating a secret value to use here.
The issue method takes a parameter and encodes it using the @auth_secret and ALGORITHM. It then returns a token. We will use this method when a user logs in, passing their id in as a parameter.
The decode method takes a token and decodes it into the related user id. We will use this to authenticate requests on our API endpoints.
Next we update the Application Controller.
class ApplicationController < ActionController::API
before_action :authenticate
def logged_in?
!!current_user
end
def current_user
@current_user ||= User.find(decoded_token["user"]) if auth_present?
end
def authenticate
render json: {error: "unauthorized"}, status: 401 unless logged_in?
end
private
def auth_present?
!!request.env.fetch("HTTP_AUTHORIZATION","").scan(/Bearer/).flatten.first
end
def decoded_token
Auth.decode(token)
end
def token
request.env["HTTP_AUTHORIZATION"].scan(/Bearer (.*)$/).flatten.last
end
end
We have a before_action that runs the method authenticate on any actions in our API. Calling the authenticate method will use the private methods to decode the token passed in the HTTP request using the Auth.decode method we created previously. It then attempts to find the user based on the decoded token. This restricts access to our API endpoints unless the request has a valid token.
We also need to setup the controllers to give the client the proper token at user login.
class SessionsController < ApplicationController
skip_before_action :authenticate, only: [:create]
def create
user = User.find_by(email: auth_params[:email])
if user && user.authenticate(auth_params[:password])
jwt = Auth.issue({user: user.id})
render json: {jwt: jwt}
else
render json: {errors: {"Improper credentials": "Invalid username or password"}}, status: 401
end
end
private
def auth_params
params.require(:auth).permit(:email, :password)
end
end
We create a sessions controller to handle logging a user in and issuing a token. Note that we have a skip_before_action
here relating to the create action. This is so we do not try to authenticate the token on this action. Like most session#create actions, we accept the user login credentials (in this case email and password) through strong parameters to sanitize our input.
We then find the user by email and call the authenticate method on the user, passing in the given password. This uses the bcrypt gem we added at the beginning of this walkthrough to check the given password against the password_digest saved in the database. If the user email and password are correct, we run the Auth.issue method to create a token based on the user.id and send that token back to the client.
Finally we need to create a route for this sessions controller action.
Rails.application.routes.draw do
# other routes...
post '/login', to: 'sessions#create'
end
We expose a /login route that the client can post a user and password to, in order to login and receive a jwt token.