#304 OmniAuth Identity
- Download:
- source codeProject Files in Zip (112 KB)
- mp4Full Size H.264 Video (24.3 MB)
- m4vSmaller H.264 Video (13.1 MB)
- webmFull Size VP8 Video (15.3 MB)
- ogvFull Size Theora Video (27.2 MB)
OmniAuth ha alcanzado hace poco la versión 1.0 y dispone de varias novedades interesantes. En este episodio veremos una nueva estrategia llamada OmniAuth Identity que permite que los usuarios creen una cuenta simplemente especificando un nombre de usuario y un correo electrónico en lugar de utilizar un proveedor externo.
Cómo funciona por ahora nuestra aplicación
Debajo se muestra una captura de pantalla de la aplicación con la que estamos trabajando. Ya hemos configurado OmniAuth con tres proveedores externos.
Si iniciamos la sesión con uno de estos servicios, por ejemplo Twitter, iremos a la web de Twitter donde nos preguntarán si queremos autorizar la aplicación para que acceda al detalle de nuestra cuenta en Twitter. Si lo aceptamos la aplicación recuperará la información de nuestro perfil de Twitter y tendremos la sesión iniciada.
Esta autorización sólo será necesaria la primera vez, tras la cual podremos iniciar la sesión en nuestra aplicación mediante Twitter haciendo clic en el icono de la página principal.
Se trata de una forma muy cómoda de permitir a los usuarios que inicien la sesión, pero estamos excluyendo a aquellos que no quieran registrarse mediante uno de estos servicios. Podemos usar OmniAuth Identity para que estos usuarios tengan la opción de crear una cuenta con su propia clave en el sitio.
Antes de empezar veremos parte del código de la aplicación para hacernos una idea de su funcionamiento. El código fuente está basado en la aplicación del episodio 241, que merece la pena consultar si no estamos familiarizados con OmniAuth.
Ha habido varios cambios en OmniAuth desde que se escribió el episodio, y uno de los más significativos ha sido la reorganización del Gemfile. Ahora cada proveedor tiene su propia gema de OmniAuth, por lo que hay que incluir la gema correcta para cada proveedor que queramos soportar.
gem 'omniauth-twitter' gem 'omniauth-facebook' gem 'omniauth-google-oauth2'
El inicializador de OmniAuth tiene el mismo aspecto que antes. Se añade OmniAuth como middleware y se incluyen los proveedores que deseamos utilizar.
Rails.application.config.middleware.use OmniAuth::Builder do provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET'] provider :google_oauth2, ENV['GOOGLE_KEY'], ENV['GOOGLE_SECRET'] provider :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_SECRET'] end
La aplicación tiene un controlador SessionsController
cuya acción create
es la que se activa por la callback de OmniAuth. En esta acción intentamos crear el usuario basado en el hash de OmniAuth y guardamos el id
de dicho usuario en una variable de sesión.
class SessionsController < ApplicationController def new end def create user = User.from_omniauth(env["omniauth.auth"]) session[:user_id] = user.id redirect_to root_url, notice: "Signed in!" end def destroy session[:user_id] = nil redirect_to root_url, notice: "Signed out!" end def failure redirect_to root_url, alert: "Authentication failed, please try again." end end
Recuperamos el usuario en el modelo User
con el método from_omniauth
.
class User < ActiveRecord::Base def self.from_omniauth(auth) find_by_provider_and_uid(auth["provider"], auth["uid"]) || create_with_omniauth(auth) end def self.create_with_omniauth(auth) create! do |user| user.provider = auth["provider"] user.uid = auth["uid"] user.name = auth["info"]["name"] end end end
El método comprueba en primer lugar si ya existe un usuario con el mismo identificador y proveedor. En tal caso se devuelve dicho usuario, y si no se encuentra se llama al método create_with_omniauth
que creará dicho usuario basándose en la información del hash que ha pasado OmniAuth, que ha sufrido un cambio que es que el parámetro user_info
ahora se llama info
, por lo que podemos tener problemas si actualizamos nuestro código a OmniAuth 1.0.
Cómo añadir OmniAuth Identity a una aplicación
Lo que hemos visto es suficiente para gestionar la autenticación con OmniAuth. A continuación añadiremos OmniAuth Identity para que los usuarios puedan crear una cuenta sin que medie un servicio externo de autenticación. Como vimos antes, cada proveedor de OmniAuth requiere el uso de una gema separada por lo que lo primero que haremos será añadir la gema omniauth-identity
al Gemfile.
# To use ActiveModel has_secure_password gem 'bcrypt-ruby', '~> 3.0.0' gem 'omniauth-twitter' gem 'omniauth-facebook' gem 'omniauth-google-oauth2' gem 'omniauth-identity'
La gema necesita bcrypt-ruby
para cifrar las claves, pero no la declara como dependencia por lo que es necesario añadir también bcrypt-ruby
al Gemfile. Como siempre, tenemos que ejecutar bundle
para que se instalen todas las gemas.
A continuación tenemos que ir al inicializador de OmniAuth para añadir al nuevo proveedor. No tenemos que añadir más parámetros en este proveedor.
Rails.application.config.middleware.use OmniAuth::Builder do provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET'] provider :google_oauth2, ENV['GOOGLE_KEY'], ENV['GOOGLE_SECRET'] provider :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_SECRET'] provider :identity end
Nos hará falta un modelo para almacenar la información para iniciar la sesión. Por defecto este modelo se debe llamar identity
pero si queremos podemos personalizarlo. Tendremos que darle los atributos name
, email
y password_digest
.
$ rails g model identity name:string email:string password_digest:string
Migremos la base de datos para añadir la tabla.
$ rake db:migrate
El README de OmniAuth nos indica lo que tenemos que hacer para que el modelo Identity funcione con varios ORMs, incluyendo ActiveRecord. Tenemos que cambiar el modelo de forma que herede de OmniAuth::Identity::Models::ActiveRecord
en lugar de ActiveRecord::Base
.
class Identity < OmniAuth::Identity::Models::ActiveRecord end
Ya casi hemos acabado. Lo único que tenemos que hacer es mostrar un enlace al nuevo proveedor en el formulario de registro junto a los enlaces de los proveedores externos.
<p> <strong>Don’t use these services?</strong> <%= link_to "Create an account", "/auth/identity/register" %> or <%= link_to "login", "/auth/identity" %> with a password. </p>
Tendremos que reiniciar el servidor para que aparezcan los cambios en OmniAuth, tras visitar la página de registro veremos los enlaces de registro e inicio de sesión. Empezaremos registrando una nueva cuenta.
OmniAuth Identity incluye un pequeño formulario de registro, tras rellenarlo habremos completado el registro e iremos a nuestra página de perfil.
La página de inicio de sesión tiene un aspecto parecido pero con sólo dos campos. El campo de inicio de sesión espera la dirección de correo que introducimos cuando nos dimos de alta, aunque esto puede no parecer obvio.
Cómo crear formularios personalizados de registro e inicio de sesión
Con esto ya tenemos funcionando el registro e inicio de sesión, sin haber tenido que cambiar casi nada en nuestra aplicación porque Identity utiliza la misma interfaz que el resto de OmniAuth para los otros proveedores. Con esto dicho, debemos tener en cuenta algunos problemas que pueden surgir. Si nuestro modelo Identity
tiene validaciones y se rellena mal el formulario el usuario no verá ningún error. Para comprobar esto, añadamos algunas validaciones en el modelo Identity
.
class Identity < OmniAuth::Identity::Models::ActiveRecord validates_presence_of :name validates_uniqueness_of :email validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i end
Si introducimos una dirección de correo no válida en el formulario de registro e intentamos crear una cuenta, volveremos al formulario sin ninguna indicación acerca de qué hicimos mal.
Igualmente, el formulario de inicio de sesión tampoco indica que el campo “Login” espera que el usuario introduzca su dirección de correo. Hay opciones para personalizar este formulario pero no son especialmente exhaustivas. Es más fácil crear nuestros propios formularios en la misma aplicación y así podremos tener completo control sobre su comportamiento y aspecto.
Empecemos con el formulario de inicio de sesión. En lugar de mostrar un enlace al formulario de inicio de sesión de OmniAuth en la página de registro lo que haremos será mostrar nuestro propio formulario de inicio de sesión:
<p> <strong>Don’t use these services?</strong> <%= link_to "Create an account", "/auth/identity/register" %> or login below. </p> <%= form_tag "/auth/identity/callback" do %> <div class="field"> <%= label_tag :auth_key, "Email" %><br> <%= text_field_tag :auth_key %> </div> <div class="field"> <%= label_tag :password %><br> <%= password_field_tag :password %> </div> <div class="actions"><%= submit_tag "Login" %></div> <% end %>
Este formulario envía un POST a /auth/identity/callback
que es donde el formulario original de OmniAuth envía sus datos. También le daremos los mismos nombres a los campos de formulario, y cuando recarguemos la página de registro veremos un formulario de inicio de sesión que podemos usar para introducir nuestros datos e iniciar la sesión igual que con el otro formulario.
Podemos hacer los mismo para cambiar el formulario de registro aunque esta vez tendremos que crear un nuevo controlador.
$ rails g controller identities
Como vamos a tratar este controlador como un recurso tendremos que modificar el fichero de rutas para añadirlo.
Auth::Application.routes.draw do root to: "sessions#new" match "/auth/:provider/callback", to: "sessions#create" match "/auth/failure", to: "sessions#failure" match "/logout", to: "sessions#destroy", :as => "logout" resources :identities end
El controlador tendrá una acción new
. Cabe la posibilidad de que haya fallado la validación del formulario de registro y el objeto identity
esté almacenado en una variable de entorno de Rack, por lo que lo recuperaremos de ahí para ver si hay mensajes que mostrar.
class IdentitiesController < ApplicationController def new @identity = env['omniauth.identity'] end end
La plantilla asociada es bastante grande, pero no hay nada raro en ella:
<h1>New Account</h1> <%= form_tag "/auth/identity/register" do %> <% if @identity && @identity.errors.any? %> <div class="error_messages"> <h2><%= pluralize(@identity.errors.count, "error") %> prohibited this account from being saved:</h2> <ul> <% @identity.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= label_tag :name %><br> <%= text_field_tag :name, @identity.try(:name) %> </div> <div class="field"> <%= label_tag :email %><br> <%= text_field_tag :email, @identity.try(:email) %> </div> <div class="field"> <%= label_tag :password %><br> <%= password_field_tag :password %> </div> <div class="field"> <%= label_tag :password_confirmation %><br> <%= password_field_tag :password_confirmation %> </div> <div class="actions"><%= submit_tag "Register" %></div> <% end %>
El formulario de esta plantilla envía un POST a /auth/identity/register
, en la parte superior del formulario mostramos los errores que hubiera; debajo están los mismos campos de formulario que tiene el formulario de OmniAuth. Rellenaremos los campos de nombre y email si existen en el objeto identity
.
Tendremos que modificar el enlace a “Create an account” de la página de registro para que ahora apunte a nuestro formulario en vez de al de OmniAuth.
<p> <strong>Don’t use these services?</strong> <%= link_to "Create an account", new_identity_path %> or login below. </p>
Cuando hagamos clic en este enlace iremos a nuestro formulario, en el que podemos registrar una nueva cuenta.
Nuestro nuevo formulario funciona muy bien para crear cuentas nuevas pero si rellenamos mal los datos y enviamos el formulario volveremos al formulario de registro estándar de OmniAuth. Para corregir esto debemos ir al inicializador de OmniAuth y modificar el proveedor identity
que escribimos antes:
Rails.application.config.middleware.use OmniAuth::Builder do provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET'] provider :google_oauth2, ENV['GOOGLE_KEY'], ENV['GOOGLE_SECRET'] provider :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_SECRET'] provider :identity, on_failed_registration: lambda { |env| IdentitiesController.action(:new).call(env) } end
La opción que hemos añadido (on_failed_registration
) recibe como argumento una aplicación Rack, que hemos configurado para que apunte a la acción new
de IdentitiesController
para que se muestre nuestro nuevo formulario. Hay un problema al llamar a esta acción directamente: en modo de desarrollo la acción no se recargará por lo que hemos envuelto el código con un lambda
para evitar que la acción quede cacheada.
Tenemos que reiniciar el servidor Rails para que estos cambios surtan efecto, tras lo cual el formulario se comportará como es de esperar si introducimos datos incorrectos.
Ya tenemos OmniAuth Identity funcionando correctamente. Nos ha llevado más tiempo crear los formularios personalizados, pero se trata de una solución muy buena si ya estamos usando OmniAuth y tan sólo queremos añadir el registro de nuevas cuentas.