Rails api auth with Grape and Devise JWT
Tags: rails, grape, jwt
Published: 2022-05-03 09:15:06

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

I am currently working on developing and api using [grape](https://github.com/ruby-grape/grape) and [devise jwt](https://github.com/waiting-for-dev/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
```ruby
gem 'grape'
gem 'devise'
gem 'devise-jwt'
```

```zsh
bundle install
```

### In app/api/api.rb
```ruby
helpers AuthHelpers
helpers do
  def unauthorized_error!
    error!('Unauthorized', 401)
  end
end

mount V1::UserRegistrationApi
```

### In app/api/auth_helpers.rb
```ruby
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
```ruby
class User < ApplicationRecord
include Devise::JWT::RevocationStrategies::Allowlist

devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         **:jwt_authenticatable, jwt_revocation_strategy: self**
end
```

### Generate AllowlistedJwt
```ruby
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
```

### In config/initializers/devise.rb
```ruby
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
```ruby
# 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
```

### In app/services/user_registration_with_mobile_number_service.rb
```ruby
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
```

### In spec/api/v1/user_registration_api_spec.rb
```ruby
# 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
```

Thanks for reading~