Skip to content

Instantly share code, notes, and snippets.

@jonbarlo
Last active September 17, 2023 16:47
Show Gist options
  • Save jonbarlo/17f911ac49ed9eea55febe544916e210 to your computer and use it in GitHub Desktop.
Save jonbarlo/17f911ac49ed9eea55febe544916e210 to your computer and use it in GitHub Desktop.
How to build rails API w/JWT auth

README

Rails API using JWT as auth framework this will have user with roles

Things we will cover:

  • Project generation

    create a new Rails API-only application

    rails new rails-jwt-api-sample --api

    create the user model

    rails g model User first_name middle_name last_name token email password_digest

    run the migrations

    rails db:migrate

    update Gemfile to include bcrypt

    gem 'bcrypt'

    and install it

    bundle install

    lets include and use bycrypt on our model

    app/models/user.rb

    class User < ApplicationRecord
        has_secure_password
    end
    

    update Gemfile to include JWT

    gem 'jwt'

    and install it

    bundle install

    write a singleton class for wrapping the JWT logic through its global variable

    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
            nil
            end
        end
    end
    

    include the lib directory when the Rails application loads

    that way we will force the use of our new singleton class

    config/application.rb

    add this line

    config.autoload_paths << Rails.root.join('lib')

    update Gemfile to include simple command is similar to the role of a helper, but instead of facilitating the connection between the controller and the view, it does the same for the controller and the model

    gem 'simple_command'

    and install it

    bundle install

    create new folder commands under app main directory

    create new command has to take the user's e-mail and password and return the user if the credentials match

    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.find_by_email(email)
        return user if user && user.authenticate(password)
    
        errors.add :user_authentication, 'invalid credentials'
        nil
    end
    end
    

    create new command to check if a token that's been appended to a request is valid

    app/commands/authorize_api_request.rb

    class AuthorizeApiRequest
    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
    

Implementing helper methods into the controllers

Create new controller for logging in users

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

create a new endpoint for this action/controller

config/routes.rb

post 'authenticate', to: 'authentication###authenticate'

we need to expose the current_user to 'persist' in order to have current_user available to all controllers, it has to be declared in the ApplicationController

app/controllers/application_controller.rb

class ApplicationController < ActionController::API
 before_action :authenticate_request
  attr_reader :current_user

  private

  def authenticate_request
    @current_user = AuthorizeApiRequest.call(request.headers).result
    render json: { error: 'Not Authorized' }, status: 401 unless @current_user
  end
end

create user roles

update the user model to have enum with roles

app/models/user.rb

enum access_level: [:sales, :admin, :operations]

update active record migration

add this right below password_digest

t.integer :access_level

update authenticate user command to encode and response back the logged user's role

app/commands/authenticate_user.rb

update call method to send email and user_access_level or role

JsonWebToken.encode(user_id: user.id, user_email: user.email, user_access_level: user.access_level) if user

update database seed file to add users for testing

db/seeds.rb

admin = User.create!(email: "admin@mail.com" , password: "123456" , password_confirmation: "123456", access_level: :admin, first_name: "John", middle_name: "Daniel", last_name: "Doe")

let's (not...lol, as you will see we will perform each task separately) scaffold a resource so see how authorizarion works

create the model

rails g model Product name:string description:text

create controller

rails g controller Product

modify controller to display all products only after authentication

before_action :authenticate_request
def index
    @products = Product.all
    render json: @products
end

add new route

get 'produtcs', to: 'produtc#index'

update database seed file to add products for testing

db/seeds.rb

Product.create!(name:"Asimo", description:"Honda's first robot")

reload updated migration for user

    rails db:drop
    rails db:create
    rails db:migrate
    rails db:seed

now run the server and have fun!

  • Ruby version

  • System dependencies JWT - JWT official website

  • Configuration

  • Database creation

    rails db:drop
    rails db:create
  • Database initialization
    rails db:migrate
    rails db:seed
  • How to run the web api rails server -p 3110

  • How to test the web api

api call using curl

    curl -H "Content-Type: application/json" -X POST -d '{"email":"admin@mail.com","password":"123456"}' http://localhost:3110/authenticate

take note of the JSON response

{"auth_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX2VtYWlsIjoiYWRtaW5AbWFpbC5jb20iLCJ1c2VyX2FjY2Vzc19sZXZlbCI6ImFkbWluIiwiZXhwIjoxNTE4NzM1ODYyfQ.HowwXLphSllo6hDK_t6hW4rq3hadSmxIFJA2D15KYCg"}

lets test our endpoint is not accessible without an auth token

curl http://localhost:3110/products
{"error":"Not Authorized"}

now lets give a try with the recently generated token

curl -H "Authorization: TOKEN_HERE " http://localhost:3000/products

now use the token

curl -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX2VtYWlsIjoiYWRtaW5AbWFpbC5jb20iLCJ1c2VyX2FjY2Vzc19sZXZlbCI6ImFkbWluIiwiZXhwIjoxNTE4NzM1ODYyfQ.HowwXLphSllo6hDK_t6hW4rq3hadSmxIFJA2D15KYCg" http://localhost:3110/products

response will show all products

[{"id":1,"name":"Asimo","description":"Honda's first robot","created_at":"2018-02-14T22:44:24.204Z","updated_at":"2018-02-14T22:44:24.204Z"},{"id":2,"name":"Mind Storm","description":"Llego's IoT for kids","created_at":"2018-02-14T22:44:24.207Z","updated_at":"2018-02-14T22:44:24.207Z"},{"id":3,"name":"Raspberry Pi","description":"Micro controller","created_at":"2018-02-14T22:44:24.209Z","updated_at":"2018-02-14T22:44:24.209Z"}]
  • Services (job queues, cache servers, search engines, etc.)

  • Deployment instructions

If you missed something, the project has been uploaded on GitHub. If you have any questions, do not hesitate to ask in the comments or feel free to message me on Github.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment