Andrés
•
18 August 2024
•
9 mins
Agregar autenticación y autorización a tus APIs es una parte fundamental en el desarrollo de tus aplicaciones. De estas dos estrategias depende gran parte de la seguridad y el correcto acceso a los datos y servicios que ofrecerás.
Para empezar, creo que es importante entender la diferencia entre autenticación y autorización en una API REST. La primera se refiere al proceso de verificar la identidad de un usuario o servicio, y la segunda corresponde al proceso de determinar si ese usuario o servicio autenticado tiene permisos para acceder al recurso o acción que intenta realizar.
¿Dónde estamos?
Para aplicar la autenticación existen algunas estrategias comunes y no es necesario reinventar la rueda, así que para clasificarlas podemos basarnos en las opciones que define OpenAPI Initiative:
Authorization):
Authorization.Authorization.Lo mismo que la autorización, no reinventaremos la rueda, así que aquí te dejo algunas de las estrategias más comunes:
Ahora que entendemos mejor los conceptos, podemos pasar a la práctica. En nuestro caso, utilizaremos devise-api para la autenticación (Bearer Token) y Pundit para la autorización (PBAC). Las gemas ya las agregamos a nuestro proyecto en los capítulos anteriores. Pero si no lo has hecho, entonces agrega lo siguiente en alguna parte de tu Gemfile:
gem 'devise'
gem 'devise-api'
gem 'pundit'
💥Ahora puedes usar el generador de autenticación de Rails 8💥
Como la autenticación depende de Devise, lo primero es que sigamos los pasos de instalación de esta gema:
rails generate devise:install
rails generate devise User
Luego devise_api:
rails generate devise_api:install
Corremos las migraciones:
rails db:migrate
Y finalmente agregamos el módulo api al modelo que contiene Devise, osea la clase User. Para este caso podemos eliminar todos los otros módulos y dejar solo los siguientes:
class User < ApplicationRecord
devise :database_authenticatable, :api
has_many :roles
has_many :projects, through: :roles
end
Para que los endpoints de devise-api sean expuestos, debemos utilizar el método devise_for original de Devise en el archivo routes.rb:
Rails.application.routes.draw do
devise_for :users
# ...
end
revoke_user_tokens POST /users/tokens/revoke(.:format) devise/api/tokens#revoke
refresh_user_tokens POST /users/tokens/refresh(.:format) devise/api/tokens#refresh
sign_up_user_tokens POST /users/tokens/sign_up(.:format) devise/api/tokens#sign_up
sign_in_user_tokens POST /users/tokens/sign_in(.:format) devise/api/tokens#sign_in
info_user_tokens GET /users/tokens/info(.:format) devise/api/tokens#info
Ahora puedes utilizar los siguientes helpers en los controladores para proteger los endpoints con autenticación. En el caso de nuestro ejemplo, queremos cubrir toda la API, así que podemos agregarlos en el ApplicationController para que los demás controladores los hereden. Además, agregaré un método que nos puede ser útil:
class ApplicationController < ActionController::API
skip_before_action :verify_authenticity_token, raise: false
before_action :authenticate_devise_api_token!
def current_user
current_devise_api_user
end
end
En este punto, casi todos los tests deberían estar fallando. Necesitaremos agregarles autenticación. Para facilitarnos esta tarea, te propongo que agregues la siguiente factoría:
# test/factories/devise_api_token.rb
FactoryBot.define do
factory :devise_api_token, class: "Devise::Api::Token" do
association :resource_owner, factory: :user
access_token { SecureRandom.hex(32) }
refresh_token { SecureRandom.hex(32) }
expires_in { 1.hour.to_i }
trait :access_token_expired do
created_at { 2.hours.ago }
end
trait :refresh_token_expired do
created_at { 2.months.ago }
end
trait :revoked do
revoked_at { 5.minutes.ago }
end
end
end
Luego tienes que crear tokens y enviarlos junto a las peticiones HTTP:
class ProjectsControllerTest < ActionDispatch::IntegrationTest
setup do
# ...
@token = FactoryBot.create(:devise_api_token).access_token
end
test 'should get index' do
get projects_url, headers: { Authorization: "Bearer #{@token}" }, as: :json
assert_response :success
end
# ...
end
El problema del capítulo 1 mencionaba varias condiciones, todas válidas, pero para simplificarnos la tarea y con el fin de una demostración, solo vamos a cubrir la autorización de Tareas. Solo los User con el rol de administrador en el proyecto podrán crearlas (create), el resto no.
Antes de continuar, debemos asegurarnos de haber seguido los pasos de instalación de Pundit. Estos son:
Agrega el módulo al ApplicationController:
class ApplicationController < ActionController::Base
include Pundit::Authorization
#... También deberías manejar los errores de Unauthorized:
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
#...
private
def user_not_authorized(exception)
policy_name = exception.policy.class.to_s.underscore
render json: { message: "#{policy_name}.#{exception.query}" }, status: :forbidden
end
end
Creamos un ApplicationPolicy que servirá de base:
rails g pundit:install
Y listo. Hagamos un test que falle:
#test/controllers/task_controllers_test.rb
# ...
test 'should create task' do
token = FactoryBot.create(:devise_api_token)
FactoryBot.create(:role, user: token.resource_owner, role: :manager, project: @project)
assert_difference('Task.count') do
post project_tasks_url(@project), headers: { Authorization: "Bearer #{token.access_token}" },
params: { task: { description: @task.description, project_id: @task.project_id, status: @task.status, title: @task.title } }, as: :json
end
assert_response :created
end
test 'should not create task' do
token = FactoryBot.create(:devise_api_token)
FactoryBot.create(:role, user: token.resource_owner, role: :contributor, project: @project)
assert_no_difference('Task.count') do
post project_tasks_url(@project), headers: { Authorization: "Bearer #{token.access_token}" },
params: { task: { description: @task.description, project_id: @task.project_id, status: @task.status, title: @task.title } }, as: :json
end
assert_response :forbidden
end
# ...
Ahora hagamos que funcionen. Pundit se maneja con archivos Policy, debemos crear uno siguiendo las instrucciones de la gema, heredando de nuestra ApplicationPolicy e implementando nuestra lógica de validación (también puedes usar el generador):
# app/policies/post_policy.rb
class TaskPolicy < ApplicationPolicy
def create?
user.roles.exists?(project: record.project, role: :manager)
end
end
Agregamos la autorización al método del controlador:
# app/controllers/tasks_controller.rb
# ...
# POST /tasks
def create
# ...
authorize @task
# ...
end
# ...
Deberíamos obtener un lindo color verde en todos nuestros tests.
Como podemos ver, aplicar autorización y autenticación en nuestra API se hace fácil con Devise y Pundit. Te recomiendo que veas las documentaciones oficiales para entender más a fondo todo lo que puedes llegar a realizar con estas dos gemas y un poco de ingenio.
Hemos visto una pequeña introducción a la autenticación utilizando un Bearer token y la autorización basada en políticas y roles cuando desarrollamos APIs. Ruby on Rails es un framework espectacular y espero que algo bueno hayas sacado de todo este post.
¡Happy coding!
El repo con todo el código: https://github.com/a-chacon/api-project-management-example
¿Te gustó? ¡Compártelo!