API Login com JWT e Devise: Segurança e Flexibilidade em Seu Projeto Rails
Rodrigo Barreto
Posted on January 24, 2024
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'
Instalar o devide:
bundle exec rails generate devise:install
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
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
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
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
# 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
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
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
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
Login:
Events:
Neste caso é importante usar no headers
key: Authorization
value: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1dWlkIjoiZGVhMzY5MDQtMDQ4Ny00OTI3LWJjNDktNDU0YmY4N2ZkMzU4IiwiZXhwIjoxNzA1NjE1NTcwfQ.BBtXmqvCxHXCPp_x_BJ53veZXwL3qUlklAHTJLjJpIk
Events quando o token é invalido:
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
Posted on January 24, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.