Introdução
Olá, devs! 👋
Bem-vindos à terceira parte da nossa série sobre Grape e Rails! Nos posts anteriores, exploramos a configuração básica da API e implementamos operações CRUD para nossos eventos. Hoje, vamos dar um passo adiante e implementar autenticação para nossa API.
A segurança é um aspecto crucial de qualquer API, e implementar um sistema de autenticação robusto é essencial para proteger seus recursos. Neste post, vamos explorar como implementar autenticação com JWT (JSON Web Tokens) e como proteger nossas rotas.
Configurando Autenticação com JWT
Para nosso exemplo, vamos utilizar JWT para autenticação. Se você não está familiarizado com JWT, é um padrão para a criação de tokens de acesso que podem ser validados sem a necessidade de consultar um banco de dados. Os tokens JWT contêm informações sobre o usuário e expiram após um determinado período.
Primeiro, vamos adicionar as gems necessárias ao nosso projeto:
gem 'devise' # Para autenticação de usuários
gem 'jwt' # Para trabalhar com tokens JWT
Execute bundle install
para instalar as dependências.
1. Criando o Serviço de JWT
Primeiro, precisamos criar um serviço para lidar com a codificação e decodificação de tokens JWT. Em app/services/jwt/json_web_token.rb
:
# frozen_string_literal: true
module Jwt
class JsonWebToken
# SECRET = Rails.application.credentials.secret_key_base
SECRET = "secret-key" # usar algum código no secrets ou algo mais seguro
ENCRYPTION = "HS256"
def self.encode(payload, exp = 120.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, SECRET)
rescue JWT::EncodeError => e
Rails.logger.error("JWT Encode Error: #{e.message}")
end
def self.decode(token)
body = JWT.decode(token, SECRET)[0]
ActiveSupport::HashWithIndifferentAccess.new(body)
rescue JWT::ExpiredSignature, JWT::DecodeError => e
Rails.logger.error("JWT Encode Error: #{e.message}")
nil
end
end
end
Observação: No ambiente de produção, você deve armazenar a chave secreta em um local seguro, como
Rails.application.credentials.secret_key_base
ou uma variável de ambiente.
2. Criando Helpers para Autenticação
Em seguida, vamos criar helpers para lidar com a autenticação em nossos endpoints. Em app/api/helpers/auth_helpers.rb
:
# frozen_string_literal: true
module Helpers
module AuthHelpers
extend Grape::API::Helpers
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
def authenticate_request!
# Este exemplo é simplificado; no projeto real, usamos apenas headers["Authorization"]
token = headers["Authorization"]&.split&.last || headers["authorization"]&.split&.last
error!({ error: "Unauthorized", success: false }, 401) unless token
# Decodifica o token JWT
@jwt_result = Jwt::JsonWebToken.decode(token)
error!({ error: "Invalid Token", success: false }, 401) unless @jwt_result
# Busca o usuário atual (ou gera erro se não for encontrado)
current_user!
rescue StandardError => e
error!({ error: "Authentication Error: #{e.message}" }, 401)
end
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
# Este método busca o usuário atual e gera erro se não for encontrado
def current_user!
@current_user ||= User.find_by(id: @jwt_result["id"])
error!({ error: "Invalid Token", success: false }, 401) unless @current_user
@current_user
end
# Este método permite usar o current_user em qualquer lugar
def current_user
@current_user
end
end
end
3. Implementando o Endpoint de Login
Agora, vamos criar um endpoint para autenticar usuários e gerar tokens JWT. Em app/api/v1/login.rb
:
# frozen_string_literal: true
module V1
class Login < Grape::API
resource :login do
desc "Login ", {
success: Entities::Users::LoginResponse,
failure: [{ code: 401, message: "Unauthorized" }],
params: ::Helpers::ContractToParams.generate_params(LoginContract)
}
post do
contract = LoginContract.new
result = contract.call(params)
error!({ errors: result.errors.to_h }, 422) if result.failure?
@user = User.find_by(email: params[:email])
if @user&.valid_password?(params[:password])
token = ::Jwt::JsonWebToken.encode({ id: @user.id })
status 200
present({ user: @user, token: token }, with: Entities::Users::LoginResponse)
else
error!({ error: "Unauthorized" }, 401)
end
end
end
end
end
Nota: Este exemplo usa
valid_password?
do Devise, que é um método padrão para verificar senhas.
4. Definindo Entidades para Resposta de Login
Para formatar as respostas do endpoint de login, vamos criar algumas entidades:
Em app/api/entities/users/user_response.rb
:
# frozen_string_literal: true
module Entities
module Users
class UserResponse < Grape::Entity
expose :id, documentation: { type: "Integer", desc: "User id" }
expose :email, documentation: { type: "String", desc: "User email" }
end
end
end
Em app/api/entities/users/login_response.rb
:
# frozen_string_literal: true
module Entities
module Users
class LoginResponse < Grape::Entity
expose :user, using: Entities::Users::UserResponse,
documentation: { type: "Array", desc: "List of users" }
expose :token, documentation: { type: "String", desc: "JWT token" }
end
end
end
5. Atualizando o API Grape
Agora, vamos atualizar nosso arquivo app/api/v1/api_grape.rb
para incluir os helpers de autenticação e o novo endpoint de login:
# frozen_string_literal: true
class V1::ApiGrape < Grape::API
version "v1", using: :path
format :json
default_format :json
use CustomErrorMiddleware
helpers ::Helpers::AuthHelpers
mount V1::Events
mount V1::Login
add_swagger_documentation(
api_version: "v1",
hide_documentation_path: true,
mount_path: "/swagger_doc",
hide_format: true,
info: {
title: "Events API",
description: "API for event management",
contact_name: "Support Team",
contact_email: "contact@example.com"
},
security_definitions: {
Bearer: {
type: "apiKey",
name: "Authorization",
in: "header",
description: 'Enter "Bearer" followed by your token. Example: Bearer abc123'
}
},
security: [{ Bearer: [] }]
)
end
6. Adicionando Dados de Teste
Para facilitar os testes, podemos criar um usuário padrão usando seeds:
# db/seeds.rb
User.find_or_create_by!(email: "teste@examle.com") do |user|
user.password = "123456"
user.password_confirmation = "123456"
end
Execute rails db:seed
para criar o usuário.
Protegendo Endpoints
Com nossa configuração de autenticação em vigor, agora podemos proteger nossos endpoints. Vamos atualizar nosso controlador de eventos para exigir autenticação:
module V1
class Events < Grape::API
before { authenticate_request! }
# Resto do código permanece o mesmo...
end
end
Agora, todos os endpoints no recurso events
exigirão um token JWT válido para acesso.
Implementando Middleware para Tratamento de Erros
Para melhorar ainda mais nossa API, vamos implementar um middleware personalizado para tratar erros comuns. Em app/middleware/custom_error_middleware.rb
:
# frozen_string_literal: true
class CustomErrorMiddleware < Grape::Middleware::Base
def call(env)
@app.call(env) # Passa a requisição adiante
rescue Grape::Exceptions::MethodNotAllowed => e
handle_method_not_allowed(e, env)
rescue ActiveRecord::RecordNotFound => e
handle_record_not_found(e)
rescue StandardError => e
handle_standard_error(e)
end
private
def handle_record_not_found(error)
# Poderia usar, por exemplo: Sentry.capture_exception(error, extra: { message: error.message })
error_response(message: "Couldn't find #{error.model || 'record'} with id: #{error.id}", status: 404)
end
def handle_method_not_allowed(error, env)
request_method = env["REQUEST_METHOD"]
path = env["PATH_INFO"]
Rails.logger.error "Method not allowed: #{request_method} for path #{path}"
allowed_methods = if error.respond_to?(:headers) && error.headers["Allow"]
"Allowed methods: #{error.headers['Allow']}"
else
"Please use the appropriate HTTP methods for this endpoint"
end
error_message = "The
#{request_method} method is not allowed for this resource.
#{allowed_methods}. please check your parameters"
unless Rails.env.development?
# Sentry.capture_exception(error, extra: {
# request_method: request_method,
# path: path,
# message: error_message
# })
end
error_response(message: error_message, status: 405)
end
def handle_standard_error(error)
Rails.logger.error(error.message)
Rails.logger.error(error.backtrace.join("\n"))
# Sentry.capture_exception(error, extra: { message: error.message }) unless Rails.env.development?
error_response(message: "Something went wrong", status: 500)
end
def error_response(message:, status:)
throw :error, message: { error: message }, status: status
end
end
Este middleware captura exceções comuns e as transforma em respostas de API adequadas. Com ele implementado, podemos simplificar nossos endpoints:
get ":id" do
@event = Event.find(params[:id])
present @event, with: ::Entities::EventResponse
end
O middleware capturará automaticamente a exceção ActiveRecord::RecordNotFound
e retornará uma resposta de erro apropriada.
Testando a Autenticação
Agora que implementamos a autenticação, vamos adicionar alguns testes para garantir que tudo funcione corretamente:
# spec/api/v1/login_spec.rb
require 'rails_helper'
RSpec.describe V1::Login, type: :request do
let(:base_url) { "/api/v1/login" }
let(:email) { "user@example.com" }
let(:password) { "password123" }
before do
@user = create(:user, email: email, password: password)
end
describe "POST /api/v1/login" do
context "with valid credentials" do
it "returns a JWT token" do
post base_url, params: { email: email, password: password }
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response["user"]["id"]).to eq(@user.id)
expect(json_response["user"]["email"]).to eq(@user.email)
expect(json_response["token"]).not_to be_nil
end
end
context "with invalid credentials" do
it "returns an error" do
post base_url, params: { email: email, password: "wrong_password" }
expect(response).to have_http_status(:unauthorized)
json_response = JSON.parse(response.body)
expect(json_response["error"]).to eq("Unauthorized")
end
end
end
end
Documentação Swagger Atualizada
Com a adição da autenticação e do endpoint de login, nossa documentação Swagger agora mostrará:
- Um novo endpoint
/login
para autenticação - Informações de segurança para todos os endpoints protegidos
- Um botão "Authorize" na interface Swagger para inserir o token JWT
Conclusão
Nesta terceira parte da nossa série, implementamos um sistema de autenticação completo para nossa API Grape:
- Configuramos JWT para gerar e verificar tokens
- Criamos helpers para autenticação
- Implementamos um endpoint de login
- Protegemos nossos endpoints com autenticação
- Adicionamos middleware para tratamento de erros
- Atualizamos nossa documentação Swagger
A autenticação é um componente crucial para qualquer API, e o Grape torna relativamente simples implementá-la. Ao combinar JWT com os helpers do Grape, conseguimos um sistema de autenticação robusto que protege nossos recursos e proporciona uma experiência de desenvolvedor agradável.
No próximo post, exploraremos recursos mais avançados do Grape, como paginação, ordenação e filtragem, para tornar nossa API ainda mais poderosa e flexível.
Gostou deste post? Deixe seus comentários abaixo e não se esqueça de conferir as partes anteriores desta série!
Repositório do exemplo: https://github.com/rodrigonbarreto/event_reservation_system/tree/mvp_sample_pt3
Referências:
Me chamo Rodrigo Nogueira Barreto. Trabalho com Ruby on Rails desde 2015.
Caso queiram acompanhar essa jornada, por favor comentem e deixem seu like!