homeASCIIcasts

235: OmniAuth 

(view original Railscast)

Other translations: En It

Other formats:

Written by Juan Lupión

Hace un par de semanas, en el episodio 233[verlo, leerlo] tratamos acerca de Janrain Engage, que es un servicio centralizado donde se puede gestionar la autenticación con credenciales de sitios Twitter, OpenID y Facebook en nuestras aplicaciones. Se trata de un servicio muy interesante pero tiene como incoveniente que actúa como intermediario entre nuestra aplicación y el proveedor de autenticación. Sería mucho mejor si existiese una gema o plugin que permitiese incorporar autenticación de terceras partes en nuestra aplicación sin requerir de tal servicio externo.

Tal gema ha sido lanzada recientemente y se llama OmniAuth, que proporciona una forma única de autenticar contra diferentes servicios. Además, si el servicio contra el cual queremos realizar la autenticación no está incluido en OmniAuth es fácil añadir uno nuevo ya que OmniAuth no es sino una colección de middlewares de Rack, lo que nos da una gran flexibilidad a la hora de usarlo.

En el blog de Rails Rumble hay una interesante anotación donde se explica con detalle cómo crear la autenticación desde cero. Nosotros aquí veremos cómo integrar OmniAuth en una aplicación donde ya existe una solución de autenticación (Devise en nuestro caso, pero podemos integrarlo igual de fácilmente con Authlogic u otro esquema de autenticación).

Inclusión de OmniAuth en la aplicación

Trabajaremos con la aplicación que escribimos en el episodio 209 y que es una sencilla aplicación de lista de tareas que utiliza Devise para autenticar a los usuarios. Los enlaces de registro y de inicio de sesión llevan al usuario a una página donde puede suministrar un nombre de usuario y una clave. Comenzaremos con el inicio de sesión en una cuenta ya existente.

Sesión iniciada en la aplicación.

OmniAuth es una gema, por lo que podemos incluirlo en la aplicación de la manera habitual modificando el fichero Gemfile.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.0'

gem 'sqlite3-ruby', :require => 'sqlite3'
gem 'devise', '1.1.3'

gem 'omniauth'

Tras esto podemos ejecutar bundle install (o únicamente bundle) para instalar la gema así como sus dependencias, tarea de la que se ocupará bundler.

El siguiente paso es ir al directorio /config/initializers de la aplicación y crear un nuevo archivo que llamaremos omniauth.rb, si bien el nombre no es realmente importante. En este fichero añadiremos OmniAuth::Builder a los middlewares de la aplicación y definiremos los servicios contra los que permitiremos autenticar a nuestros usuarios.

/config/initializers/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, 'CONSUMER_KEY', 'CONSUMER_SECRET'
  provider :facebook, 'APP_ID', 'APP_SECRET'
  provider :linked_in, 'CONSUMER_KEY', 'CONSUMER_SECRET'
end

En esta aplicación sólo vamos a utilizar Twitter por lo que podemos eliminar las otras dos líneas. Para soportar la autenticación de usuarios a través de Twitter tendremos que dar de alta nuestra aplicación en Twitter visitando la página para desarrolladores y procediendo allí al registro. El formulario de registro es sencillo, y una vez finalizado el proceso recibiremos la clave y el secreto que tendremos que pegar en el fichero de inicialización.

/config/initializers/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, 's3dXXXXXXXXXXXX', 'lR23XXXXXXXXXXXXXXXXXXXX'
end

Si arrancamos el servidor podremos ver OmniAuth en acción. Si visitamos la URL auth/twitter seremos redirigidos a Twitter, donde se nos preguntará si queremos dar acceso a esta aplicación.

La página de Twitter solicita permiso para autenticar usando la aplicación de asciicasts

Si hacemos clic en “Allow” seremos llevados de vuelta a la aplicación con la URL /auth/twitter/callback. Nuestra aplicación tendrá que manejar esta URL para decidir qué hacer cuando un usuario ha iniciado la sesión. En nuestro caso crearemos un recurso separado para gestionar la respuesta al que llamaremos Authentication.

Se puede crear dicho modelo y su controlador por separado pero para que las cosas sean más fáciles usaremos el generador Nifty Scaffold de Ryan Bates para crear el modelo, la vista y el controlador de una tacada. El recurso Authentication tendrá una columna user_id, una columna provider que almacenará el nombre del proveedor de autenticación (esto es, Twitter, Facebook, etc.) y un uid que contendrá el identificador del usuario en el proveedor. Para el controlador querremos tener las acciones index, create y destroy.

$ rails g nifty:scaffold authentication user_id:integer
provider:string uid:string index create destroy

Una vez completado el comando tendremos que ejecutar la migración de base de datos.

$ rake db:migrate

A continuación tenemos que configurar la relación entre los modelos User y Authentication. Un usuario podrá autenticarse de varias maneras distintas, por lo que un usuario tendrá muchas autenticaciones.

/app/models/user.rb

class User < ActiveRecord::Base
  has_many :authentications
  # Include default devise modules. Others available are:
  # :token_authenticatable, :lockable, :timeoutable and :activatable
  # :confirmable,
  devise :database_authenticatable, :registerable, 
         :recoverable, :rememberable, :trackable, :validatable

  # Setup accessible (or protected) attributes for your model
  attr_accessible :email, :password, :password_confirmation
end

Igualmente, una Authentication pertenecerá a un User.

/app/models/authentication.rb

class Authentication < ActiveRecord::Base
  belongs_to :user
end

El generador automáticamente ha creado un controlador llamado AuthenticationsController que podemos usar para gestionar la respuesta de OmniAuth. Mapearemos la URL de respuesta a la acción create del controlador modificando el archivo de rutas.

/config/routes.rb

ProjectManage::Application.routes.draw do |map|
  match '/auth/:provider/callback' => 'authentications#create'
  devise_for :users
  resources :projects
  resources :tasks
  resources :authentications
  root :to => 'projects#index'
end

Nótense los dos puntos en la cadena del método match. Esto quiere decir que podemos aceptar cualquier proveedor y recibirlo como parámetro.

En la acción create podemos recuperar los detalles de autenticación con una llamada a request.env["rack.auth"] (en futuras versiones de OmniAuth será request.env["omniauth.auth"]). Por ahora simplemente visualizaremos esta información como texto para que podamos ver su contenido.

/app/controllers/authentication_controller.rb

class AuthenticationsController < ApplicationController
  def index
  end
  
  def create
    render :text => request.env["rack.auth"].to_yaml
  end
  
  def destroy
  end
end

Si ahora visitamos /auth/twitter y nos autenticamos en Twitter veremos que disponemos de una gran cantidad de información.

La respuesta de autenticación recibida de Twitter

La información devuelta es un hash compuesto de más hashes anidados. En la parte superior están los campos provider y uid que son los que nos interesan para guardarlos en nuestro modelo Authentication. Al final del archivo hay algo de información que podría interesarnos guardar en el modelo User.

user_info: 
  nickname: eifion
  name: Eifion
  location: North Wales
  image: http://a1.twimg.com/profile_images/434158309/Adium_Icon_normal.png
  description: Web developer using .Net and Windows by day and Ruby and Rails on OS X the rest of the time. I run http://asciicasts.com
  urls: 
    Website: http://asciicasts.com

Almacenamiento de la información de autenticación

Tendremos que modificar la acción create para cambiar su comportamiento según el estado actual del usuario pero antes tendremos que contemplar el caso más simple: si un usuario tiene una sesión iniciada y escoge vincular su cuenta con Twitter queremos guardar esta nueva autenticación en los datos de este usuario.

/app/controllers/authentications_controller.rb

def create
  auth = request.env["rack.auth"] current_user.authentications.create(:provider => auth ['provider'], :uid => auth['uid'])
  flash[:notice] = "Authentication successful."
  redirect_to authentications_url
end

En la acción create recuperamos la información de autenticación en un hash y luego creamos una nueva Authentication para el usuario basándonos en dos de los parámetros de la información devuelta por el proveedor de autenticación. A continuación crearemos el mensaje de flash y volveremos a redigir a la acción index.

Si visitamos /auth/twitter y nos autenticamos ahora seremos redirigidos a la página de index donde veremos los detalles de la nueva autenticación que acabamos de añadir con el proveedor correcto y el uid.

Inicio de sesión con éxito.

Mejora del aspecto de la página principal

Hay una utilísima cuenta de GitHub llamada Authbuttons que ofrece iconos para muchos servicios diferentes de autenticación. Podemos utilizarlos para mejorar el aspecto de la página donde el usuario escoge el método de autenticación para iniciar sesión. Por comodidad vamos a gestionar todo esto dentro de la acción index de AuthenticationsController pero en una aplicación de producción pondríamos todo esto en una página separada.

Sin embargo antes de hacer esto, tendremos que hacer un cambio en la acción index. El código generado automáticamente recuperará todas las autenticaciones por lo que lo cambiaremos para que sólo recupere las autenticaciones del usuario actual.

/app/controllers/authentications_controller.rb

def index
  @authentications = current_user.authentications if current_user
end

Es también un buen momento para hacer un cambio parecido en la acción destroy de forma que no se puedan eliminar autenticaciones que no pertenezcan al usuario actual.

/app/controllers/authentications_controller.rb

def destroy
  @authentication = current_user.authentications.find(params[:id])
  @authentication.destroy
  flash[:notice] = "Successfully destroyed authentication."
  redirect_to authentications_url
end

Lo siguiente que haremos será cambiar el código de la vista. Es un poco extenso, pero no es nada complicado.

/app/views/authentications/index.html.erb

<% title "Sign In" %>
<% if @authentications %>
 <% unless @authentications.empty? %>
  <p><strong>You can sign in to this account using:</strong></p>
   <div class="authentications">
    <% for authentication in @authentications %>
     <div class="authentication">
      <%= image_tag "#{authentication.provider}_32.png", ↵
      :size => "32x32" %>
      <div class="provider"><%= authentication.provider.titleize↵   
      %></div>
      <div class="uid"><%= authentication.uid %></div>
       <%= link_to "X", authentication, :confirm => ↵
      'Are you sure you want to remove this authentication ↵ 
      option?', :method => :delete, :class => "remove" %>
     </div>
     <% end %>
     <div class="clear"></div>
    </div>
   <% end %>
   <p><strong>Add another service to sign in with:</strong></p>
  <% else %>
  <p><strong>Sign in through one of these services:</strong></p>
<% end %>

<a href="/auth/twitter" class="auth_provider">
  <%= image_tag "twitter_64.png", :size => "64x64", ↵
  :alt => "Twitter" %>Twitter</a>
<a href="/auth/facebook" class="auth_provider">
  <%= image_tag "facebook_64.png", :size => "64x64", ↵
  :alt => "Facebook" %>Facebook</a>
<a href="/auth/google_apps" class="auth_provider">
  <%= image_tag "google_64.png", :size => "64x64", ↵
  :alt => "Google" %>Google</a>
<a href="/auth/open_id" class="auth_provider">
  <%= image_tag "openid_64.png", :size => "64x64", ↵
  :alt => "OpenID" %>OpenID</a>
<div class="clear"></div>

La página de autenticaciones tendrá un aspecto mucho mejor cuando la recarguemos.

La página de autenticación tras añadir los iconos.

Por supuesto nuestra aplicación todavía no soporta todos los servicios mostrados pero el pantallazo nos muestra el aspecto que tendría si lo hiciera. Pero la página, a pesar de tener mejor aspecto, tiene un bug oculto. Si tenemos la sesión iniciada y autenticada vía Twitter e intentamos vincular otra vez nuestra cuenta con Twitter se creará un segundo registro de autenticación cuando debería usar el ya existente.

La aplicación se ha autenticado incorrectamente dos veces con el mismo proveedor.

Es un problema muy fácil de resolver. Sólo tenemos que modificar la acción create del controlador AuthenticationController para que utilice find_or_create_by_ para ver si ya existe una autenticación antes de crear una nueva instancia del modelo.

/app/controllers/authentication_controller.rb

def create
  auth = request.env["rack.auth"]
  current_user.authentications.find_or_create_by_provder_and_uid↵
  (auth['provider'], auth['uid'])
  flash[:notice] = "Authentication successful."
  redirect_to authentications_url
end

Si eliminamos manualmente la segunda autenticación y luego nos autenticamos por Twitter otra vez veremos que el usuario sigue teniendo una única autenticación asociada.

Un requisito importante que todavía no hemos acometido es la autenticación cuando el usuario todavía no ha iniciado sesión. En la acción create hemos asumido que tenemos disponible un usuario que ha iniciado la sesión normalmente. ¿Qué ocurre cuando alguien llega al sitio por primera vez e intenta iniciar la sesión mediante Twitter? Veremos esta problemática en el siguiente episodio.