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

rodrigonbarreto_86

Rodrigo Barreto

Posted on January 24, 2024

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

💖 💪 🙅 🚩
rodrigonbarreto_86
Rodrigo Barreto

Posted on January 24, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

What was your win this week?
weeklyretro What was your win this week?

November 29, 2024

Where GitOps Meets ClickOps
devops Where GitOps Meets ClickOps

November 29, 2024