#250 Authentication from Scratch
- Download:
- source codeProject Files in Zip (103 KB)
- mp4Full Size H.264 Video (23.6 MB)
- m4vSmaller H.264 Video (16.4 MB)
- webmFull Size VP8 Video (42.9 MB)
- ogvFull Size Theora Video (33.4 MB)
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
.
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.
<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.
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 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.
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.
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
.
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.
<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.
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.
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.
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:
<!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 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.
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"
.
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.
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.
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:
<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
.
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.
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
.
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.