Saltar al contenido

¿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.

7 min de lectura
  • ruby
  • rails
  • api
  • security

Publicado originalmente en Medium

¿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].

Recapacita

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
 
end

Agregaremos dos nuevos modelos:

$ rails g model Token token expires_at:datetime user:references
$ rails g model Article title body:text user:references

Nuestro 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
end

User 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
 
end

Token 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
end

Users 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
end

Nuestra 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
 
end

Finalmente 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
end

Finalmente…

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:

Gracias por leer; si te gustó, ¡compártelo con tus amigos! Good luck!

FIN…

Gracias por leer