homeASCIIcasts

250: Autenticación desde cero 

(view original Railscast)

Other translations: En Ja

Written by Juan Lupión

Casi todas las aplicaciones Rails utilizan algún tipo de autenticación. Las librerías más usadas para implementar autenticación basada en claves son Authlogic, Devise, Restful Authentication y Clearance, ¿cuál deberíamos usar? Han transcurrido varios meses desde las últimas versiones de Authlogic y Restful Authentication, lo cual nos deja sólo con Devise y Clearance, que son engines de Rails lo que quiere decir que incorporan controladores y vistas que gestionarán la autenticación por nosotros. Esto puede que no sea del todo ideal porque en algo tan importante con la autenticación podemos acabar sobreescribiendo mucha de la funcionalidad que proporcionan estas capas de controladores y vistas. En estos casos los engines pierden gran parte de su atractivo y nuestra aplicación se complica.

Por supuesto los engines tienen su utilidad y no hay ninguna razón para dejar de usarlos pero cabe considerar otras alternativas. En lo que a la autenticación se refiere, los generadores pueden ser una solución mejor porque todo el código reside en nuestra aplicación, lo que hace que sea más fácil de personalizar. Por ejemplo, los Nifty Generators de Ryan Bates incluyen un generador de autenticación que entrega un código muy simple para implementar la autenticación basada en claves en nuestras aplicaciones. Pero en este episodio no vamos a hablar de Nifty Authentication, en vez de eso vamos a ver cómo implementar la autenticación basada en claves desde cero. Así, la próxima vez que usemos para estos menesteres un engine o un generador tendremos mejor idea de qué es lo que está ocurriendo por debajo.

Empezando

Como vamos a hacer la autenticación desde cero, iremos creando una nueva aplicación Rails 3 a la que bautizaremos auth.

$ rails new auth

A continuación cambiaremos al nuevo directorio auth y crearemos el proceso de registro. Necesitamos un controlador para crear usuarios, así que creamos el controlador UsersController que tendrá una acción new.

$ rails g controller users new

Nos hará falta también un modelo User donde guardar las direcciones de correo de los usuarios y sus claves. Por razones obvias no deberíamos jamás guardar las claves en texto claro, por lo que guardaremos la clave con una clave y una sal.

$ rails g model user email:string password_hash:string password_salt:string

Una vez que hemos creado el modelo podemos migrar la base de datos para crear la tabla de los usuarios.

$ rake db:migrate

A continuación tenemos que escribir el código de las acciones new y create en el controlador UsersController.

/app/controllers/users_controller.rb

class UsersController < ApplicationController
  def new
    @user = User.new
  end
  
  def create
    @user = User.new(params[:user])
    if @user.save
      redirect_to root_url, :notice => "Signed up!"
    else
      render "new"
    end
  end
end

Se trata de un controlador normal. En la acción new creamos un nuevo User, mientras que en la acción create creamos un nuevo User basándonos en los parámetros recibidos (que vendrán del formulario). Si ese nuevo User es válido redirigimos a la portada del sitio (que todavía no existe) y de lo contrario volvemos a mostrar la plantilla new.

Escribamos ahora la plantilla de new. Tendrá un formulario con los campos email, password y password_confirmation, así como el código necesario para mostrar los errores de validación.

/app/views/users/new.html.erb

<h1>Sign Up</h1>

<%= form_for @user do |f| %>
  <% if @user.errors.any? %>
    <div class="error_messages">
      <h2>Form is invalid</h2>
      <ul>
        <% for message in @user.errors.full_messages %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  <p>
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </p>
  <p>
    <%= f.label :password %><br />
    <%= f.password_field :password %>
  </p>
  <p>
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation %>
  </p>
  <p class="button"><%= f.submit %></p>
<% end %>

El modelo User no posee los atributos password o password_confirmation pero más adelante crearemos métodos de acceso que los gestionen. También haremos un par de cambios al fichero de rutas. El generador de controlador ha generado la siguiente ruta.

get "users/new"

Vamos a cambiar esta ruta para que sea /sign_up, que apuntará a users#new, y le daremos el nombre <ocde>sign_up</ocde>. También crearemos una ruta raíz que apunte al formulario de registro. Por último, añadiremos un recurso para el modelo usuario de forma que la acción create funcione correctamente.

/config/routes.rb

Auth::Application.routes.draw do
  get "sign_up" => "users#new", :as => "sign_up"
  root :to => "users#new"
  resources :users
end

Si ahora arrancamos el servidor y visitamos la página de registro veremos un mensaje de error.

El error que recibimos al ver el formulario.

El error se produce porque en el formulario aparece el campo password para el modelo User pero en la base de datos no existe dicha columna y por consiguiente el atributo no existe. Vamos a crear este atributo en el modelo, así como un atributo para controlar la confirmación de clave. Para password_confirmation podemos utilizar validates_confirmation_of, que también comprueba si la clave y su validación coinciden. También podemos añadir otras validaciones al formulario, tales como comprobar la presencia de una dirección de correo.

/app/models/user.rb

class User < ActiveRecord::Base
  attr_accessor :password
  validates_confirmation_of :password
  validates_presence_of :password, :on => :create
  validates_presence_of :email
  validates_uniqueness_of :email
end

Cuando creamos el modelo User incorporamos los campos password_hash y password_salt en la base de datos donde se guardará la versión encriptada de la clave. Cuando se envíe el formulario tendremos que cifrar el valor del campo password y guardar el hash y la sal en estos dos campos. Para cifrar las claves podemos usar bcrypt, incorporándolo en nuestra aplicación mediante la gema bcrypt-ruby. Primero añadiremos una referencia a dicha gema en el Gemfile y luego la instalaremos con Bundler.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.3'

gem 'sqlite3-ruby', :require => 'sqlite3'
gem 'bcrypt-ruby', :require => 'bcrypt'

Lo siguiente será modificar el modelo User para que cifre la clave antes de guardarla. Podemos hacerlo utilizando un filtro before_save que invoque a un nuevo método llamado encrypt_password, que comprobará que la clave esté presente y, en su caso, generará la sal y el hash correspondiente utilizando los métodos generate_salt y hash_secret de BCrypt::Engine.

/app/models/user.rb

class User < ActiveRecord::Base
  attr_accessor :password
  before_save :encrypt_password
  
  validates_confirmation_of :password
  validates_presence_of :password, :on => :create
  validates_presence_of :email
  validates_uniqueness_of :email
  
  def encrypt_password
    if password.present?
      self.password_salt = BCrypt::Engine.generate_salt
      self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)
    end
  end
end

Cuando ahora un usuario se de de alta en la base de datos se guardarán correctamente los atributos password_hash y password_salt. Si visitamos el formulario de registro veremos que funciona: si lo rellenamos correctamente seremos redirigidos a la página principal. Si luego miramos la tabla users de la base de datos veremos que el nuevo usuario aparece con su clave cifrada.

$ rails dbconsole
SQLite version 3.6.12
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .mode column
sqlite> .header on
sqlite> SELECT * FROM users;
id          email                  password_hash                                                 password_salt                  created_at                  updated_at                
----------  ---------------------  ------------------------------------------------------------  -----------------------------  --------------------------  --------------------------
1           eifion@asciicasts.com  $2a$10$Jh./oyCeThSChUCY8Of6F.fiHP8m4gMkZNjUR3vsDgvupUPgumNs.  $2a$10$Jh./oyCeThSChUCY8Of6F.  2011-01-26 21:51:56.399518  2011-01-26 21:51:56.399518

Inicio de sesión

Ya estamos a mitad de camino. Los usuarios se pueden registrar pero todavía no pueden iniciar la sesión. Para ello vamos a crear un nuevo controlador llamado sessions que controlará el inicio de sesión.

$ rails g controller sessions new

En el fichero new recién generado crearemos el formulario para iniciar la sesión.

/app/views/sessions/new.html.erb

<h1>Log in</h1>

<%= form_tag sessions_path do %>
  <p>
    <%= label_tag :email %><br />
    <%= text_field_tag :email, params[:email] %>
  </p>
  <p>
    <%= label_tag :password %><br />
    <%= password_field_tag :password %>
  </p>
  <p class="button"><%= submit_tag %></p>
<% end %>

Hemos utilizado form_tag en lugar de form_for porque este último implica que existe un recurso detrás del formulario y dado que no tenemos un modelo Session, está claro que este no es nuestro caso. El formulario envía un POST a sessions_path, que coincidirá con la acción create de SessionController. El formulario tiene dos campos, uno para la dirección de correo y otro para la clave.

Tenemos que hacer algunos ajustes en las rutas. Cambiaremos la ruta sessions/new (que había creado el generador) por una llamada log_in. También tendremos que añadir resources :sessions para que funcione bien el formulario de inicio de sesión.

/config/routes.rb

Auth::Application.routes.draw do
  get "log_in" => "sessions#new", :as => "log_in"

  get "sign_up" => "users#new", :as => "sign_up"
  root :to => "users#new"
  resources :users
  resources :sessions
end

En SessionsController tenemos que escribir la acción create para controlar la autenticación cuando el usuario inicia la sesión, en la que tendremos que validar al usuario llamando a un nuevo método de clase en el modelo User. Este método devolverá una instancia de User si el usuario ha sido autenticado correctamente, en cuyo caso además guardaremos el id del usuario en una variable de sesión y luego redirigiremos a la página principal, mostrando un mensaje para que el usuario sepa que ha iniciado la sesión adecuadamente. Si el intento falla, lo indicaremos con un mensaje distinto y volveremos a mostrar el formulario. Nótese que usamos flash.now porque estamos mostrando una página, mas que redirigiendo a otra.

/app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  def new
  end
  
  def create
    user = User.authenticate(params[:email], params[:password])
    if user
      session[:user_id] = user.id
      redirect_to root_url, :notice => "Logged in!"
    else
      flash.now.alert = "Invalid email or password"
      render "new"
    end
  end
end

Ahora tenemos que escribir el método User.authenticate. Primero intentará encontrar el usuario por la dirección de correo recibida, si lo encuentra cifrará la clave del formulario de la misma manera que hizo cuando el usuario inició sesión, utilizando el campo password_salt. Si esto es correcto y el hash cifrado coincide con el que hay almacenado en la base de datos, entonces la clave es correcta y se devolverá la instancia del usuario, en caso contrario se devolverá nil. La sentencia else no es estrictamente necesaria en Ruby, porque siempre se devuelve nil por defecto, pero se deja aquí por claridad.

/app/models/user.rb

def self.authenticate(email, password)
    user = find_by_email(email)
    if user && user.password_hash == BCrypt::Engine.hash_secret ↵
      (password, user.password_salt)
      user
    else
      nil
    end
  end
end

Antes de probar este último código modificaremos el layout de la aplicación para que se muestren los mensajes recibidos por flash:

/app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
<head>
  <title>Auth</title>
  <%= stylesheet_link_tag :all %>
  <%= javascript_include_tag :defaults %>
  <%= csrf_meta_tag %>
</head>
<body>
  <% flash.each do |name, msg| %>
    <%= content_tag :div, msg, :id => "flash#{name}" %>
  <% end %>

<%= yield %>

</body>
</html>

Si ahora intentamos iniciar la sesión con un nombre de usuario o clave incorrectos veremos otra vez el formulario de inicio de sesión y el mensaje nos dirá que no hemos iniciado sesión.

Si no se inicia la sesión se muestra un mensaje.

Si introducemos la información de inicio de sesión correcta seremos redirigidos a la página prinicpal con un mensaje que indica que tenemos una sesión iniciada.

somos dirigidos a la página principal si iniciamos la sesión correctamente.

Cierre de sesión

Aún nos queda por implementar una manera de cerrar la sesión. Lo primero que haremos será implementar una nueva ruta: "log_out".

/config/routes.rb

Auth::Application.routes.draw do
  get "log_in" => "sessions#new", :as => "log_in"
  get "log_out" => "sessions#destroy", :as => "log_out"

  get "sign_up" => "users#new", :as => "sign_up"
  root :to => "users#new"
  resources :users
  resources :sessions
end

Esta ruta apunta al método destroy de SessionsController, el cual cerrará la sesión del usuario simplemente eliminando la variable de sesión user_id y dirigiendo a la página principal.

/app/controllers/sessions_controller.rb

def destroy
  session[:user_id] = nil
  redirect_to root_url, :notice => "Logged out!"
end

Podemos hacer la prueba si visitamos /log_out. Al hacerlo seremos llevados a la página principal y veremos el mensaje correspondiente.

Hemos sido llevados a la página principal al cerrar la sesión.

Mostrando enlaces

En lugar de tener que teclear URLs en la barra de direcciones para iniciar y cerrar la sesión, estaría mucho mejor que tuviésemos enlaces en la página para poder hacer esto. Esto se hace añadiendo el siguiente código en el fichero de layout justo antes del código que muestra los mensajes flash:

/app/views/layouts/application.html.erb

<div id="user_nav">
  <% if current_user %>
    Logged in as <%= current_user.email %>
    <%= link_to "Log out", log_out_path %>
  <% else %>
    <%= link_to "Sign up", sign_up_path %> or 
    <%= link_to "Log in", log_in_path %>
  <% end %>
</div>

Todavía no tenemos el método current_user así que vamos a escribirlo. Lo pondremos en ApplicationController.

/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery
  helper_method :current_user
  
  private
  def current_user
    @current_user ||= User.find(session[:user_id]) if ↵
      session[:user_id]
  end
end

Este método recuperará el usuario actual por su id (utilizando el valor de la variable de sesión) y cachea el resultado en una variable de instancia. Haremos también que sea un método helper para poder usarlo en el código de las vistas.

Si se recarga la página ya veremos los enalces de registro e inicio de sesión. Si iniciamos la sesión veremos tanto la información de la cuenta actual como un enlace para cerrar la sesión.

También tenemos enlaces para iniciar y cerrar la sesión.

En este episodio hemos visto mucho código pero ya tenemos un sistema de autenticación completamente funcional. Toda la lógica de autenticación sucede en el modelo User, principalmente en los métodos self.authenticate y encrypt_password, que tienen un código muy sencillo.

Hemos visto que no es muy complicado construir nuestro propio sistema de autenticación en lugar de usar un engine. Podemos además personalizar los controladores y vistas exactamente como neceistamos. Lo que hemos hecho aquí es prácticamente el mínimo imprescindible y en una aplicación de producción tendríamos que añadir más validaciones al modelo User, tales como validar la longitud de la clave y el formato del e-mail.

Es importante añadir al modelo User código para restringir los atributos que se pueden asignar masivamente. Esto se puede hacer con attr_accessible en el modelo User.

/app/models/user.rb

class User < ActiveRecord::Base
  attr_accessible :email, :password, :password_confirmation
  
  #rest of code omitted
end

Así evitaremos que nadie actualice el password_hash u otros campos en el modelo User.

Con esto termina este episodio. Aunque las gemas de autenticación ya existentes son muy buenas, siempre viene bien saber cómo funcionan por dentro. Si además de la autenticación basada en claves queremos añadir autenticación de terceras partes podemos ver el episodio 241 [verlo, leerlo] que trata de OmniAuth.