DEV Community

Rodrigo Barreto
Rodrigo Barreto

Posted on • Edited on

API Login com JWT e Devise: Segurança e Flexibilidade em Seu Projeto Rails

Nos posts anteriores, desenvolvemos um projeto sobre eventos no Rails. Agora, queremos adicionar uma camada de segurança: apenas usuários autenticados devem ter acesso aos detalhes dos eventos. Para isso, vamos implementar um sistema de login usando o Devise - uma solução flexível e amplamente utilizada para autenticação em Rails - em conjunto com a gem JWT. Embora o Devise possa ser substituído por outras abordagens, sua integração com o JWT oferece uma solução robusta e eficiente para nossas necessidades de autenticação.

Por que usar JWT?

A utilização do JWT oferece várias vantagens, especialmente em aplicações web modernas. Ele é leve e fácil de usar, facilitando a integração entre diferentes serviços e plataformas. Além disso, o JWT proporciona um alto nível de segurança, pois a informação contida nele é criptografada, evitando assim o acesso não autorizado aos dados. Essas características tornam o JWT ideal para situações onde a confiabilidade e a segurança das informações de autenticação são cruciais.

O que é JWT?

JSON Web Token (JWT) é um padrão compacto e seguro de URL para a transmissão de informações entre partes como um objeto JSON. Ele é especialmente útil em cenários de autenticação e autorização, pois permite a troca de informações de forma segura e eficiente. Um JWT é composto de três partes: um cabeçalho, um payload (carga útil) e uma assinatura, cada um contribuindo para garantir a integridade e a autenticidade dos dados transmitidos.

gem 'devise'
gem 'jwt'
Enter fullscreen mode Exit fullscreen mode

Instalar o devide:

bundle exec rails generate devise:install
Enter fullscreen mode Exit fullscreen mode

Criação da Classe JsonWebToken
Para gerenciar os tokens JWT em nossa aplicação Rails, precisamos criar uma classe especializada chamada JsonWebToken. Essa classe terá a responsabilidade de codificar (criar) e decodificar (verificar) os tokens JWT, que são essenciais para o processo de autenticação.

Por que uma Classe Personalizada?
Utilizar uma classe personalizada para o manejo dos tokens JWT nos permite maior controle e flexibilidade. Podemos definir exatamente como os tokens são criados e validados, adaptando-os às necessidades específicas do nosso sistema.

Implementação da Classe JsonWebToken
Aqui está a implementação básica da nossa classe JsonWebToken:

# frozen_string_literal: true

class JsonWebToken
  SECRET = 'secret-key'
  ENCRYPTION = 'HS256'
  def self.encode(payload, exp = 24.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET)
  end

  def self.decode(token)
    body = JWT.decode(token, SECRET)[0]
    HashWithIndifferentAccess.new(body)
  rescue JWT::ExpiredSignature
    nil
  rescue StandardError
    nil
  end
end

Enter fullscreen mode Exit fullscreen mode

Vamos implementar a Classe de Erros agora,
Uma parte crucial de qualquer sistema de autenticação é o gerenciamento de erros. Para isso, criaremos uma classe Errors personalizada, que nos ajudará a lidar com erros de uma maneira estruturada e reutilizável em toda a nossa aplicação.

# frozen_string_literal: true
class Errors < Hash
  def add(key, value, _opts = {})
    self[key] ||= []
    self[key] << value
    self[key].uniq!
  end

  def add_multiple_errors(errors_hash)
    errors_hash.each do |key, values|
      values.each { |value| add key, value }
    end
  end

  def each
    each_key do |field|
      self[field].each { |message| yield field, message }
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Após estabelecer as bases com a classe JsonWebToken e a nossa gestão de erros, o próximo passo é criar uma estrutura comum que será utilizada em diferentes contextos da nossa aplicação. Para isso, vamos implementar a classe Common::ApplicationService.

Esta classe servirá como um alicerce para os módulos de autenticação (auth) e autorização (authorization), fornecendo métodos e estruturas que serão compartilhados entre eles. A ideia é promover a reutilização de código e manter nossa aplicação organizada e eficiente, evitando a repetição desnecessária de lógicas semelhantes em diferentes partes do código.

Agora, vamos focar em um elemento crucial do nosso sistema de autenticação: a classe Auth::Base. Esta classe, que herda de Common::ApplicationService, é responsável por abstrair e gerenciar a lógica central de autenticação em nossa aplicação.

# frozen_string_literal: true

module Auth
  class Base < Common::ApplicationService

    attr_accessor :email, :password, :user

    def call
      generate_token if user
    end

    def generate_token
      token = JsonWebToken.encode(uuid: user.uuid)
      user.update(token: token)
      token
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Logo em seguida vamos criar o create e o destroy

# app/services/auth/user/token/create.rb
# frozen_string_literal: true

class Auth::User::Token::Create < Auth::Base
  def initialize(email, password)
    @email = email
    @password = password
  end

  def user
    user = User.find_by(email: email)
    return user if user&.valid_password?(password)

    errors.add :user_authentication, 'invalid credentials'
  end
end
Enter fullscreen mode Exit fullscreen mode
# frozen_string_literal: true
# app/services/auth/user/token/destroy.rb
class Auth::User::Token::Destroy < Auth::Base
  attr_reader :token, :user_id

  def initialize(token, user_id)
    @token = token
    @user_id = user_id
  end

  def call
    destroy_token if user
  end

  def user
    @user ||= User.where(uuid: @user_id, token: http_auth_header)&.first
    errors.add(:invalid_token, 'invalid Token') if @user.blank?
    @user
  end

  def destroy_token
    return errors if @user.blank?

    @user.update(token: "")
  end

  def http_auth_header
    return @token.split.last if @token.present?

    errors.add(:token, 'missing token')

    nil
  end
end
Enter fullscreen mode Exit fullscreen mode

Logo em seguida vamos criar a classe AuthorizeApiRequestUser

# frozen_string_literal: true


module Authorization
  module User
    class AuthorizeApiRequestUser < Common::ApplicationService

      attr_reader :headers

      def initialize(headers = {})
        @headers = headers
      end

      def call
        user
      end

      private

      def decoded_auth_token
        @decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
      end

      def http_auth_header
        return headers['Authorization'].split.last if headers['Authorization'].present?

        errors.add(:token, 'missing token')

        nil
      end

      def user
        @user ||= ::User.find_by(uuid: decoded_auth_token[:uuid], token: http_auth_header) if decoded_auth_token
        @user || (errors.add(:token, 'invalid token of user') && nil)
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Criando um Endpoint para Geração de Token JWT

Com as bases de autenticação já estabelecidas, o próximo passo é criar um endpoint específico que permitirá aos usuários gerar um token JWT. Esse token será essencial para acessar as partes protegidas da aplicação.

skip_before_action é fundamental para termos um lugar para gerar a chave de acesso:

class SessionController < ApplicationController
  skip_before_action :authenticate_request, only: [:create]

  def create
    auth = Auth::User::Token::Create.call(params["email"], params["password"])
    if auth.success?
      uuid = ::JsonWebToken.decode(auth.result)["uuid"]
      user = User.find_by(uuid: uuid)
      render json: user, except: [:created_at, :updated_at, :id], status: :ok
    else
      render json: { errors: auth.errors }, status: :unauthorized
    end
  end

  def destroy
    auth = Auth::User::Token::Destroy.call(request.headers['Authorization'], User.first.uuid)
    if auth.success?
      render json: { message: auth.result }
    else
      render json: { errors: auth.errors }, status: :unauthorized
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Aprimorando o ApplicationController para Autenticação e Gerenciamento de Usuário

Agora, vamos aprofundar na funcionalidade do nosso ApplicationController para gerenciar a autenticação dos usuários e garantir que eles tenham acesso às operações autorizadas em nossa aplicação.

Criando o attr_reader :current_user

Primeiro, adicionaremos um attr_reader :current_user no nosso ApplicationController. Isso nos permite acessar o usuário autenticado atual em qualquer lugar da nossa aplicação. Embora neste exemplo usemos current_user, você pode nomear este atributo como e onde preferir.


class ApplicationController < ActionController::API
  before_action :authenticate_request
  attr_reader :current_user

  rescue_from ActionDispatch::Http::Parameters::ParseError do |_exception|
    render json: { error: 'something happens', status: :bad_request }
  end
  def authenticate_request
    auth = Authorization::User::AuthorizeApiRequestUser.call(request.headers)
    @current_user = auth.result
    render json: { errors: auth.errors }, status: :unauthorized unless @current_user
  end
end

Enter fullscreen mode Exit fullscreen mode

Login:

Image description

Events:
Neste caso é importante usar no headers
key: Authorization value: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1dWlkIjoiZGVhMzY5MDQtMDQ4Ny00OTI3LWJjNDktNDU0YmY4N2ZkMzU4IiwiZXhwIjoxNzA1NjE1NTcwfQ.BBtXmqvCxHXCPp_x_BJ53veZXwL3qUlklAHTJLjJpIk

Image description

Events quando o token é invalido:

Image description

Logout:
Image description

Para facilitar seus testes e exploração, criei uma branch especial chamada jwt_login no repositório do GitHub. Esta branch contém todas as implementações recentes relacionadas ao nosso sistema de login JWT. Convido você a baixar esta branch e experimentar as funcionalidades que discutimos.

github: jwt_login

Top comments (0)