Аутентификация в rails-приложениях через facebook, vkontakte

В данной статье будет рассказано, как сделать простейшую аутентификацию в rails-приложении через социальные сети vkontakte и facebook, помогают в этом гемы omniauth, omniauth-facebook, omniauth-vkontakte. Материал рассчитан на новичка. Хоть это и будет учебное приложение, но для придания законченности используем bootstrap с помощью гема twitter-bootstrap-rails.
Каркас
Создаём новое приложение (bundle exec перед командами буду опускать):
rails new authproviders
Пишем необходимые гемы в Gemfile
source 'https://rubygems.org' gem 'rails', '3.2.3' gem 'sqlite3', :group => :development gem 'pg', :group => :production group :assets do gem 'sass-rails', '~> 3.2.3' gem 'coffee-rails', '~> 3.2.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'haml-rails' gem 'twitter-bootstrap-rails' gem 'devise' gem 'omniauth' gem 'omniauth-facebook' gem 'omniauth-vkontakte'
Выполняем установку:
bundle install --without production
Сделаем приложение чуть посимпатичнее, применим bootstrap:
rails g bootstrap:layout application fixed
Не забываем удалить index.html из директории public,
если остался файл application.html.erb, то тоже удаляем.
Создаём модель пользователя, где url — адрес страницы с профилем:
rails g scaffold User username:string nickname:string provider:string url:string
Настраиваем devise:
rails generate devise:install rails generate devise User rake db:migrate
Добавляем в модель User модуль omniauthable:
class User < ActiveRecord::Base # Include default devise modules. Others available are: # :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :omniauthable # Setup accessible (or protected) attributes for your model attr_accessible :email, :password, :password_confirmation, :remember_me attr_accessible :nickname, :provider, :url, :username end
Создаём контроллер обратных вызовов с экшенами для каждого провайдера
rails g controller Users::OmniauthCallbacks facebook vkontakte
Наследуем его от Devise::OmniauthCallbacksController
class Users::OmniauthCallbacksControllerController < Devise::OmniauthCallbacksController def facebook end def vkontakte end end
В routes.rb прописываем маршрутизацию:
Authproviders::Application.routes.draw do devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" } resources :users, :only => [:index, :destroy] root :to => 'users#index' end
Добавляем в шаблон application.html.haml ссылки на соц сети(user_omniauth_authorize_path(:facebook) и user_omniauth_authorize_path(:vkontakte)), полный код шаблона:
!!! 5 %html(lang="en") %head %meta(charset="utf-8") %meta(name="viewport" content="width=device-width, initial-scale=1.0") %title= content_for?(:title) ? yield(:title) : "Authproviders" = csrf_meta_tags / Le HTML5 shim, for IE6-8 support of HTML elements /[if lt IE 9] = javascript_include_tag "http://html5shim.googlecode.com/svn/trunk/html5.js" = stylesheet_link_tag "application", :media => "all" %link(href="images/favicon.ico" rel="shortcut icon") %link(href="images/apple-touch-icon.png" rel="apple-touch-icon") %link(href="images/apple-touch-icon-72x72.png" rel="apple-touch-icon" sizes="72x72") %link(href="images/apple-touch-icon-114x114.png" rel="apple-touch-icon" sizes="114x114") %body .navbar.navbar-fixed-top .navbar-inner .container %a.btn.btn-navbar(data-target=".nav-collapse" data-toggle="collapse") %span.icon-bar %span.icon-bar %span.icon-bar %a.brand(href="#") Authproviders .container.nav-collapse %ul.nav - if user_signed_in? %li= link_to "#{current_user.username} (#{current_user.provider})", current_user.url %li= link_to "Sign out", destroy_user_session_path, :method => :delete .container .content .row .span9 = yield .span3 .well.sidebar-nav %h3 Providers %ul.nav.nav-list - if !user_signed_in? %li= link_to "Sign in with Facebook", user_omniauth_authorize_path(:facebook) %li= link_to "Sign in with Vkontakte", user_omniauth_authorize_path(:vkontakte) %footer %p © Company 2012 = javascript_include_tag "application"
Поправим шаблон users/index.html.haml для вывода зарегистрированных пользователей
- model_class = User.new.class %h1=t '.title', :default => model_class.model_name.human.pluralize %table.table.table-striped %thead %tr %th= model_class.human_attribute_name(:username) %th= model_class.human_attribute_name(:nickname) %th= model_class.human_attribute_name(:provider) %th= model_class.human_attribute_name(:sign_in_count) %th= model_class.human_attribute_name(:created_at) %th=t '.actions', :default => t("helpers.actions") %tbody - @users.each do |user| %tr %td= link_to user.username, user.url %td= user.nickname %td= user.provider %td= user.sign_in_count %td= user.created_at %td = link_to t('.destroy', :default => t("helpers.links.destroy")), user_path(user), :method => :delete, :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')), :class => 'btn btn-mini btn-danger'
Убедимся что всё работает и приступаем к самому интересному:
rails s
Идём на страницу https://developers.facebook.com/apps и создаём новое приложение

(для отладки на локалхосте в site url можно написать localhost:3000)
В файл инициализации devise.rb дописываем строчку:
config.omniauth :facebook, 'APP_ID', 'APP_SECRET'
'APP_ID', 'APP_SECRET' меняем на значения, выданные при создании нового приложения.
В модель User добавим метод facebook, который будет искать пользователя по адресу его странички, если такого нет, то создавать нового (не самое лучшее решение с точки зрения безопасности, но это лишь пример):
def self.find_for_facebook_oauth access_token if user = User.where(:url => access_token.info.urls.Facebook).first user else User.create!(:provider => access_token.provider, :url => access_token.info.urls.Facebook, :username => access_token.extra.raw_info.name, :nickname => access_token.extra.raw_info.username, :email => access_token.extra.raw_info.email, :password => Devise.friendly_token[0,20]) end end
В котроллер Users::OmniauthCallbacksController добавляем так же метод facebook:
def facebook @user = User.find_for_facebook_oauth request.env["omniauth.auth"] if @user.persisted? flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Facebook" sign_in_and_redirect @user, :event => :authentication else flash[:notice] = "authentication error" redirect_to root_path end
Vkontakte
Для контакта процедура аналогична: http://vk.com/developers.php
создаём приложение:

В файл инициализации devise.rb не забываем дописываем строчку:
config.omniauth :vkontakte, 'APP_ID', 'APP_SECRET'
Протестировать работу с контактом на локалхосте оказалось сложнее, можно использовать такую штуку: https://github.com/progrium/localtunnel
После установки через bundle, при первом запуске подгружаем ключ:
localtunnel -k ~/.ssh/id_rsa.pub 3000
Затем запускаем тунель, он выдаст нам адрес, по которому будет доступно приложение (его и забиваем в контакт), следом рельсы
localtunnel 3000 This localtunnel service is brought to you by Twilio. Port 3000 is now publicly accessible from http://4v9p.localtunnel.com ... rails s
Добавляем по методу vkontakte в модель и контроллер, по аналогии с facebook (замечу, что согласно политике ИБ контакта, адреса почты не отдаются, поэтому чтобы не конфликтовать с валидацией, я при создании пользователя создаю суррогатные адреса вида: домен + @vk.com):
class User < ActiveRecord::Base devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :omniauthable attr_accessible :email, :password, :password_confirmation, :remember_me attr_accessible :nickname, :provider, :url, :username def self.find_for_facebook_oauth access_token if user = User.where(:url => access_token.info.urls.Facebook).first user else User.create!(:provider => access_token.provider, :url => access_token.info.urls.Facebook, :username => access_token.extra.raw_info.name, :nickname => access_token.extra.raw_info.username, :email => access_token.extra.raw_info.email, :password => Devise.friendly_token[0,20]) end end def self.find_for_vkontakte_oauth access_token if user = User.where(:url => access_token.info.urls.Vkontakte).first user else User.create!(:provider => access_token.provider, :url => access_token.info.urls.Vkontakte, :username => access_token.info.name, :nickname => access_token.extra.raw_info.domain, :email => access_token.extra.raw_info.domain+'@vk.com', :password => Devise.friendly_token[0,20]) end end end
Код контроллера:
class Users::OmniauthCallbacksController < ApplicationController def facebook @user = User.find_for_facebook_oauth request.env["omniauth.auth"] if @user.persisted? flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Facebook" sign_in_and_redirect @user, :event => :authentication else flash[:notice] = "authentication error" redirect_to root_path end end def vkontakte @user = User.find_for_vkontakte_oauth request.env["omniauth.auth"] if @user.persisted? flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Vkontakte" sign_in_and_redirect @user, :event => :authentication else flash[:notice] = "authentication error" redirect_to root_path end end end
Конечно, используя DRY, можно и нужно обобщить этот код.
Само приложение:

Заключение
Как мы убедились создать приложение на рельсах с аутентификацией через фейсбук и контакт — очень просто.
Работающее демо лежит тут: http://authproviders.herokuapp.com/
Код примера: https://github.com/mystdeim/Authproviders
Приведу некоторые полезные ссылки:
- http://railsapps.github.com/rails-examples-tutorials.html — демонстрационные приложения, использующие в основном devise и omniauth
- http://www.communityguides.eu/articles/11 -хорошее решение, для объединения нескольких аккаунтов одного человека использующего разные провайдеры (требуется, чтобы была одинаковая почта, к сожалению, с контактом трюк не пройдет)
P.S. Это моя первая статья, проба пера, можно сказать.
UP: секрет кеи и и ид приложений лучше держать отдельно, чтобы они случайно не попали в репозиторий http://habrahabr.ru/post/142128/#comment_4757653
