Rails api auth с использованием Grape и Devise JWT

В настоящее время я работаю над разработкой api с использованием grape и devise jwt для аутентификации пользователей.

Настройка devise jwt довольно проста, все, что вам нужно сделать, это просто следовать инструкциям в readme.

с Grape

Установите gems

gem 'grape'
gem 'devise'
gem 'devise-jwt'
Войдите в полноэкранный режим Выйдите из полноэкранного режима

установите драгоценные камни

bundle install
Войдите в полноэкранный режим Выйти из полноэкранного режима

в app/api/api.rb

class Api < Grape::API
  helpers AuthHelpers
  helpers do
    def unauthorized_error!
      error!('Unauthorized', 401)
    end
  end

  mount V1::UserRegistrationApi
end
Войти в полноэкранный режим Выйти из полноэкранного режима

в app/api/auth_helpers.rb

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
Войти в полноэкранный режим Выйти из полноэкранного режима

В настоящее время существует 3 стратегии отзыва, которые предоставляет devise jwt, такие как jtimatcher, denylist и allowlist. но пока я использую стратегию allowlist.

в 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
Войти в полноэкранный режим Выйти из полноэкранного режима

Далее нам нужно сгенерировать модель 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
Войти в полноэкранный режим Выйти из полноэкранного режима

Вам нужно будет предоставить секрет jwt. Вы можете сгенерировать свой собственный секретный ключ с помощью rails, а затем хранить его в месте, где вы сможете получить к нему доступ

bin/rails secret
Войти в полноэкранный режим Выйти из полноэкранного режима

в config/initializers/devise.rb

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
Войти в полноэкранный режим Выйти из полноэкранного режима

в app/api/v1/user_registration_api.rb

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
Войти в полноэкранный режим Выйти из полноэкранного режима

в 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
Войти в полноэкранный режим Выход из полноэкранного режима

в spec/api/v1/user_registration_api_spec.rb

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
Войти в полноэкранный режим Выход из полноэкранного режима

Дополнительные примечания:
Попробуйте это в консоли

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
Вход в полноэкранный режим Выйти из полноэкранного режима

Оцените статью
Procodings.ru
Добавить комментарий