Authentication with JWT
Psst, this is a continuation of my previous blog post, Invalid Authenticity Token.
JSON Web Tokens, or JWT, are a modern solution to transferring secure information between parties. Each token is signed using the unique identifier of the sender, cross-checking for any alterations.
JWT saved me from having to rewrite an entire frontend in Rails just to ensure user security. All I had to do was implement it with an expiration to guarantee further protection. Below, I’ll go through how I implemented the gem!
Installing
Installation is simple — just add gem 'jwt'
to your Gemfile and run bundle install
in your terminal.
Backend
Sessions Controller
- Create a
SessionsController
and make sure to set the HMAC secret and algorithm type so that we can use this in the future. - We have two methods using JWT,
encode
anddecode
.encode
takes in auser_id
argument, creates a payload hash, and uses the built-in JWTencode
method to set the payload, the secret, and algorithm type that we set earlier.decode
takes in a token and uses it with the built-in JWTdecode
method. Since the information we want is nested, thedecoded_token[0][‘user_id’]
line retrieves from the actual token. - In the create method, we can then use the encode method we defined in
token = SessionsController.encode(user.id)
.
class SessionsController < ApplicationController HMAC_SECRET = ENV['HMAC_SECRET'] ALGORITHM_TYPE = 'HS256' def self.encode(user_id) payload = {user_id: user_id} JWT.encode(payload, HMAC_SECRET, ALGORITHM_TYPE) end def self.decode(token) decoded_token = JWT.decode(token, HMAC_SECRET, true, {algorithm: ALGORITHM_TYPE}) decoded_token[0]['user_id'] end def create user = User.find_by(email: session_params[:email]) if user && user.authenticate(session_params[:password]) token = SessionsController.encode(user.id) session[:username] = user.username session[:token] = token session[:user_id] = user.id render json: { logged_in: true, session: session } else render json: { logged_in: false, error: "Unable to authenticate user." } end end def destroy session.clear end
private def session_params params.require(:user).permit(:email, :password) endend
Problems Controller
- In the
ProblemsController
, we need to include theActionController::HttpAuthentication::Token
module. - Now we can use
before_action
to authenticate the user before logging them in and creating a new session.
class ProblemsController < ApplicationController include ActionController::HttpAuthentication::Token before_action :authenticate_user, only: [:create] def index render json: ProblemSerializer.new(Problem.all) end def create problem = Problem.create(problem_params) if problem.save render json: ProblemSerializer.new(problem) else render json: {error: "Problem did not save."} end end def show problem = Problem.find(params[:id]) if problem render json: ProblemSerializer.new(problem) end end private
def authenticate_user token, _options = token_and_options(request) user_id = SessionsController.decode(token) User.find(user_id) rescue ActiveRecord::RecordNotFound render json: {authorized: false} end def problem_params params.require(:problem).permit(:title, :description, :user_id) endend
Frontend
Problem.js
- In the frontend, you can authenticate the user each time a fetch request is sent in using the
“Authorization”: `Bearer ${sessionStorage.token}`
header. Below, I’m authenticating a user before they are able to create a new Problem.
const obj = { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", "Accept": "application/json", "Authorization": `Bearer ${sessionStorage.token}` }, body: JSON.stringify({problem: {title: title, description: description, user_id: userId}})}fetch("http://localhost:3000/problems", obj) .then(response => response.json()) .then(jsObj => { let newProblem = new Problem(jsObj.data) newProblem.renderProblem() }).catch(error => { console.log(error) alert(error)})