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

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. 

# with Grape

Install gems
```ruby
gem 'grape'
gem 'devise'
gem 'devise-jwt'
```

install the gems
```bash
bundle install
```

in app/api/api.rb
```ruby
class Api < Grape::API
  helpers AuthHelpers
  helpers do
    def unauthorized_error!
      error!('Unauthorized', 401)
    end
  end

  mount V1::UserRegistrationApi
end
```
in app/api/auth_helpers.rb
```ruby
module AuthHelpers
  def current_user
    decoder = Warden::JWTAuth::UserDecoder.new
    decoder.call(token, :user, nil)
  rescue
    unauthorized_error!
  end

  def token
    auth = headers['Authorization'].to_s
    auth.split.last
  end
end
```
Currently, there 3 revocation strategies that devise jwt provide such as `jtimatcher`, `denylist` and `allowlist`. but for now I am using `allowlist` strategy.

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
```
Next, we need to generate AllowlistedJwt model
```bash
bin/rails g model AllowlistedJwt
```
Edit the migration file base on your need
```ruby
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
```
you will need to provide jwt secret. you can generate your own secret key using rails then store it somewhere that you can access to it
```csh
bin/rails secret
```

in config/initializers/devise.rb
```ruby
Devise.setup do |config|
# other code
config.jwt do |jwt|
    jwt.secret = Rails.application.credentials.devise_jwt_secret_key!
    jwt.expiration_time = 3600
  end
end
```
in app/api/v1/user_registration_api.rb
```ruby
module V1
  class UserRegistrationApi < Grape::API
    namespace :user do
      namespace :register do
        before do
          @user_mobile_number = UserRegistrationWithMobileNumberService.new params[:mobile_number]
        end

        # set response headers
        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
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(response.headers['Authorization']).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: response.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
```

Additional notes:
Try it out in the console

```bash
newuser = UserRegistrationWithMobileNumberService.new('+85593555115')
registered_user = newuser.register
jwt = registered_user.allowlisted_jwts.last
payload = { jti: jwt.jti, aud: jwt.aud }

User.jwt_revoked? payload, registered_user
User.revoke_jwt payload, registered_user
```