DEV Community

Cover image for Rails api auth with Grape and Devise JWT
radin reth
radin reth

Posted on • Updated on

Rails api auth with Grape and Devise JWT

[cover image by Henri Guérin at pixels.com]

I am currently working on developing and api using grape and devise jwt for user user authentication.

Configure devise jwt is pretty straightforward all you need to do is just follow the instruction in the readme.

Install gems

gem 'grape'
gem 'devise'
gem 'devise-jwt'
Enter fullscreen mode Exit fullscreen mode
bundle install
Enter fullscreen mode Exit fullscreen mode

In app/api/api.rb

helpers AuthHelpers
helpers do
  def unauthorized_error!
    error!('Unauthorized', 401)
  end
end

mount V1::UserRegistrationApi
Enter fullscreen mode Exit fullscreen mode

In app/api/auth_helpers.rb

module AuthHelpers
  def current_user
    Warden::JWTAuth::UserDecoder.new.call(
      token, :user, nil
    )
    rescue
      unauthorized_error!
  end

  def token
    auth = headers['Authorization'].to_s
    auth.split.last
  end
end
Enter fullscreen mode Exit fullscreen mode

In app/models/user.rb

class User < ApplicationRecord
include Devise::JWT::RevocationStrategies::Allowlist

devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         **:jwt_authenticatable, jwt_revocation_strategy: self**
end
Enter fullscreen mode Exit fullscreen mode

Generate AllowlistedJwt

bin/rails g model AllowlistedJwt
class CreateAllowlistedJwts < ActiveRecord::Migration[6.1]
  def change
    create_table :allowlisted_jwts do |t|
      t.string :jti, null: false
      t.string :aud
      t.datetime :exp, null: false
      t.references :user, foreign_key: { on_delete: :cascade }, null: false

      t.timestamps
    end

    add_index :allowlisted_jwts, :jti, unique: true
  end
end
Enter fullscreen mode Exit fullscreen mode

In config/initializers/devise.rb

config.jwt do |jwt|
    jwt.secret = Rails.application.credentials.devise_jwt_secret_key!
    jwt.expiration_time = 3600
  end
Enter fullscreen mode Exit fullscreen mode

In app/api/v1/user_registration_api.rb

# frozen_string_literal: true

module V1
  class UserRegistrationApi < Grape::API
    namespace :user do
      namespace :register do
        before do
          @user_mobile_number = UserRegistrationWithMobileNumberService.new params[:mobile_number]
        end

        after do
          header 'Authorization', @user_mobile_number.token
        end

        post do
          @user_mobile_number.register
        end
      end

      put :verify do
        current_user.verify(params[:code])
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

In app/services/user_registration_with_mobile_number_service.rb

class UserRegistrationWithMobileNumberService
  attr_reader :mobile_number, :token

  def initialize(mobile_number)
    @mobile_number = mobile_number
  end

  def register
    user = User.find_or_initialize_by mobile_number: mobile_number
    if user.save
      @token, payload = Warden::JWTAuth::UserEncoder.new.call(user, :user, nil)
      user.on_jwt_dispatch(@token, payload)
      # TODO: UserRegistrationJob.perform_later(user.id)
    end

    user
  end
end
Enter fullscreen mode Exit fullscreen mode

In spec/api/v1/user_registration_api_spec.rb

# frozen_string_literal: true

require 'rails_helper'

RSpec.describe V1::UserRegistrationApi, '/api/v1/user/register' do
  let(:mobile_number) { '01234567' }

  context 'with phone number' do
    it 'creates new user' do
      expect do
        post '/api/v1/user/register', params: { mobile_number: mobile_number }
      end.to change(User, :count).by 1
    end

    it 'responses the new created user' do
      post '/api/v1/user/register', params: { mobile_number: mobile_number }

      expect(json_body).to include mobile_number: mobile_number
      expect(json_body).to include status: 'pending'
      expect(json_body[:code]).to be_present
    end

    it 'responses with jwt authorization token' do
      post '/api/v1/user/register', params: { mobile_number: mobile_number }

      expect(jwt_token).to match /(^[\w-]*\.[\w-]*\.[\w-]*$)/
    end

    context 'when mobile number is already registered' do
      let!(:user) { create(:user, mobile_number: mobile_number)}

      it 'responses with jwt token' do
        post '/api/v1/user/register', params: { mobile_number: mobile_number }

        expect(jwt_token).to match /(^[\w-]*\.[\w-]*\.[\w-]*$)/
      end
    end
  end

  context 'when confirm' do
    before do
      post '/api/v1/user/register', params: { mobile_number: mobile_number }
    end

    context 'with correct code' do
      it 'changes status from pending to confirmed' do
        put '/api/v1/user/verify', params: { code: json_body[:code] }, headers: { 'Authorization': "Bearer #{jwt_token}" }

        expect(json_body(reload: true)[:status]).to eq 'confirmed'
      end
    end

    context 'with wrong code' do
      it 'unable to confirm' do
        put '/api/v1/user/verify', params: { code: 'wrong-code' }, headers: { 'Authorization': "Bearer #{jwt_token}" }

        expect(json_body(reload: true)[:status]).to eq 'pending'
      end
    end

    context 'without authorized jwt token header' do
      it 'responses unauthorized' do
        put '/api/v1/user/verify', params: { code: json_body[:code] }

        expect(response).to be_unauthorized
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Thanks for reading~

Top comments (1)

Collapse
 
santigolucass profile image
Lucas Santiago

You have a good and usefull case here, I just think you should improve the formatting of the post. Thanks for sharing!