DEV Community

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

Posted on • Edited 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!