DEV Community

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

Posted 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.

with Grape

Install gems

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

bundle install

in app/api/api.rb
helpers AuthHelpers
helpers do
def unauthorized_error!
error!('Unauthorized', 401)
end
end

mount V1::UserRegistrationApi

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

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

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
Enter fullscreen mode Exit fullscreen mode

end
end

in config/initializers/devise.rb
config.jwt do |jwt|
jwt.secret = Rails.application.credentials.devise_jwt_secret_key!
jwt.expiration_time = 3600
end

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
Enter fullscreen mode Exit fullscreen mode

end
end

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
Enter fullscreen mode Exit fullscreen mode

end
end

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

end
end

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!

An Animated Guide to Node.js Event Loop

>> Check out this classic DEV post <<