#360 Facebook Authentication
- Download:
- source codeProject Files in Zip (191 KB)
- mp4Full Size H.264 Video (29.8 MB)
- m4vSmaller H.264 Video (15.9 MB)
- webmFull Size VP8 Video (17.6 MB)
- ogvFull Size Theora Video (40 MB)
En la aplicación de ejemplo que se muestra a continuación tenemos un listado de películas en las que los usuarios pueden escribir sus opiniones. Queremos añadir un poco de interacción social integrando nuestra aplicación con Facebook de forma que los usuarios puedan compartir las reseñas con sus amigos, en este episodio veremos cómo hacerlo.
Registro de la aplicación en Facebook
La forma más sencilla de integrar Facebook en una aplicación es mediante el uso de los plugins sociales, con los que podemos añadir un botón “Like”, una sección de comentarios y más. Si queremos algo un poco más integrado con la funcionalidad de nuestro sitio tendremos que ir más allá y será necesario registrar nuestra aplicación en el sitio para desarrolladores de Facebook con el objetivo de poder comunicarnos con la plataforma Open Graph y su API. Para registrar la aplicación tan sólo tenemos que hacer clic en el botón “Create New App” del sitio para desarrolladores y darle un nombre y un espacio de nombres a nuestra aplicación. Nosotros hemos escogido “Cinematron” pero en este caso lo importante es que el espacio de nombres debe ser único de entre todas las aplicaciones de Facebook (es probable que tengamos que probar con alguna variación del nombre de nuestra aplicación hasta dar con uno que no haya usado nadie). Facebook ha llegado a un acuerdo con Heroku para alojar aplicaciones web, por lo que si dejamos marcada la casilla del fondo de la página quedará todo configurado para usar Heroku, si bien podemos usar cualquier alojamiento que queramos.
Para enviar este formulario tenemos que rellenar un captcha. Tras esto iremos a una página que muestra cierta información sobre nuestra aplicación. La página contiene un identificativo de aplicación y un secreto, que son ambos imprescindibles para que nuestra aplicación se comunique con la API de Facebook. Hay varias opciones que podemos especificar aquí para personalizar nuestra aplicación. Una que tenemos que habilitar es “Website with Facebook Login”, dado que es la permitirá que los usuarios de nuestra aplicación se autentiquen mediante Facebook. Tendremos que especificar la URL de nuestro sitio, por motivos de seguridad. Mientras la aplicación se encuentre en desarrollo podemos dejarlo a http://localhost:3000/
pero cuando la aplicación pase a producción tendremos que cambiar el dominio para que coincida con nuestra aplicación. Para probar la aplicación en local tras su paso a producción podemos modificar el fichero /etc/hosts
de nuestra máquina de forma que el dominio de producción apunte a nuestra máquina local.
La siguiente opción es “Auth Dialog”. Es lo que mostrará Facebook a los usuarios cuando traten de autenticarse. Rellenaremos este formulario con detalles acerca de nuestra aplicación.
Nótese que no hemos proporcionado URLs para la política de privacidad ni los términos de servicio. Facebook exige su presencia a no ser que nuestra aplicación se encuentre en modo de pruebas. Tras guardar los cambios podremos previsualizar el diálogo que contiene exactamente lo que verá el usuario cuando se autentique. Lo siguiente que tendremos que hacer es modificar la configuración avanzada, que es donde podemos activar el modo de pruebas (Sandbox mode), que aparece desactivado por defecto. Es el único cambio que hemos de hacer en esta pantalla.
Integración de Facebook con nuestra aplicación
Ya estamos listos para integrar la aplicación en Facebook. Lo primero que haremos es añadir un enlace para que el usuario pueda autenticarse mediante Facebook. Para gestionar la autenticación utilizaremos la gema OmniAuth, que es útil porque soporta la autenticación tanto en el servidor como en el cliente usando JavaScript. El README detalla bien cómo gestionar la autenticación con Facebook. Como viene siendo habitual empezaremos añadiendo la gema al Gemfile
de la aplicación y luego ejecutaremos bundle
para instalarla.
gem 'omniauth-facebook'
A continuación, crearemos un nuevo fichero de inicialización donde pondremos la configuración de OmniAuth.
OmniAuth.config.logger = Rails.logger Rails.application.config.middleware.use OmniAuth::Builder do provider :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_SECRET'] end
Aquí hacemos que las trazas de OmniAuth aparezcan en el log de Rails y añadiremos OmniAuth a los middlewares de Rack, indicando que el proveedor a integrar es Facebook, incluyendo las variables de entorno con el identificativo de aplicación y el secreto. Cuando reiniciemos la aplicación y visitemos /auth/facebook
deberíamos ir a la página de inicio de sesión de Facebook. Si recibimos un error, deberíamos comprobar que las variables de entorno que le estamos pasando a OmniAuth sean las correctas.
Al hacer clic en ‘Go To App’ para iniciar la sesión, iremos de vuelta a la aplicación. Veremos un error de rutas al hacerlo, lo cual no nos debería sorprender porque la aplicación todavía no es capaz de gestionar esta URL a la que le redirige Facebook. Esto se corrige de forma similar a como lo hicimos en el episodio 241. Primero añadiremos las rutas para gestionar las URLs que necesita OmniAuth.
Cinema::Application.routes.draw do match 'auth/:provider/callback', to: 'sessions#create' match 'auth/failure', to: redirect('/') match 'signout', to: 'sessions#destroy', as: 'signout' resources :movies do resources :reviews end root to: 'movies#index' end
Cuando Facebook nos redirija a la aplicación buscará la acción create
del controlador SessionsController
. Nuestra aplicación todavía no tiene este controlador, así que lo tendremos que escribir ahora.
class SessionsController < ApplicationController def create user = User.from_omniauth(env["omniauth.auth"]) session[:user_id] = user.id redirect_to root_url end end
El código de este controlador depende de un modelo User
que todavía no hemos escrito, pero que buscará o creará un usuario basándose en los datos que le pasa el hash de OmniAuth. Conviene plantearse si necesitamos persistir los datos del usuario de esta manera, porque si no tenemos que mantener ninguna otra asociación con el usuario bien podríamos guardar este hash en la sesión en lugar de crear un registro en la base de datos que es como lo haremos nosotros.
$ rails g model user provider uid name oauth_token oauth_expires_at:datetime $ rake db:migrate
En el modelo User
almacenaremos las credenciales provider
y uid
que nos da OmniAuth, así como nombre, oauth_token
y oauth_expires_at
. Los tokens OAuth de Facebook no son eternos, por lo que debemos almacenar la fecha de expiración. Leyendo el README de OmniAuth para Facebook encontraremos un hash de ejemplo con detalles con los datos que la aplicación recibe de Facebook. Algunos de estos datos los queremos guardar en la base de datos, como por ejemplo la URL de la imagen para poder mostrar la imagen de perfil del usuario.
A continuación, modificaremos nuestro modelo User
y escribiremos el código del método from_omniauth
que invocaremos desde SessionsController
.
class User < ActiveRecord::Base def self.from_omniauth(auth) where(auth.slice(:provider, :uid)).first_or_initialize.tap do |user| user.provider = auth.provider user.uid = auth.uid user.name = auth.info.name user.oauth_token = auth.credentials.token user.oauth_expires_at = Time.at(auth.credentials.expires_at) user.save! end end end
Este código es distinto del que hemos visto antes. Aquí intentamos encontrar un usuario que coincida con los valores de provider
y uid
recibidos en el hash de Omniauth, y luego llamamos a first_or_initialize
sobre el resultado. Se trata de un método introducido en Rails 3.2 que o bien devuelve el primer resultado o bien inicializa un nuevo registro con los parámetros que se le pasan. Luego se invoca tap
para pasar esa instancia de User
al bloque. Dentro del bloque se establecen varios atributos del usuario basándonos en el hash de OmniAuth. Nótese, sin embargo, que estamos usando las claves del hash como si fueran métodos, esto es así porque OmniAuth utiliza la gema Hashie, que hace precisamente esto. Por último guardamos el registro y lo devolvemos. Un detalle interesante es que estamos estableciendo estos atributos cada vez, incluso para registros de User
ya existentes. Esto es importante por que hace que no sólo tengamos siempre el último token de autenticación sino que si el usuario cambia su nombre o cualquier otro atributo en Facebook, estos valores se vean actualizados en nuestra base de datos.
Todavía nos quedan por hacer algunos cambios. En ApplicationController
tenemos que añadir un método para recuperar el usuario actual.
class ApplicationController < ActionController::Base protect_from_forgery private def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end helper_method :current_user end
En el fichero de layout de la aplicación tenemos que añadir el código que permite a los usuarios iniciar y terminar la sesión.
<div id="user_nav"> <% if current_user %> Signed in as <strong><%= current_user.name %></strong>! <%= link_to "Sign out", signout_path, id: "sign_out" %> <% else %> <%= link_to "Sign in with Facebook", "/auth/facebook", id: "sign_in" %> <% end %> </div>
Ya podemos hacer la prueba. Si recargamos la portada de la aplicación veremos el enlace para iniciar la sesión, y si hacemos clic en él habremos iniciado la sesión directamente porque ya habíamos autorizado la aplicación con Facebook anteriormente.
Autenticación en el cliente
Facebook tiene publicado un SDK en JavaScript que podemos usar para autenticar al usuario en el lado del cliente de forma que no parezca que deja nuestra aplicación para luego regresar a ella. Vamos a añadir esta funcionalidad a nuestra aplicación, haciendo que degrade a la solución basada en el servidor. Lo único que hace falta es un poco de JavaScript, o CoffeeScript en nuestro caso. Bajo el directorio /app/assets/javascripts
vamos a crear un nuevo fichero facebook.js.coffee.erb
. Ponemos la extensión extra .erb
para poder incluir contenido dinámico.
jQuery -> $('body').prepend('<div id="fb-root"></div>') $.ajax url: "#{window.location.protocol}//connect.facebook.net/en_US/all.js" dataType: 'script' cache: true window.fbAsyncInit = -> FB.init(appId: '<%= ENV["FACEBOOK_APP_ID"] %>', cookie: true) $('#sign_in').click (e) -> e.preventDefault() FB.login (response) -> window.location = '/auth/facebook/callback' if response.authResponse $('#sign_out').click (e) -> FB.getLoginStatus (response) -> FB.logout() if response.authResponse true
Tras la carga del DOM insertamos un elemento div
con id
de fb-root
en la parte superior del body
del documento, que es donde Facebook espera encontrar este elemento. Luego hacemos una petición AJAX a los SDKs en JavaScript de Facebook. Esta llamada cargará el código de forma asíncrona, interpretando lo que nos devuelvan como si fuera un script. Tras su ejecución, este script invocará la función fbAsyncInit
que se haya definido en el objeto de la ventana en JavaScript, así que cuando se invoque esta función sabremos que el SDK ha terminado de cargar y está listo para ser usado. Aprovecharemos este evento para inicializar algunos valores incluyendo el identificador de la aplicación que establecemos usando la misma variable de entorno de antes (aquí es donde es útil usar ERb) También habilitamos la opción que hace que las credenciales del usuario se guardan en una cookie para poder accederlas en el lado del servidor.
Una vez que ha terminado toda esta inicialización tenemos que atender al evento click
del enlace del inicio de sesión. En lugar del comportamiento por defecto, haremos que este clic active la función FB.login
que mostrará el diálogo al usuario para que inicie la sesión en Facebook. Una vez que hagan esto, será redirigidos a /auth/facebook/callback
para que termine el proceso de autenticación y se genere el registro de User
(o lo que quiera que sea que estemos usando) en el servidor. También podríamos activar esto mediante AJAX en lugar de redirigir al usuario si no queremos que cambie de página.
Por último, tenemos código que se ejecuta cuando el usuario hace clic en el enlace para cerrar la sesión. Aquí se comprueba si el usuario tiene dicha sesión, en cuyo caso se cierra. La función siempre devuelve true de forma que no se bloquee el comportamiento por defecto del enlace, que en este caso también lo lleva a la URL que cierra la sesión.
Ya podemos hacer la prueba. Hemos quitado la aplicación de nuestra lista de aplicaciones Facebook autorizadas para poder ver los efectos del inicio de sesión. Al hacer clic en este enlace aparece un diálogo emergente que nos pide autorizar la aplicación, una vez que lo hacemos habremos iniciado la sesión y se habrá creado el registro de usuario correspondiente.
Si hacemos clic en el enlace para cerrar la sesión, habremos finalizado la sesión tanto en nuestra aplicación como en Facebook, por lo que si volvemos a intentar iniciar la sesión primero tendremos que hacerlo en Facebook, tras lo cual habremos iniciado sesión automáticamente también en la aplicación.
Esta solución en JavaScript puede dar algunos problemas extraños. Por ejemplo, en la consola del navegador veremos un mensaje de advertencia, pese a lo cual el JavaScript parece funcionar, si bien sería conveniente imaginarse cómo podemos parar esta advertencia.
A veces también podría haber casos en los que la cookie de sesión de Facebook y la de nuestra aplicación quedan desacopladas, lo cual no es generalmente un problema pero sí puede dar lugar a comportamientos anómalos, así que debemos vigilar que esto no ocurra.
Interacción con Facebook
Tras autenticar al usuario, ¿cómo interactuamos con Facebook? Podemos hacerlo mediante el SDK en JavaScript o bien en Ruby con una gema llamada Koala. El README de esta gema incluye una explicación de su uso con la API de Facebook, y aprenderemos más sobre la materia en el episodio de pago de esta semana.