tutoriales.com

Creando un Sistema de Autenticación JWT desde Cero en Ruby con Sinatra

Este tutorial paso a paso te guía en la construcción de una API REST segura en Ruby utilizando Sinatra, aplicando autenticación basada en JWT, encriptación de contraseñas con BCrypt y manejo de sesiones sin estado.

Intermedio8 min de lectura7 views
Reportar error

🚀 Introducción a la Autenticación basada en Tokens en Ruby

La autenticación es uno de los pilares fundamentales en el desarrollo de aplicaciones web y APIs modernas. A diferencia de las sesiones tradicionales basadas en cookies, los JSON Web Tokens (JWT) ofrecen un enfoque sin estado (stateless) altamente escalable y perfecto para arquitecturas desacopladas.

En este tutorial completo, aprenderemos a construir un sistema de autenticación robusto utilizando Ruby y Sinatra, un framework ligero y rápido ideal para microservicios y APIs. No utilizaremos frameworks mastodónticos como Rails; en su lugar, implementaremos todo desde cero para comprender profundamente cómo funcionan los tokens bajo el capó.

💡 Consejo: Asegúrate de tener instalado Ruby (versión 3.0 o superior) y Bundler en tu entorno de desarrollo antes de comenzar.

🛠️ Configuración del Entorno y Dependencias

Para dar vida a nuestro proyecto, necesitamos crear un directorio de trabajo y configurar nuestro archivo Gemfile. Las gemas principales que utilizaremos son sinatra para el servidor web, jwt para la codificación y decodificación de tokens, bcrypt para el hash seguro de contraseñas, y sqlite3 junto con sequel para la persistencia de datos.

Crea un nuevo directorio para tu proyecto y añade el siguiente contenido en tu archivo Gemfile:

source 'https://rubygems.org'

gem 'sinatra', '~> 3.0'
gem 'sinatra-contrib', '~> 3.0'
gem 'jwt', '~> 2.7'
gem 'bcrypt', '~> 3.1'
gem 'sqlite3', '~> 1.6'
gem 'sequel', '~> 5.70'
gem 'dotenv', '~> 3.0'

Instala las dependencias ejecutando el siguiente comando en tu terminal:

bundle install

🗄️ Estructura de la Base de Datos y Modelo de Usuario

Antes de escribir la lógica de autenticación, necesitamos una tabla de usuarios para almacenar credenciales de forma segura. Utilizaremos Sequel como ORM ligero para interactuar con SQLite.

Crea un archivo llamado database.rb para inicializar la conexión y definir la estructura de la base de datos:

require 'sequel'
require 'bcrypt'

DB = Sequel.sqlite('development.db')

unless DB.table_exists?(:users)
  DB.create_table :users do
    primary_key :id
    String :email, null: false, unique: true
    String :password_hash, null: false
    DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
  end
end

class User < Sequel::Model
  plugin :validation_helpers

  def password=(new_password)
    self.password_hash = BCrypt::Password.create(new_password)
  end

  def authenticate(attempted_password)
    BCrypt::Password.new(password_hash) == attempted_password
  end

  def validate
    super
    validates_presence [:email, :password_hash]
    validates_unique :email
  end
end
CLIENTE Usuario/Navegador SERVIDOR API Validación BCrypt (Hash) Generación JWT (Sign) 1. POST /login (Credenciales) 2. HTTP 200 { "token": "..." } 3. GET /data + Bearer Token JWT Almacenado Auth

🔑 Implementación del Generador y Decodificador de JWT

El núcleo de nuestro sistema de seguridad es la emisión y validación de los tokens JWT. Crearemos una clase de utilidad para gestionar este proceso utilizando una clave secreta.

Crea un archivo llamado auth_token.rb:

require 'jwt'

class AuthToken
  SECRET_KEY = ENV.fetch('JWT_SECRET_KEY') { 'super_secret_key_change_me_in_production' }

  def self.encode(payload, exp = 24.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET_KEY)
  end

  def self.decode(token)
    body = JWT.decode(token, SECRET_KEY)[0]
    HashWithIndifferentAccess.new(body)
  rescue JWT::ExpiredSignature, JWT::VerificationError => e
    nil
  end
end
⚠️ Advertencia: Nunca hardcodees claves secretas en producción. Utiliza siempre variables de entorno gestionadas de forma segura mediante la gema dotenv.

🌐 Construcción de los Endpoints en Sinatra

Ahora uniremos todas las piezas en nuestra aplicación principal con Sinatra (app.rb). Implementaremos rutas para el registro de usuarios, el inicio de sesión (que genera el token) y una ruta protegida que requiere que el cliente envíe el token en las cabeceras HTTP.

require 'sinatra'
require 'sinatra/json'
require 'dotenv/load'
require_relative 'database'
require_relative 'auth_token'

class AuthApiApp < Sinatra::Base
  helpers do
    def current_user
      auth_header = request.env['HTTP_AUTHORIZATION']
      return nil unless auth_header && auth_header.start_with?('Bearer ')
      
      token = auth_header.split(' ').last
      decoded_payload = AuthToken.decode(token)
      return nil unless decoded_payload

      @current_user ||= User[decoded_payload['user_id']]
    end

    def authenticate!
      halt 401, json({ error: 'No autorizado. Token faltante o inválido.' }) unless current_user
    end
  end

  post '/api/v1/register' do
    data = JSON.parse(request.body.read)
    user = User.new(email: data['email'])
    user.password = data['password']

    if user.save
      status 201
      json({ message: 'Usuario registrado exitosamente', user_id: user.id })
    else
      status 422
      json({ errors: user.errors })
    end
  end

  post '/api/v1/login' do
    data = JSON.parse(request.body.read)
    user = User.where(email: data['email']).first

    if user && user.authenticate(data['password'])
      token = AuthToken.encode(user_id: user.id)
      json({ message: 'Login exitoso', token: token })
    else
      status 401
      json({ error: 'Credenciales inválidas' })
    end
  end

  get '/api/v1/profile' do
    authenticate!
    json({
      id: current_user.id,
      email: current_user.email,
      created_at: current_user.created_at
    })
  end
end

🧪 Pruebas de la API con cURL

Es momento de probar nuestro sistema de autenticación en funcionamiento. Inicia el servidor ejecutando:

ruby app.rb

1. Registrar un nuevo usuario

curl -X POST http://localhost:4567/api/v1/register \
  -H "Content-Type: application/json" \
  -d '{"email": "test@example.com", "password": "mi_password_seguro"}'

2. Iniciar sesión para obtener el token

curl -X POST http://localhost:4567/api/v1/login \
  -H "Content-Type: application/json" \
  -d '{"email": "test@example.com", "password": "mi_password_seguro"}'

La respuesta devolverá un JSON conteniendo tu token JWT. Cópialo y utilízalo en el siguiente paso.

3. Acceder a la ruta protegida

curl -X GET http://localhost:4567/api/v1/profile \
  -H "Authorization: Bearer <PEGA_TU_TOKEN_AQUI>"
📌 Nota: Si el token ha expirado o es manipulado, el servidor responderá automáticamente con un código de estado 401 Unauthorized.

📋 Resumen de Buenas Prácticas

Para mantener tu sistema de autenticación seguro en entornos de producción, ten en cuenta las siguientes recomendaciones:

HTTPS obligatorio: Transmite siempre los tokens mediante canales cifrados utilizando SSL/TLS.
Expiración corta: Configura expiraciones reducidas para los tokens de acceso y utiliza refresh tokens si es necesario.
Rotación de secretos: Cambia periódicamente la clave secreta utilizada para firmar los JWT.
¿Qué hacer si necesito revocar un token JWT?Dado que los JWT son sin estado, no se pueden invalidar directamente en el servidor antes de su expiración a menos que implementes una lista negra (blacklist) utilizando una base de datos en memoria como Redis.

🎉 Conclusión

¡Felicidades! Avanzado Has construido exitosamente un sistema de autenticación JWT completamente funcional desde cero en Ruby utilizando Sinatra. Este enfoque te otorga control absoluto sobre cada capa de seguridad de tu aplicación sin la complejidad innecesaria de frameworks monolíticos.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!