Аутентификация в rails-приложениях с помощью facebook, vkontakte

    Аутентификация в 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
    


    Facebook


    Идём на страницу 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
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 33

      +1
      Картинки не хватает в завершении
        +2
        Я похожие вещи делал на rusrails.ru для авторизации через твиттер, гугл и гитхаб — код можно посмотреть тут github.com/morsbox/rusrails

        В принципе все очень похоже.
          +1
          Ваш пост как нельзя вовремя)
            +1
            Мне понравилось, пиши ещё!
              0
              Хороший топик, спасибо за труд!
              Есть вопрос по gem 'twitter-bootstrap-rails', у вас команда rails g bootstrap:layout application fixed сгенерировала haml код?
                0
                Вроде при подключении haml-rails так происходит, гем twitter-bootstrap-rails поддерживает haml.
                  0
                  О спасип, а я юзал просто haml
                0
                В файл инициализации devise.rb не забываем дописываем строчку:
                config.omniauth :vkontakte, 'APP_ID', 'APP_SECRET'
                  +7
                  Вот так не очень хорошо поступать, стоит сразу привыкать делать правильно, тем более никакой сложности это не вносит. А именно, с oauth секрет кеями вести себя так же, как и с креденшелами к базам данным — выносить в отдельный конфиг, чтобы оно не светилось в репозитории.

                  config/initializers/devise.rb:
                  OAUTH_CREDENTIALS_PATH = "#{ENV['HOME']}/mysite/oauth.yml"
                  OAUTH_CREDENTIALS = YAML.load_file(OAUTH_CREDENTIALS_PATH)[Rails.env]
                  


                  ~/mysite/oauth.yml:

                  development: &defaults
                    :vkontakte:
                      :app_id: 1111111
                      :app_secret: 11111111111111111111
                      :app_permissions: "notify,wall,offline"
                  
                  test:
                    <<: *defaults
                  
                  staging:
                    <<: *defaults
                  
                  production:
                    <<: *defaults
                    :vkontakte:
                      :app_id: 2222222
                      :app_secret: 22222222222222222222
                      :app_permissions: "notify,wall,offline"
                  
                  
                    0
                    спасибо за совет
                      0
                      Тогда уж лучше
                      Rails.root.join("config", "oauth.yml")
                      
                    0
                    / Le HTML5 shim, for IE6-8 support of HTML elements — лол

                    Мсье знает толк.
                      +1
                      Тема не раскрыта до конца, сделать аутентификацию из социальных сетей действительно не сложно и можно. Самое интересное начинается когда задумываться о том что было бы не плохо что бы система логинила меня в один и тот же аккаунт в не зависимости от того из какой сети я зашел, конечно при условии что я предварительно привязал другие аккаунты либо система видит что они относятся к одному и тому же человеку.

                      Так например, Логинов через социальные сети очень актуальная штука для ресурсов на которые пользователь ходит редко, соответственно и регестрироваться ему лениво. Так вот при таком раскладе, если я захожу редко, я уже не вспоминаю через что я логинился в прошлый раз.

                      Если социальная сеть отдает почту то система должна сверяться по ней и логинить меня в соответствующий аккаунт, причем если второй раз я за логинился через другую социальную сеть, то система должна дополнить мой профиль информацией из этой сети.

                      Ну и самое веселое начинается при подключении профилей из социалок. Если я добавлю профиль, аккаунт которого уже существует, система должна их смерджить

                      Это я все к тому что если задуматься о том как надо бы все это дело сделать то задача далеко не тривиальная
                        0
                        Позвольте не согласиться: система Вам ничего не должна (-: Не перекладывайте ответственность — помнить сеть, из какой логинились — на чужие плечи. Единственное, что может стоит сделать, — это кукиз какой-то сохранить да и всё, чтоб следующий раз не проходить авторизацию.
                          0
                          Тоже про это подумал, но то, что вы пишете, сделать нельзя. Нет такого признака, по которому можно однозначно определить, принадлежат ли два профиля в фейсбуке и контакте одному и тому же человеку или нет (даже если контакт почту бы давал, у многих соц. сети на разные почты зарегестрированы).
                            0
                            Просто когда уже вошел в аккаунт, подключаешь дополнительные аккаунты уже из своего профиля. Много где это реализовано.
                          0
                          Для теста вконтакта локально не обязательно использовать localtunnel, можно прописать:
                          Адрес сайта: 127.0.0.1:3000
                          Базовый домен: 127.0.0.1
                            0
                            Адрес сайта: h t t p ://127.0.0.1:3000
                              0
                              Можно, но при этом callback на production не будет происходить. У Facebook в настройках приложения можно указать два домена (сайт, мобильный сайт), один из которых можно указать свой сайт, а второй — локалхост. А у ВК — только один.
                                +1
                                я обычно использую 2 приложения, одно для локального теста, другое для продакшена
                              –2
                              ИМХО если нужна простая аутентификация, то легче использовать Loginza.

                              Остается только забирать данные и при необходимости давать пользователю отредактировать.
                                0
                                Логинза отвратительна. И внешне, и по юзабилити. Как минимум нужно три клика, чтобы войти.
                                +1
                                Советую использовать first_or_create! вместо if/else.
                                  0
                                  Спасибо автору за перевод статьи из официальной вики девайза, правда, конечно, отличия есть, например, вместо скафолда использовал бутсрап, ибо это сейчас модно. Но зачем это все не совсем понятно, в реальной жизни требуется привязка к одному аккаунту, и это более показательно и интересно. Для того чтобы оставить коммент, можно использовать виджеты, там и социальность сразу.
                                  И, кстати, create! тут по мне не уместен, он просто вызовет RecordInvalid, что приведет пользователя к 500 странице, можно и нужно использовать create или find_or_create_by_url или first_or_create, тем более что дальше идет проверка на существование записи.
                                  Есть ещё один вариант хранения переменных авторизации, это просто хранить их в переменных окружения системы, тогда не нужны файла, которые все равно желательно включать в репо.
                                    0
                                    всегда хотел узнать почему omniauth не передает state параметр для защиты
                                      0
                                      Перестало работать демо-приложение.
                                        0
                                        У меня очень нубский вопрос. ФБ сейчас требует сертификат при авторизации. Какой самый правильный способ получать его и подпихивать devise`у?
                                          0
                                          Разобрался )
                                          0
                                          А как реализовать получение почты юзера из социалки и если не отдает то запрашивать, чтоб привязывать к одному аккаунту на сайте?
                                            0
                                            Ответил выше
                                            Не нужно получать почту, нужно авторизоваться во всех провайдерах в одном аккаунте.
                                            0
                                            /home/constantin/testapplication/app/views/users/index.html.haml:13: syntax error, unexpected '<', expecting keyword_end
                                            
                                             <hh user=users>.each do |user|
                                              ^
                                            /home/constantin/testapplication/app/views/users/index.html.haml:13: syntax error, unexpected '.'
                                            
                                             <hh user=users>.each do |user|
                                                             ^
                                            
                                            /home/constantin/testapplication/app/views/users/index.html.haml:23: syntax error, unexpected keyword_ensure, expecting $end
                                            


                                            Уже не знаю что и делать, что это за ошибка?
                                              0
                                              Это особенность верстки на Хабре.
                                              <hh user=users> необходимо заменить на @users
                                                0
                                                спасибо, забыл написать что разобрался)

                                            Only users with full accounts can post comments. Log in, please.