¿Cómo agregar seguridad a tu API RESTful?
Autenticación por tokens para una API RESTful en Rails; modelos User y Token, generación con SecureRandom, expiración y verificación de permisos por usuario.
¿Cansado de sufrir hackeos?, ¿de tener que abrir tu página web al día siguiente y ver un enorme 'F*ck you!' en el header? ¡¡¡ENTONCES ESTE POST ES PARA TI!!!… Ok, quizás no para ti, pero si te interesa cómo autentificar usuarios mediante tokens en tu API RESTful para aumentar la seguridad de esta, entonces definitivamente debes seguir leyendo lo que resta del post.
El hackeo que me enseñó sobre seguridad…
Era una mañana tranquila en un hospital; recuerdo que llevaba menos de 1 mes en el área de programación, cuando de pronto el gran 'Jefe' llamó al jefe del área porque 'alguien' había hackeado la página web del hospital…
…resultó que a alguien se le ocurrió hackear la página y 'romper' varias etiquetas. Para solucionar este problema se usó simplemente un backup que se tenía guardado y se logró 'salvar' de alguna forma la página.
Luego de 2 meses terminé con la tarea que me encargaron, que era la de hacer un update a la nueva versión de Joomla [sí, ¡usaban JOOMLA! como CMS de su página web].

Sábado, 8:00 am. Dormía tranquilamente cuando el encargado del área de programación me llama y me dice, con un tono de voz desesperado: '¡¡¡HAN HACKEADO LA PÁGINA!!!'… Desde ese momento entendí lo importante de la seguridad ಥ_ಥ
API's y Ruby on Rails…
En uno de mis posts pasados hablé sobre lo que es una API y cómo podemos crear una muy sencilla en Ruby on Rails. Teníamos un modelo user, el cual posee un email y name:
# my_api/app/models/user.rb
class User < ActiveRecord::Base
validates :email, presence: true, uniqueness: true
validates :name, presence: true
endAgregaremos dos nuevos modelos:
$ rails g model Token token expires_at:datetime user:references
$ rails g model Article title body:text user:referencesNuestro modelo Token tiene un campo 'token' que almacenará una cadena random [la cual se generará automáticamente] y un campo 'expires_at' para darle un tiempo de expiración a nuestro token [pues no queremos que se tenga acceso infinito a nuestra API].
Nuestros usuarios usarán el token para autentificarse en nuestra API [en una aplicación web convencional tendríamos sesiones para nuestros usuarios; sin embargo, en una API esto no es posible, por lo que usaremos tokens en vez de sesiones].
El token lo usaremos para poder identificar al usuario que desea crear, actualizar o eliminar ciertos artículos [solo el que creó el artículo puede hacer un update o destroy en ellos; después de todo, no queremos que cualquier usuario pueda eliminar cualquier artículo].
No me centraré en hacer pasos específicos o detallados de cómo hacer cada cosa, pues estoy suponiendo que usted ya tiene un nivel de conocimiento o experiencia básica con Ruby on Rails [después de todo, alguien no empieza aprendiendo Rails creando API's, ¿verdad?].
¿Cómo funcionarán nuestros tokens?
En cada request que hagan a nuestra API [como por ejemplo para crear un artículo] nos deberán enviar el token; verificaremos si el token está vencido o no [nuestros tokens tendrán una duración máxima de 3 meses; si este se vence, entonces el usuario tendrá que solicitar un nuevo token] y finalmente encontraremos a qué usuario pertenece el token enviado, y es con este usuario que haremos las acciones respectivas.
Puede sonar algo confuso, pero ya lo entenderemos mejor viendo algo de código.
Nuestras rutas
Nada del otro mundo, tenemos las rutas básicas para hacer funcionar nuestra API:
Rails.application.routes.draw do
namespace :api, defaults: { format: "json" } do
namespace :v1 do
resources :users, only: [:create] # solo usaremos la accion 'create' para agregar nuevos usuarios
resources :articles, except: [:new, :edit] # no necesitaremos estas acciones al tratarse de un API
end
end
endUser model
Para registrar usuarios agregaremos un método de clase 'with_token', en donde nuestro parámetro 'data' contendrá los datos de nuestro usuario a crear [algo así: params { data: { email: 'giancarlos@devacademy.la', name: 'Giancarlos Isasi' } }].
Solo para explicar un poco, la línea 9 funciona de la siguiente manera: primero buscará un usuario con el email que nos envíen; si no lo encuentra, entonces creará un nuevo registro con los datos que le especificamos en el bloque de código [first or create: si no lo encuentras, crea uno… si encuentra el registro, entonces nos devolverá aquel objeto].
# my_api/app/models/user.rb
class User < ActiveRecord::Base
validates :email, presence: true, uniqueness: true
validates :name, presence: true
def self.with_token(data)
User.where(email: data[:email]).first_or_create do |user|
user.email = data[:email]
user.name = data[:name]
end
end
endToken model
Crearemos dos métodos de instancia: 'valid?' nos servirá para comprobar que nuestro token no esté vencido, y 'generate_token', el cual se ejecutará antes de crear un token. 'SecureRandom.hex' creará una cadena random hexadecimal, algo parecido a esto: '95c871e0a3285d0b744fea1987c4c439'. Y en la línea 16 le damos un valor de 3 meses a partir de la fecha actual a nuestro campo 'expires_at'.
class Token < ApplicationRecord
belongs_to :user
before_create :generate_token
def valid?
self.expires_at > DateTime.now
end
private
def generate_token
loop do
self.token = SecureRandom.hex
break unless Token.where(token: self.token).exists?
end
self.expires_at = 3.month.from_now
end
endUsers Controller
En nuestro método create primero validaremos que nos estén enviando la información del usuario a crear (params[:user]).
A partir de aquí colocaré la explicación como comentarios dentro del código ✍(◔◡◔)
# POST api/v1/users, params: { user: { email: 'giancarlos@devacademy.la', name: 'Giancarlos Isasi' } }
def create
if params[:user]
@user = User.with_token(params[:user]) # le pasamos la información que nuestro metodo 'with_token' usara para crear al usuario
@token = @user.tokens.create() # Al usuario que nos devuelve nuestro metodo 'with_token' le creamos un nuevo token
render "api/v1/users/show" # en esta vista mostraremos la info del usuario y su token respectivo
else
render json: { error: 'user params info is missing' } # solo si no nos envian la info necesaria
end
endNuestra vista 'show' tendrá lo siguiente:
# views/api/v1/users/show.json.jbuilder
json.(@token, :token)
json.(@user, :id, :name, :email)Si hacemos una request 'POST' enviando los respectivos parámetros, tendremos algo como esto:
{
"token": "8b7794a62dd440db597b3566f95dc6fb",
"id": 1,
"name": "Giancarlos Isasi",
"email": "giancarlos@devacademy.la"
}Nuestro Application Controller
Crearemos un método para identificar a qué usuario pertenece el token enviado. Por ejemplo, haciendo tests con RSpec, nuestra petición POST sería:
post 'api/v1/articles', params: { article: { title: 'my first API', body: 'my first api with Ruby on Rails!!' }, token: '8b7794a62dd440db597b3566f95dc6fb' }Observa que no enviamos ningún dato del usuario, tan solo enviamos su token.
class ApplicationController < ActionController::Base
# protect_from_forgery with: :null_session
protected
def authenticate # este método lo usaremos en nuestro controlador Articles para saber que usuario desea crear un articulo.
# Tambien nos servira para comprobar si el usuario es el dueño del articulo que desea actualizar o eliminar
token_of_params = params[:token] # obtenemos el token que nos pasaran en la petición
token = Token.find_by_token(token_of_params) # verificamos que el token exista en la base de datos
if !token.nil?
if token.valid? # comprobamos que el token sea valido con el método de instancia que creamos en nuestro modelo token
@current_user = token.user # obtenemos el usuario actual a partir del token enviado
else
render json: { error: 'token invalid :(' } # mensaje de error si el token ya expiro
end
else
render json: { error: 'token is missing :|' } # mensaje de error si el token no es enviado o no existe en la base de datos
end
end
endFinalmente nuestro ArticlesController
No es tan difícil como parece (̶◉͛‿◉̶)
class Api::V1::ArticlesController < ApplicationController
before_action :authenticate, only: [:create, :update, :destroy] # usando el metodo de nuestro applicationController verificamos que el token sea valido y autentificamos al usuario (este sera asignado a nuestra variable @current_user)
before_action :set_article, only: [:show, :update, :destroy] # el tipico metodo para encontrar al articulo, esta demas explicarlo
def index
@articles = Article.all
end
def show
end
def create
@article = @current_user.article.new(quiz_params) # ya tenemos el usuario identificado por lo que procedemos a crearle el articulo respectivo
if @article.save
render template: 'api/v1/articles/show' # hacemos un render a nuestra vista show para mostrar los datos del articulo creado
else
render json: { error: @article.errors }, status: :unprocessable_entity # si no pasan las validaciones entonces muestra los errors, deberia pasar pues nosotros no tenemos validaciones :|
end
end
def update
if @current_user == @article.user # aqui usamos el current_user para verificar que efectivamente el usuario sera el mismo que creó el articulo
@article.update(quiz_params) # una vez se ha validado que el usuario es el dueño del articulo, procedemos a hacer el update
render 'api/v1/quizzes/show' # mostramos los datos actualizados
else
render json: { error: 'you dont have permission' }, status: :unauthorized # si el usuario no es el mismo que creó el articulo, entonces se muestran los errores
end
end
private
def set_article
@article = Article.find(params[:id]) # identificamos el articulo a actualizar o eliminar
end
def quiz_params
params.require(:article).permit(:title, :body) # los tipicos strong params, no hay mucho que explicar
end
endFinalmente…
Entonces nuestro flujo quedaría de la siguiente forma: creamos un usuario, recibimos el token y luego usamos el token para hacer nuestras peticiones POST, UPDATE o DELETE.
WE DID IT! (͡° ͜ʖ ͡°)
Sé que hay algunas cosas por mejorar, el mismo hecho de haber hecho TDD, algunos DRY o mejores convenciones de código, pero lo dejaré ahí pues de lo contrario el post nunca acabaría [¡¡si ya vamos más de 1000 palabras!! ¿Cómo pudiste leerte todo? Te admiro, chaval, agrégame al Steam (͡• ͜ʖ ͡•)].
Puedes leer también los siguientes posts:
- Qué es y cómo crear una API en Ruby on Rails
- ¿No sabes cómo testear un controlador en Rails? ¡Este post es para ti!
- 5 tips para empezar con el pie derecho en Ruby on Rails
Gracias por leer; si te gustó, ¡compártelo con tus amigos! Good luck!
FIN…
