yamotonalds's blog

Webアプリケーション開発における技術メモが中心です。たまにWebサービス、興味を持ったデバイス、自作PCに関する話題もあるかも。Amazon好きなのでAmazon.co.jpアソシエイト使ってます。

OmniauthでTwitter認証(OAuth認証)

Twitterによる外部認証をやってみることにした。

Deviseを使っているのでOmniauthで楽にできそう。

以下のページを参考にした。

 

OmniAuth: Overview · plataformatec/devise Wiki · GitHub

ASCIIcasts - “Episode 235 - OmniAuth Part 1”

ASCIIcasts - “Episode 236 - OmniAuth Part 2”

Ruby - deviseでfacebook,twitter認証 - Qiita [キータ]

 

内容の古い部分があったりこちらの要件に合わない部分があったりしたので少し変更しながら実装した。

前提

  • rails 3.2.13
  • devise 2.2.4
  • omniauth 1.1.4
  • omniauth-twitter 1.0.0

既にDeviseでユーザー認証をしているところに外部認証を追加してゆく。

Twitter Developersにアプリケーションを登録する

まず Twitter Developers にログインし、右上のアイコンメニューから My  applications を選択する。

Create a new application を選択し、各項目を入力する。後から変更できるのでサクッと登録してしまって良い。Callback URL にはコールバックされないので本番のURLにしていても良い。

アプリケーションが登録できたら Details タブの OAuth settings に表示されている Consumer keyConsumer secret をメモする。

また、 Settings タブの Application Type にある Allow this application to be used to Sign in with Twitter にチェックを入れる。これにチェックを入れていないと /oauth/authenticate の代わりに /oauth/authorize に飛ばされるため、ログインの度に毎回アプリケーションにアクセスを許可するかどうか聞かれることになる。

 omniauth-twitterをインストールする

Twitterアプリケーションの登録と設定変更が完了したら後はソースコードを書くだけ。

まずはomniauth-twitterをインストールする。

# Gemfile
# ...
gem 'omniauth-twitter'
# ...

omniauthは依存関係で自動的にインストールされるので記述するのはomniauth-twitterだけでOK。Facebook等、他の認証プロバイダを使用する場合は対応するomniauth-○○を記述する。

インストールコマンドを実行してインストールは完了。

>bundle install

 Omniauthの設定

次にTwitterにアクセスするため config/initializers/devise.rb に以下の記述を追加。

config.omniauth :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']

以降、サーバー・テスト実行時には環境変数TWITTER_KEY, TWITTER_SECRETにTwitterアプリケーション作成時にメモしたConsumer key,Consumer secretをセットしておくこと。

Userクラスに以下の記述を追加。

devise :omniauthable, :omniauth_providers => [:twitter]

 Controllerの作成とRouting設定

 次にControllerを作成する。Deviseには Devise::OmniauthCallbacksController というクラスが既にあるが、外部認証完了時にいろいろ処理を行うためサブクラスを作成した。

# app/controllers/mydevise/omniauth_callbacks_controller.rb
class Mydevise::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def twitter
    render text: request.env["omniauth.auth"]
  end
end

 ルーティング設定も変更する。

# config/routes.rb
devise_for :users, controllers: { omniauth_callbacks: "mydevise/omniauth_callbacks" }

 Twitter認証のためのリンクは

<%= link_to "Sign in with Twitter", user_omniauth_authorize_path(:twitter) %>

 で作れるけど app/views/devise/shared/_links.erb を使っていればSign in/upの画面に自動的にリンクが表示される。

ここまでできれば実際にTwitter認証をしてみると画面に認証によって得られたデータが表示されるはず。

認証データを保持するモデルを作成する

次に認証データの保存先となるモデルを作成する。Userモデルにカラムを追加してる例もあるけれど、一人のユーザーが複数の認証プロバイダを使用できるようにするためと小さく分割できるところは分割した方が管理が楽だと思ったのでRailscasts(ASCIIcasts)と同様に認証モデルを作ることにした。

>rails g model authentication provider:string uid:string user_id:integer
# app/models/authentication.rb
class Authentication < ActiveRecord::Base
  belongs_to :user
  attr_accessible :provider, :uid, :user

  validates :provider, presence: true, uniqueness: { scope: :uid }, length: { maximum: 255 }
  validates :uid, presence: true, length: { maximum: 255 }
  validates :user, presence: true
end
# app/models/user.rb
class User < ActiveRecord::Base
  has_many :authentications, dependent: :destroy, autosave: true
  # ...
end

仕上げ

後はOmniauthCallbacksController側で認証モデルを保存したり保存された認証モデルからユーザーモデルをロードしてログインさせたりするだけ。

# app/controllers/mydevise/omniauth_callbacks_controller.rb
class Mydevise::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def twitter
    omniauth = request.env["omniauth.auth"]
    auth = Authentication.find_by_provider_and_uid(omniauth["provider"], omniauth["uid"])

    if auth
      sign_in_and_redirect auth.user, :event => :authentication
      set_flash_message(:notice, :success, :kind => "Twitter") if is_navigational_format?
    elsif current_user
      current_user.authentications.create!(provider: omniauth["provider"], uid: omniauth["uid"])
      redirect_to root_url
      set_flash_message(:notice, :success, :kind => "Twitter") if is_navigational_format?
    else
      session[:omniauth] = omniauth.except('extra')
      redirect_to new_user_registration_url
      set_flash_message(:notice, :success, :kind => "Twitter") if is_navigational_format?
      flash[:notice] += "続けて以下の項目を入力してください。"
    end
  end
end

Railscasts(ASCIIcasts)にもあるけれど注意するのは新規ユーザー を作成する場合。Twitter認証ではユーザーのメールアドレスが取得できないようなのでユーザーを作成する代わりにセッションに認証データを保持しておいてユーザーの新規登録画面にリダイレクトしている。

新規登録画面側ではセッションから認証データを取得してモデルを構築する。

# config/routes.rb
devise_for :users, controllers: { 
  omniauth_callbacks: "mydevise/omniauth_callbacks",
  registrations: "mydevise/registrations"
}
# app/controllers/mydevise/registrations_controller.rb
class Mydevise::RegistrationsController < Devise::RegistrationsController
  def create
    super
    session[:omniauth] = nil unless @user.new_record?
  end

  private
  def build_resource(*args)
    super
    if session[:omniauth]
      @user.apply_omniauth(session[:omniauth])
    end
  end
end

 apply_omniauth、view、password_required?に関してはRailscasts(ASCIIcasts)通りなので割愛。build_resourceでのバリデーションは不要なので行わないようにした。

 

これでTwitter認証によるログイン、既存ユーザーのTwitter認証追加、Twitter認証からのユーザー登録ができるようになった。