Autenticação JWT em uma Api Rest Ruby On Rails
Tags: jwt, rest, rails, ruby
Published: 2023-06-22 00:42:04

Neste artigo vou mostrar como implementar um autenticação jwt seguindo uma arquitetura um pouco fora da curva do que as docs do rails ensina.

Eu sou uma pessoa em processo de adaptação ao rails, então procurando por tutoriais na internet quase sempre me deparo apenas com exemplos simples, onde tudo é resolvido nos controllers, algo que me incomoda muito pois eu sei que conforme o projeto cresce as regras de negócio passam a ser muito mais complexas que um simples MVP de um blog, logo logo esses controllers vão ficar enormes, resolvi fazer uso então de services para guardar as regras de negócio como veremos mais para frente.

O setup inicial do projeto segue o desse artigo a parte:
https://dev.to/jackson_primo/inicializando-um-projeto-ruby-on-rails-usando-postgresql-docker-compose-1gh5

# Lets Bora

Começaremos adicionando nosso model de User na nossa aplicação, afinal ele será o foco da autenticação:

``` shell
$ rails g model user name:string username:string email:string password_digest:string
```

Agora vamos adicionar algumas modificações no model:
``` ruby
class User < ApplicationRecord
  has_secure_password
  validates :email, format: {with: URI::MailTo::EMAIL_REGEXP}, presence: true, uniqueness: true
  validates :name, presence: true, length: { maximum: 50 }
  validates :password, presence: true, length: { minimum: 6 }

  before_save :downcase_email

  private

  def downcase_email
    self.email = email.downcase
  end
end

```
Na segunda linha temos o **has_secure_password**, que adiciona recursos de autenticação nativos do rails no model, como a criação do campo password_digest que abriga o password encriptado e o método authenticate no model que verifica se uma string corresponde ao password encriptado.

Crie uma migração para que o seu banco receba as atualizações dos models da aplicação.
``` shell
$ rails db:migrate
```

Com nosso model pronto vamos configurar os controllers, primeiramente adicionando alguns métodos no application_controller, que vai ser herdado por todos os outros controllers:
``` ruby
class ApplicationController < ActionController::API
  def render_result result
    if result[:error]
      render json: { error: result[:error] }, status: result[:code]
    else
      render json: result
    end
  end

  def params 
    request.params
  end
end

``` 
Vamos gerar o controller que ficará responsável pela autenticação
``` shell
$ rails g controller auth signin signup
```
Nele colocaremos 2 métodos um de registro e outro de login
``` ruby
class AuthController < ApplicationController
  def signin
    result = ::Services::Auth::Signin.new(params).call
    render_result result
  end
  
  def signup
    result = ::Services::Auth::Signup.new(params).call
    render_result result
  end
end

```
Note que em cada função decidi deixar as regras de negócio para um arquivo a parte que seria o service. 
Ps: Antes de prosseguirmos uma breve explicação sobre os services, o uso deles a meu ver representa bem o uso do principio Single Responsability do SOLID, pois cada arquivo representa apenas uma ação que deve ser executada, possuindo um nome que reflete esta ação e apenas um método público "call". Elas serão adicionadas dentro de app -> service e cada pasta dentro dela representa um módulo que trata de um conjunto de regras de negócio, podendo ser de uma funcionalidade ou apenas de um model no banco.

Vamos criar o modulo de service Auth, começando pela classe base que é responsável por abrigar funções e variáveis que podem ser reaproveitadas por outros arquivos dentro do modulo. 

``` ruby
# app/services/auth/base.rb
module Services
 module Auth
  class ServiceException < Exception
    attr_reader :code
    
    def initialize(message, error_code=500)
      super(message)
      @code = error_code
    end
  end

  class Base
   def initialize(params)
    @params = params
   end
  end
 end
end
```
Inicialmente ela só vai pegar os parâmetros da request e jogar em uma variável de instancia, também adicionei uma classe para tratar exceções aceitando a mensagem e o código de erro.

Agora vamos criar nosso signin:
``` ruby
require "json_web_token"

module Services
  module Auth
    class Signin < Base
      def call
        find_user
        authenticate
      rescue ServiceException => e
        { error: e.message, code: e.code }
      rescue Exception => e
        { error: e.message, code: 500 }
      end

      def find_user
        @user = User.find_by_email(@params[:email])
      end

      def authenticate 
        if @user&.authenticate(@params[:password])
          token = JsonWebToken.encode(user_id: @user.id)
          time = Time.now + 24.hours.to_i

          { 
            token: token, 
            exp: time.strftime("%m-%d-%Y %H:%M"),
            username: @user.name 
          }
        else
          raise ServiceException.new("cannot signin with this credentials", 403)
        end
      end
    end
  end
end
```
Agora partimos para o signup:
``` ruby
module Services
  module Auth
    class Signup < Base
      def call
        already_has_user_with_this_email?
        create_user
      rescue ServiceException => e
        { error: e.message, code: e.code }
      rescue Exception => e
        { error: e.message, code: 500 }
      end

      def already_has_user_with_this_email?
        user = User.find_by_email(@params[:email])
        raise ServiceException.new('email already in use', 400) if user 
      end

      def create_user
        user = User.new(sanitize_params)
        return user if user.save!
        raise ServiceException.new("cannot register user: #{user.errors}", 400)
      end

      def sanitize_params
        {
          name: @params[:name],
          password:  @params[:password],
          email:  @params[:email]
        }
      end
    end
  end
end
```
Caso você venha a ter problemas do tipo "NameError: uninitialized constant AuthController::Services" é provável que o auto import das configurações esteja seguindo as novas regras de nomenclatura de pastas(uma frescura ae do rails que não sei o motivo de existir), para evitar esse problema adicione a config:
``` ruby
# config/application.rb
config.eager_load_paths.delete("#{Rails.root}/app/services")
config.eager_load_paths.unshift("#{Rails.root}/app")
```
Nas referências tem um artigo que explica melhor sobre isso.

Antes de finalizar, para dar um toque bacana na chamada dos services, ao invés de inicializar a classe de service e depois chamar a função call, podemos fazer apenas a chamada do próprio método passando os parâmetros:

``` ruby
class AuthController < ApplicationController
  def signin
    result = ::Services::Auth::Signin.call(params)
    render_result result
  end
  
  def signup
    result = ::Services::Auth::Signup.call(params)
    render_result result
  end
end
```
Para isso precisamos criar uma classe que deve ser herdada pelos nossos services:
``` ruby
# app/services/application_service.rb
module Services
  class ApplicationService
   def self.call(*args, &block)
     new(*args, &block).call
   end
  end
end
```
Esta classe possui uma função de classe chamada call que cria uma nova instancia e chama a função call desta instancia.
Agora basta fazer o base dos services herdarem essa classe:
``` ruby
module Services
 module Auth
  class Base < ApplicationService
   def initialize(params)
    @params = params
   end
  end
 end
end
```
Isto é necessário? Não (mas fica do balacobaco)

Com isso temos uma estrutura de pastas e separação de regras de negócio bem interessante. Qualquer dica, sugestão ou duvida deixa nos comentários. 

link de referências do artigo:
https://www.toptal.com/ruby-on-rails/rails-service-objects-tutorial
https://blog.appsignal.com/2020/06/17/using-service-objects-in-ruby-on-rails.html
https://www.fastruby.io/blog/rails/upgrade/zeitwerk/upgrading-to-zeitwerk.html
https://dev.to/joker666/ruby-on-rails-pattern-service-objects-b19
https://www.thoughtco.com/nameerror-uninitialized-2907928
https://medium.com/binar-academy/rails-api-jwt-authentication-a04503ea3248