#274 Remember Me & Reset Password
- Download:
- source codeProject Files in Zip (109 KB)
- mp4Full Size H.264 Video (17.5 MB)
- m4vSmaller H.264 Video (11.9 MB)
- webmFull Size VP8 Video (14.3 MB)
- ogvFull Size Theora Video (27.6 MB)
Aunque existan muchas soluciones de autenticación para nuestras aplicaciones Rails, esto no quiere decir que debamos descartar escribir nuestra propia implementación. Veíamos cómo hacerlo en el episodio 250 [verlo, leerlo], mientras que en el episodio 270 [verlo, leerlo] vimos cómo con Rails 3.1 es todavía más fácil utilizando el método has_secure_password
que genera los cifrados de las claves.
La autenticación desarrollada en estos episodios era muy básica, por lo que vamos a dedicar este episodio a añadir un par de mejoras. Primero añadiremos la posibilidad de recordar al usuario para aquellos que quieran iniciar la sesión automáticamente y luego añadiremos la funcionalidad de recuperación de contraseñas para los usuarios que las pierdan. Implementaremos estas funcionalidades extendiendo la aplicación del episodio 270, que funciona con Rails 3.1 pero los cambios que veremos aquí funcionan también en Rails 3.0.
Cómo recordar a los usuarios
Cuando los usuarios inician sesión en la aplicación su id
se almacena en la sesión. Esto tiene lugar en la acción create
del controlador SessionsController
.
def create user = User.find_by_email(params[:email]) if user && user.authenticate(params[:password]) session[:user_id] = user.id redirect_to root_url, :notice => "Logged in!" else flash.now.alert = "Invalid email or password" render "new" end end
Cuando los usuarios con sesión iniciada cierran el navegador la cookie de sesión se borra, por lo que tienen que volver a introducir sus credenciales la próxima vez que visiten la aplicación. Vamos a cambiar la cookie de sesión por una de tipo permanente para que el identificador de usuario persista entre sesiones.
El problema más obvio con esto es que los códigos de id
quedan almacenados como enteros secuenciales. Si el id
queda almacenado en una cookie permanente sería muy fácil que un usuario malicioso cambiese el valor de la misma y viese los datos de otros usuarios. Para evitar esto generaremos un token único para cada usuario que sea imposible de adivinar y que será lo que guardaremos en la cookie.
Cada usuario tendrá su propio token que quedará almacenado en la base de datos así que tenemos que crear una migración para añadir un campo llamado auth_token
a la tabla de usuarios y luego migrarla.
$ rails g migration add_auth_token_to_users auth_token:string
Tendremos que generar este token único cuando se crea el usuario, así que escribiremos un método llamado generate_token
en el modelo User
. Este método tendrá un argumento column
por si más adelante necesitamos añadir otro tipo de tokens.
class User < ActiveRecord::Base attr_accessible :email, :password, :password_confirmation has_secure_password validates_presence_of :password, :on => :create before_create { generate_token(:auth_token) } def generate_token(column) begin self[column] = SecureRandom.urlsafe_base64 end while User.exists?(column => self[column]) end end
La clase SecureRandom
de ActiveSupport
es la encargada de generar la cadena aleatoria. Hacemos que la cadena sea única generando tokens hasta dar con uno que haya sido asignado a ningún usuario. Llamamos a este método en un filtro before_create
para generar el token cuando la instancia del usuario se guarda por primera vez. Si ya tuviésemos usuarios en la base de datos tendríamos que generarles sus tokens mediante una tarea rake, lo que no veremos hoy.
Modificaremos la acción create
de SessionsController
para almacenar el token en una cookie cuando el usuario inicia la sesión. Cambiaremos la acción destroy
de forma que se elimine dicha cookie cuando el usuario cierra la sesión.
class SessionsController < ApplicationController def new end def create user = User.find_by_email(params[:email]) if user && user.authenticate(params[:password]) cookies.permanent[:auth_token] = user.auth_token redirect_to root_url, :notice => "Logged in!" else flash.now.alert = "Invalid email or password" render "new" end end def destroy cookies.delete(:auth_token) redirect_to root_url, :notice => "Logged out!" end end
Tal y como está, todos los usuarios quedarán con sesión iniciada permanentemente, lo que no tiene por qué ser lo que deseen, así que añadiremos una casilla de verificación para que puedan escoger si desean este comportamiento. Los cambios son muy sencillos, tan sólo tenemos que añadir la casilla junto con una etiqueta que explique su cometido.
<h1>Log in</h1> <%= form_tag sessions_path do %> <div class="field"> <%= label_tag :email %> <%= text_field_tag :email, params[:email] %> </div> <div class="field"> <%= label_tag :password %> <%= password_field_tag :password %> </div> <div class="field"> <%= label_tag :remember_me %> <%= check_box_tag :remember_me, 1, params[:remember_me] %> </div> <div class="actions"><%= submit_tag "Log in" %></div> <% end %>
Con esto ya podemos modificar SessionsController
para que la cookie se establezca sólo si el usuario marca la casilla. De lo contrario, se utilizará una cookie de sesión.
def create user = User.find_by_email(params[:email]) if user && user.authenticate(params[:password]) if params[:remember_me] cookies.permanent[:auth_token] = user.auth_token else cookies[:auth_token] = user.auth_token end redirect_to root_url, :notice => "Logged in!" else flash.now.alert = "Invalid email or password" render "new" end end
Todavía nos queda otro cambio por hacer. Hay que cambiar ApplicationController
de forma que lea el token de autenticación a partir de la cookie en lugar del id
de un usuario a partir de la sesión.
class ApplicationController < ActionController::Base protect_from_forgery private def current_user @current_user ||= User.find_by_auth_token( cookies[:auth_token]) if cookies[:auth_token] end helper_method :current_user end
Ya podemos probar esto. Si iniciamos la sesión veremos la casilla para recordar la sesón, si la marcamos y luego cerramos y volvemos a abrir el navegador veremos que la sesión se inicia automáticamente.
Recuperación de contraseñas
Veamos ahora cómo permitir a los usuarios cambiar una contraseña que hayan olvidado. Empezaremos añadiendo el enlace correspondiente en el formulario de inicio de sesión.
<h1>Log in</h1> <%= form_tag sessions_path do %> <div class="field"> <%= label_tag :email %> <%= text_field_tag :email, params[:email] %> </div> <div class="field"> <%= label_tag :password %> <%= password_field_tag :password %> </div> <p><%= link_to "forgotten password?", new_password_reset_path %></p> <div class="field"> <%= label_tag :remember_me %> <%= check_box_tag :remember_me, 1, params[:remember_me] %> </div> <div class="actions"><%= submit_tag "Log in" %></div> <% end %>
El enlace apunta a new_password_reset_path
que es parte de un recurso que todavía no hemos escrito. Creemos el controlador PasswordResets
con la acción new
.
$ rails g controller password_resets new
Queremos tratar este controlador como si fuese un recurso REST, por lo que modificaremos el fichero de rutas y cambiaremos la ruta generada por una llamada a resources
.
Auth::Application.routes.draw do get "logout" => "sessions#destroy", :as => "logout" get "login" => "sessions#new", :as => "login" get "signup" => "users#new", :as => "signup" root :to => "home#index" resources :users resources :sessions resources :password_resets end
Esto nos servirá aunque no se trate de un recurso respaldado por un modelo.
En la acción new
crearemos un formulario para que el usuario pueda introducir su dirección de correo y solicitar el restablecimiento de su clave. El formulario queda así:
<h1>Reset Password</h1> <%= form_tag password_resets_path, :method => :post do %> <div class="field"> <%= label_tag :email %> <%= text_field_tag :email, params[:email] %> </div> <div class="actions"><%= submit_tag "Reset Password" %></div> <% end %>
En este formulario tenemos que usar form_tag
porque no se trata de un recurso ActiveRecord. El formulario hace un POST a la acción create
del controlador PasswordResets
, que escribiremos a continuación. Buscará al usuario por la dirección de correo y luego le enviará las instrucciones para restablecer su contraseña, lo que se hará en el método send_password_reset
del modelo User
.
def create user = User.find_by_email(params[:email]) user.send_password_reset if user redirect_to root_url, :notice => "Email sent with password reset instructions." end
El mensaje aparece tanto si el usuario se encuentra como si no, para complicarle un poco las cosas a los usuarios maliciosos que traten de saber si un usuario determinado existe en la base de datos.
A continuación escribiremos el método send_password_reset
, en el que enviaremos un correo que contendrá el token para la petición de restablecimiento de contraseña. Queremos que el token expire después de un tiempo determinado, por ejemplo un par de horas, de forma que el enlace sea válido sólo durante un breve tiempo tras el envío del correo. Para esto nos harán falta un par de campos más en la tabla de usuarios. Escribamos la migración correspondiente.
$ rails g migration add_password_reset_to_users password_reset_token:string password_reset_sent_at:datetime
En send_password_reset
utilizaremos el metodo generate_token
de antes para crear un token de restablecimiento de clave. Igualmente estableceremos también el campo password_reset_sent_at
para saber cuándo debería expirar el token. Después de guardar los cambios en el modelo del usuario se lo pasaremos al UserMailer
de forma que envíe el correo.
def send_password_reset generate_token(:password_reset_token) self.password_reset_sent_at = Time.zone.now save! UserMailer.password_reset(self).deliver end
Todavía no tenemos el UserMailer
por lo que lo escribiremos a continuación.
$ rails g mailer user_mailer password_reset
En el mailer asignaremos el usuario a una variable de instancia para poder acceder a ella desde la plantilla y poder establecer el asunto y el destinatario.
class UserMailer < ActionMailer::Base default from: "from@example.com" def password_reset(user) @user = user mail :to => user.email, :subject => "Password Reset" end end
La plantilla contendrá las instrucciones necesarias para restablecer la contraseña, así como un enlace.
To reset your password click the URL below.
<%= edit_password_reset_url(@user.password_reset_token) %>
If you did not request your password to be reset please ignore this email and your password will stay as it is.
El enlace en el correo lleva al usuario a la acción edit
de PasswordResetsController
. No se trata del enfoque REST más ortodoxo pero en este caso nos servirá. Para que las URLs funcionen en los correos tenemos que modificar la configuración de nuestro entorno para añadir la siguiente linea en development.rb
.
Auth::Application.configure do # Other config items omitted. config.action_mailer.default_url_options = { :host => "localhost:3000" } end
Añadiremos también una línea similar en production.rb
con el nombre del dominio de producción.
Hagamos una prueba. Si visitamos la página de restablecimiento de contraseña e introducimos nuestra dirección de correo deberíamos leer un correo con las instrucciones para restablecer la contraseña.
En el log de desarrollo veremos los detalles del correo.
Sent mail to eifion@asciicasts.com (65ms) Date: Thu, 14 Jul 2011 20:18:48 +0100 From: from@example.com To: eifion@asciicasts.com Message-ID: <4e1f4118af661_31a81639e544652a@noonoo.home.mail> Subject: Password Reset Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit To reset your password click the URL below. http://localhost:3000/password_resets/DeStUAsv2QTX_SR3ub_N0g/edit If you did not request your password to be reset please ignore this email and your password will stay as it is. Redirected to http://localhost:3000/ Completed 302 Found in 1889ms
El correo incluye un enlace a la URL de restablecimiento de contraseña, que lleva el token y el parámetro id
.
A continuación debemos escribir la acción edit
, en ella recuperaremos al usuario por su token único. Nótese que utilizaremos el método con exclamación para emitir un error 404 en caso de que el usuario no sea encontrado.
def edit @user = User.find_by_password_reset_token!(params[:id]) end
Crearemos también un formulario en la vista correspondiente para que el usuario pueda volver a introducir su contraseña.
<h1>Reset Password</h1> <%= form_for @user, :url => password_reset_path(params[:id]) 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 %> <div class="field"> <%= f.label :password %> <%= f.password_field :password %> </div> <div class="field"> <%= f.label :password_confirmation %> <%= f.password_field :password_confirmation %> </div> <div class="actions"><%= f.submit "Update Password" %></div> <% end %>
Utilizamos form_for
porque en este caso estamos modificando un recurso. Debido a ello tenemos que establecer el parámetro :url
para que el formulario no sea enviado como POST a UsersController
. En su lugar se envía a la acción update
de PasswordResetsController
pasándo el testigo de reinicio del clave como id
. El formulario contiene una sección para mostrar los mensajes de error y los campos para introducir y confirmar la nueva contraseña.
A continuación escribiremos la acción update
, que primero comprueba que el token de restauración de contraseña tiene menos de dos horas y si no es así, se redirige al usuario otra vez al formulario. Si el token es lo suficientemente reciente tratamos de actualizar al usuario. Si esto tiene éxito redirigiremos a la página principal y mostraremos un mensaje, si la operación no tiene éxito es que debe haber ocurrido un error en el formulario por lo que lo volvemos a mostrar.
def update @user = User.find_by_password_reset_token!(params[:id]) if @user.password_reset_sent_at < 2.hours.ago redirect_to new_password_reset_path, :alert => "Password ↵ reset has expired." elsif @user.update_attributes(params[:user]) redirect_to root_url, :notice => "Password has been reset." else render :edit end end
Podemos hacer la prueba pegando la URL del correo electrónico en la ventana del navegador.
Si introducimos una clave y una confirmación incorrectas veremos un mensaje de error, y si enviamos correctamente el formulario la contraseña cambiará.
Esta técnica de restablecimiento de contraseña se puede usar para desarrollar otras funcionalidades, por ejemplo para confirmar las nuevas cuentas, que consistiría en algo similar al restablecimiento de contraseña pero cuando se hace clic en el enlace del correo lo que hacemos es apuntar en la base de datos que la cuenta está confirmada.
Con esto cerramos este episodio en el que hemos visto el inicio de sesión automático y el restablecimiento de claves. Algunas herramientas, como Devise, proporcionan esta funcionalidad de manera automática pero puede ser interesante desarrollarla desde cero.