#235 OmniAuth Part 1
- Download:
- source codeProject Files in Zip (133 KB)
- mp4Full Size H.264 Video (25.2 MB)
- m4vSmaller H.264 Video (14.6 MB)
- webmFull Size VP8 Video (30 MB)
- ogvFull Size Theora Video (32.4 MB)
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.
OmniAuth es una gema, por lo que podemos incluirlo en la aplicación de la manera habitual modificando el fichero 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.
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.
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.
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.
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
.
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.
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["omniauth.auth"]
. Por ahora simplemente visualizaremos esta información como texto para que podamos ver su contenido.
class AuthenticationsController < ApplicationController def index end def create render :text => request.env["omniauth.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 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.
def create auth = request.env["omniauth.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
.
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.
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.
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.
<% 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.
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.
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.
def create auth = request.env[".auth"] current_user.authentications.find_or_create_by_proivder_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.