読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

Railsのログイン認証gemのDeviseとOmniAuth-Twitterの連携(Twitterでログインする)

Rails gem

https://camo.githubusercontent.com/b1c21cc10f2f94857dea5135fe55f2e4d451e028/68747470733a2f2f7261772e6769746875622e636f6d2f706c617461666f726d617465632f6465766973652f6d61737465722f6465766973652e706e67


次のように、よく見かけるTwitterでユーザ登録Facebookでユーザ登録のように「RailsでDeviseを使ってTwitterのOAuthを実装する方法」について説明します。
f:id:nipe880324:20140802233738j:plain

実際に試す場合は、上記の2記事を実施済みだと差分を埋め合わせる必要がなくなり簡単です。


動作確認

  • Rails 4.1.4
  • Devise 3.2.4
  • OmniAuth

目次

  1. Twitter Developer画面でAPP_IDとAPP_SECRETを取得
  2. DeviseにOmniAuthのインストールと設定(初期設定とTwitterへの接続)
  3. DeviseにOmniAuthのインストールと設定(コールバック処理の実装)
  4. DeviseにOmniAuthのインストールと設定(プロフィール変更の修正)

Twitter Developer画面で[API key]と[API secret]を取得

1. Twitter Developer画面にアクセスします。(Sign inしてなかったらしてください)

f:id:nipe880324:20140803001147p:plain:w360

2. [Create New App]を押し、各項目を入力して下さい。
  • Name: アプリ名を記載(任意)
  • Description: 説明を記載(任意)
  • Website: ホームページを記載(ユーザーが認証時に表示されるURLで、OmniAuthの動作に関与しないので任意)
  • Callback URL: コールバックURLを記載(今回はテストでローカルサーバーのためhttp://127.0.0.1:3000/users/omniauth_callbacksを指定)

f:id:nipe880324:20140803001703p:plain:w360

3. [Create your Twitter application]を押します。
4. 作成したアプリの[settings]タブの[Allow this application to be used to Sign in with Twitter]のチェックボックスをONにして、[Update settings]をして下さい。

f:id:nipe880324:20140803001843p:plain:w360

5. 作成したアプリの[API Keys]タブで[API key]と[API secret]を後から使うのでコピーしておいて下さい。(下の画像では値を隠しています)

f:id:nipe880324:20140803002424p:plain:w360


DeviseにOmniAuthのインストールと設定(初期設定とTwitterへの接続)

では、RailsにOmniAuthをインストールしていきます。
まずは、deviseomniauth-twitterをGemfileに追加します。

# Gemfile
gem 'devise'
gem 'omniauth-twitter'

次に、gemをインストールします。

$ bundle install

OAuthで取得するprovideruidを保持するために、Userモデルにカラムを追加します。

$ rails g migration add_columns_to_users provider uid
$ rake db:migrate

Deviseの設定ファイルに先ほど取得した[API key]と[API secret]を追加します。

# config/initializers/devise.rb
Devise.setup do |config|
  ...
  ...
  # omniauth
  # 先ほど設定した値を記載して下さい。
  config.omniauth :twitter, "[API Key]", "[API secret]"
end

Userモデルに:omniauthableを設定します。
下記のケースの場合、:validatableの後に,を忘れないようにして下さい。

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable and :timeoutable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :omniauthable, omniauth_providers: [:twitter]

  validates :username, presence: true, uniqueness: true
end

注意点として、現在(devise 3.2.4)では、DeviseはOmniAuthを1つのモデルでしか使えません。
複数のモデルでOmniAuthを使いたい場合はここを参照して下さい。


:omniauthableをUserモデルに追加し、config/routes.rbdevise_for :usersが設定されている場合、下記の2つのルートが自動で追加されます。
$ rake routesで確認しましょう。

$ rake routes
$ rake routes
                  Prefix Verb     URI Pattern                            Controller#Action
  ...
 user_omniauth_authorize GET|POST /users/auth/:provider(.:format)        omniauth_callbacks#passthru {:provider=>/twitter/}
  user_omniauth_callback GET|POST /users/auth/:action/callback(.:format) omniauth_callbacks#(?-mix:twitter)
  ...

注意点として、Deviseは上記の2つのパスの*_urlを作成しないことと、自分で2番目のuser_omniauth_callbackはViewなどで指定しません。

では、application.html.erbにリンクを追加しましょう。

# app/views/layouts/application.html.erb
....
<body>
<header>
  <nav>
    <!-- user_signed_in? はユーザがログインしているか調べるdeviseのHelperメソッド -->
    <% if user_signed_in? %> 
      <!-- current_user は現在ログインしているUserオブジェクトを返すdeviseのHelperメソッド -->
      <!-- *_path はUserモデルを作成したときに、
        deviseにより自動で作成されてますので、rake routesで確認できます -->
      Logged in as <strong><%= current_user.email %></strong>.
      <%= link_to 'プロフィール変更', edit_user_registration_path %> |
      <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
    <% else %>
      <%= link_to "サインイン", new_user_registration_path %> |
      <%= link_to "ログイン", new_user_session_path %> |
      <%= link_to "Sign in with Twitter", user_omniauth_authorize_path(:twitter) %>
    <% end %>
  </nav>
</header>
  
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

<%= yield %>

</body>
</html>


では、サーバーを再起動し、ログイン画面を確認してみましょう。「Sign in with Twitter」リンクが追加されているはずです。
これは、app/views/devise/shared/_links.erb_links_erbにデフォルトで:omniauthableが有効化されたらリンクを表示するように記載されていたためです。


では、[Sign in with Twitter]リンクを押すと、Twitter画面に遷移し、その後、Railsにリダイレクトされます。
f:id:nipe880324:20140803013919p:plain:w360
f:id:nipe880324:20140803005647p:plain:w360

そして、コールバックコントローラーがないため、エラーになります。
f:id:nipe880324:20140803005648p:plain:w360


DeviseにOmniAuthのインストールと設定(コールバック処理の実装)

コールバックコントローラーを作成しましょう。
まず、コールバックコントローラーへのルートを追加します。

# config/routes.rb
  ...
  devise_for :users, path_names: { sign_in: "login", sign_out: "logout"},
    controllers: { omniauth_callbacks: "omniauth_callbacks" }
  ...

次に、rails gコマンドでコールバックコントローラーを作成します。

$ rails g controller omniauth_callbacks


では、コールバックコントローラーを実装しましょう。

# app/controllers/omniauth_callbacks_controller.rb
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def all
    # profiderとuidでuserレコードを検索。存在しなければ、新たに作成する
    user = User.from_omniauth(request.env["omniauth.auth"])
    # userレコードが既に保存されているか
    if user.persisted?
      # ログインに成功
      flash.notice = "ログインしました!!"
      sign_in_and_redirect user
    else
      # ログインに失敗し、サインイン画面に遷移
      session["devise.user_attributes"] = user.attributes
      redirect_to new_user_registration_url
    end
  end

  # alias_methodはクラスやモジュールのメソッドに別名をつけます
  # 実態がallメソッドのtwitterメソッドを定義しています
  # こうすることで、様々なメソッド名で同じ処理を実装することができます。
  # OAuthの処理はほとんど同じためこのようにしています。
  # 例えば、Facebookに対応する場合、alias_method :facebook, :allだけですみます
  alias_method :twitter, :all
end

Userモデルにメソッドを実装します。

# app/models/user.rb
  ...
  def self.from_omniauth(auth)
    # providerとuidでUserレコードを取得する
    # 存在しない場合は、ブロック内のコードを実行して作成する
    where(auth.slice(:provider, :uid)).first_or_create do |user|
      # auth.provider には "twitter"、
      # auth.uidには twitterアカウントに基づいた個別のIDが入っている
      # first_or_createメソッドが自動でproviderとuidを設定してくれるので、
      # ここでは設定は必要ない
      user.username = auth.info.nickname # twitterで利用している名前が入る
      user.email = auth.info.email # twitterの場合入らない
    end
  end
  ...


では、画面から [Sign in with Twitter]を押してみましょう。すると、サインアップ画面が表示されます。
これは、OmniauthCollbacksControllerのUser.from_omniauthメソッドで新規にレコードを作成するので、user.persisted?で「false」になっているためです。
f:id:nipe880324:20140803014918p:plain:w480


では、サインアップ画面にリダイレクトされたときに、Twitterから取得したデータを表示するようにします。

# app/models/user.rb
  ...
  # Devise の RegistrationsController はリソースを生成する前に self.new_sith_session を呼ぶ
  # つまり、self.new_with_sessionを実装することで、サインアップ前のuserオブジェクトを初期化する
  # ときに session からデータをコピーすることができます。
  # OmniauthCallbacksControllerでsessionに値を設定したので、それをuserオブジェクトにコピーします。
  def self.new_with_session(params, session)
    if session["devise.user_attributes"]
      new(session["devise.user_attributes"], without_protection: true) do |user|
        user.attributes = params
        user.valid?
      end
    else
      super
    end
  end
  ...

データがコピーされていることを確認するため、 画面で[Sign in with Twitter]を押してましょう。
EmailとPasswordのエラーが表示されています。
f:id:nipe880324:20140803023726p:plain:w480


Twitter経由で認証した場合、パスワードを入力しなくてもログインをできるようにする
TwitterではEmailは取得できないため入力しないといけません、PasswordはTwitterで認証されているため入力はいりません。そのため、パスワードを入力しなくてもログインできるようにしましょう。
ちなみに、omniauth-twitter 公式ドキュメントに取得できる値が記載されています。

まずは、Userモデルがproviderが存在する場合にパスワードを要求しないようにします。

# app/models/user.rb
  ...
  # providerがある場合(Twitter経由で認証した)は、
  # passwordは要求しないようにする。
  def password_required?
    super && provider.blank?
  end
  ..

また、Viewではパスワードが要求されていない場合は、f.object.password_required?を追加して、passwordの入力フィールドを表示しないようにします。

# app/views/devise/registrations/new.html.erb
<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>

  <div><%= f.label :username %><br />
  <%= f.text_field :username, autofocus: true %></div>

  <div><%= f.label :email %><br />
  <%= f.email_field :email %></div>

  <% if f.object.password_required? %>
    <div><%= f.label :password %><br />
      <%= f.password_field :password, autocomplete: "off" %></div>

    <div><%= f.label :password_confirmation %><br />
      <%= f.password_field :password_confirmation, autocomplete: "off" %></div>
  <% end %>

  <div><%= f.submit "Sign up" %></div>
<% end %>

<%= render "devise/shared/links" %>


では、画面を開いて、 [Sign in with Twitter]リンクを押しましょう。
エラーがEmailの未入力だけになっていますね。Emailを入力して、ユーザ登録しましょう。
f:id:nipe880324:20140803025926p:plain:w320

ユーザ登録できました。
f:id:nipe880324:20140803030035p:plain:w320

ログアウトして、[Sign in with Twitter]リンクをするとログインができます。
f:id:nipe880324:20140803030045p:plain:w320


DeviseにOmniAuthのインストールと設定(プロフィール変更の修正)

ほぼほぼ完成ですが1つ問題があります。
パスワードが存在しないためプロフィール変更ができません。

まず、パスワードがなくても更新できるように、Userモデルにメソッドを追加します。

# app/models/user.rb
  ...
  # プロフィールを変更するときによばれる
  def update_with_password(params, *options)
    # パスワードが空の場合
    if encrypted_password.blank?
      # パスワードがなくても更新できる
      update_attributes(params, *options)
    else
      super
    end
  end
  ...


Viewで現在のパスワードフィールドを表示しないようにします。

# app/views/devise/registrations/edit.html.erb
<h2>Edit <%= resource_name.to_s.humanize %></h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
  <%= devise_error_messages! %>

  <div><%= f.label :username %><br />
  <%= f.email_field :username, autofocus: true %></div>

  <div><%= f.label :email %><br />
  <%= f.email_field :email %></div>

  <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
    <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
  <% end %>

  <div><%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
    <%= f.password_field :password, autocomplete: "off" %></div>

  <div><%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "off" %></div>

  <% if f.object.encrypted_password.present? %>
    <div><%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
      <%= f.password_field :current_password, autocomplete: "off" %></div>
  <% end %>

  <div><%= f.submit "Update" %></div>
<% end %>

<h3>Cancel my account</h3>

<p>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %></p>

<%= link_to "Back", :back %>

では、画面でプロフィール更新画面で現在のパスワードフィールドが表示されません。
f:id:nipe880324:20140803031905p:plain:w320


そして、プロフィールが更新できました。
f:id:nipe880324:20140803031907p:plain:w320


とっても長くなりましたが以上です。