Autenticación con Ruby on Rails en modo API usando JWT

Carlos Castro
5 min readAug 22, 2017

--

Con el apogeo de la arquitectura REST para el desarrollos de APIs y tras ver los estragos que pasan las compañías con el monolito de Rails, además de que Rails 5 viene con la opción de modo API, decidí experimentar un poco y cree un pequeño API que uso para probar diversas tecnologías de frontend.

A continuación voy a mostrar como implementar un API, incluyendo autenticación, usando rails.

¿En qué consiste nuestro API?

La verdad es que es un API muy sencillo que va a usar un token para autenticar las llamadas al API y para esto voy a usar una gema: Knock. Esta gema utiliza un JWT (JSON Web Token), que es un estándar de la industria para poder identificar seguramente al cliente que está realizando una solicitud al API.

¿Por qué no un cookie?

Por definición, los API de arquitectura REST no tienen una noción de estado del cliente, es decir que distintos clientes pueden acceder al API y sus respectivos estados no tienen injerencia en los procesos internos del API. En el caso de los cookies, esto no ocurre pues una sesión tiene que ser creada por cada cliente en el servidor y almacenada en una base de datos. El cliente a su vez debe guardar la identificación de la sesión para poder comunicarse con el servidor. Los navegadores se encargan de guardar estas identificaciones. Si los únicos clientes de un API fuesen los navegadores, esto podría funcionar medianamente bien a pesar de la carga extra a la base de datos aunque no sería un API REST.

En cambio, el JWT es una firma digital generada a través de un algoritmo de codificación (on confundir con encriptación). Está firma digital es usada por el cliente en cada solicitud al API y el API sólo debe decodificar el JWT para validar la solicitud sin necesidad de verificar ningún estado en la base de datos.

Código fuente

Si tienes curiosidad de ver todo el código, puedes acceder a mi repositorio en github. Además ahí hay información sobre el demo.

Bueno, hasta ahí con el preámbulo. ¡Empecémos!

Manos a la Obra

Ejecutemos el comando para iniciar una nueva aplicación en Rails en modo API usando PostgreSQL y sin incluir Minitest. No incluyo MiniTest porque no lo uso, pues suelo integrar Rspec aunque por motivos de brevedad no voy a incluir pruebas en este tutorial.

rails new rails-api --api -d postgresql -T

Si en vez deseas usar SQLite e incluir MiniTest entonces ejecuta:

rails new rails-api --api

Ahora editemos el archivo Gemfile para incluir todas las gemas que necesitamos:

Ahora instalemos la nuevas gemas:

bundle install

Entidad Usuario

Como ya mencioné en un inicio, nuestro API sólo va a tener una entidad, la entidad Usuario. Esta entidad va a estar representada por el modelo User (Usuario en inglés) y va a contar con los campos name (nombre en inglés), email y password_digest. El último campo es necesario para poder utilizar la funcionalidad has_secure_password.

rails generate scaffold User name:string email:string \
password_digest:string

O alternativamente usando “g”, la abreviación del comando “generate”:

rails g scaffold User name:string email:string \
password_digest:string

Ahora debemos ejecutar la migración. Pero antes, si estamos usando PostgreSQL, debemos primero crear la base de datos:

# Sólo si usas PostgreSQL
createdb rails-api_development

Ahora si, ejecutemos la migración.

rails db:migrate# Alternativamente, también puedes ejecutar
# rake db:migrate

Knock para autenticar con JWT

Como ya incluimos la gema en un inicio, sólo nos queda usar el generador para instalar knock en la aplicación:

rails generate knock:install

Este comando crea el archivo config/initializers/knock.rb en donde se pueden aplicar configuraciones avanzadas.

Integrando Knock al modelo User

A continuación generamos el controlador con la acción necesaria para la autenticación. El generador también se encarga de agregar la ruta necesaria en el archivo config/route.rb.

rails generate knock:token_controller user

Ahora debemos incluir “has_secure_password” en nuestro modelo para indicar que queremos usar BCrypt para verificar contraseña con la contraseña encriptada en la base de datos. La gema bcrypt se encarga de la encriptación, el único requisito previo es incluir el atributo “password_digest”, lo cual ya hicimos al generar nuestro modelo.

No está demás resaltar que agregué también validaciones para los atributos del modelo y también agregué el método de devolución (callback) “before_save” para que cada vez que se tratar de guardar un email, este sea almacenado sólo con letras minúsculas.

Protejamos a nuestros controladores

Primero, hagamos que nuestros controladores tengan la habilidad de ser autenticables a través de Knock:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include Knock::Authenticable
end

Ahora, en un REST API pueden haber entidades y métodos que requieren autenticación y otros que no. A continuación veamos como exigir autenticación para una sólo acción en el controlador:

Cabe destacar que en la función user_params, en dónde se genera una lista blanca de los campos que pueden ser modificados, remplazamos :password_digest por :password y :password_confirmation.

Habilitar CORS

Cross-Origin Resource Sharing (CORS), se refiere a la habilidad de compartir recursos de un dominio a otro. Esto puede ser vídeos, imágenes, hojas de estilo, etc. Sin embargo, debido a la política del mismo origen los navegadores no pueden realizar solicitudes AJAX desde el documento actual (léase página actual) a otro documento pero que pertenezca a un dominio distinto. De modo que un API debe habilitar específicamente que dominios pueden acceder al API. Si es un API público entonces se puede habilitar a cualquier dominio. En nuestro caso habilitaremos a todos los dominios debido a que no queremos restricciones para probar nuestro API desde cualquier dominio, incluyendo nuestro “localhost”.

Respuesta HTTP en JSON

Para esto ya incluimos en inicio la gema active_model_serializers. Por defecto va a incluir todos los campos de la migración (db/migrate/*_create_users.rb), pero vamos a retirar cualquier mención del atributo “password” (contraseña).

# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :email
end

Retoques finales

Voy a agregar un “endpoint” más para obtener la información del usuario actual: /users/current

Para esto agreguemos la acción en el controlador:

# app/controllers/users_controller.rb
def current
render json: current_user
end

Y también debemos agregar nuestra ruta en el archivo routes.rb:

# config/routes.rb
Rails.application.routes.draw do
post ‘user_token’ => ‘user_token#create’
get ‘users/current’ => ‘users#current’
resources :users
end

Tiempo de prueba

Primero, iniciemos el servidor en localhost:3000:

rails server
# Alternativamente, también puedes ejecutar
# rails s

Segundo, creemos dos usuarios usando el comando curl:

curl -H "Content-type: application/json" -d '{"user":{"name":"Luke Skywalker","email":"luke@starwars.com","password": "12345678","password_confirmation":"12345678"}}' http://localhost:3000/userscurl -H "Content-type: application/json" -d '{"user":{"name":"Han Solo","email":"han@starwars.com","password": "12345678","password_confirmation":"12345678"}}' http://localhost:3000/users

Tercero, listemos los usuarios:

http://localhost:3000/users

Cuarto, autentiquémosnos:

curl -H “Content-type: application/json” -d ‘{“auth”: {“email”: “luke@starwars.com”, “password”: “12345678”}}’ http://localhost:3000/user_token

Observemos que la respuesta nos va a devolver el JWT:

{"jwt":"este_es_el_jwt"}

Quinto, obtengamos la información de nuestro propio usuario (Luke Skywalker):

curl -H "Authorization: Bearer el_jwt_recibido_previamente" http://localhost:3000/users/current

Sexto, obtengamos la información de otro usuario, en este caso Han Solo:

curl -H "Authorization: Bearer el_jwt_recibido_previamente" http://localhost:3000/users/2

Demo online

El API está online en https://carlos-rails-api.herokuapp.com/. Podemos verificar rápidamente con el navegador:

https://carlos-rails-api.herokuapp.com/users

Gracias

Gracias por haber llegado hasta el final. Si deseas hacer preguntas, compartir una observación o dar una opinión, por favor no dejes de comentar.

--

--