Railsはdeviseとomniauthで簡単認証

# deviseとomniauthで簡単認証

* `対象者`:Railsでアカウント認証を実装したい人
* `成果`:認証機能の実装

## 最初に

私自身Rails初学者なんで間違っている箇所があるかと思います。
コメントなどで知らせてもらえると幸いです。

とりあえず[Ruby on Rails Tutorial](http://railstutorial.jp)の第３章から第５章までを終わらせている体で話します。
ファイルはアプリケーションルートからで記載しています。
bundle execは追い出しています。コマンド使えない場合は頭にbundle execを付けてください。

## 何はともあれインストール

```ruby:gemfile
gem 'devise'
gem 'omniauth'
gem 'omniauth-twitter'
gem 'omniauth-facebook'
```

```bash
$ bundle install
$ rails generate devise:install

# こっからはメッセージ(勝手に出ます)
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================
 
Some setup you must do manually if you haven't yet:
 
  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:
 
       config.action_mailer.default_url_options = { :host => 'localhost:3000' }
 
     In production, :host should be set to the actual host of your application.
 
  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:
 
       root :to => "home#index"
 
  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:
 
       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>
 
  4. If you are deploying on Heroku with Rails 3.2 only, you may want to set:
 
       config.assets.initialize_on_precompile = false
 
     On config/application.rb forcing your application to not access the DB
     or load models when precompiling your assets.
 
  5. You can copy Devise views (for customization) to your app by running:
 
       rails g devise:views
 
===============================================================================
# ここまでがメッセージ
 
$ rails generate devise User        # rails generate devise MODEL
$ rails generate migration AddOmniauthColumnsToUsers uid provider name
```

generate長いので以降のコマンドではgを使います。
今回はこの超重要な勝手に表示されるマニュアルを載せましたが、コレ以降コマンドの反応は載せません。だるいですし
1,2,5を実施します(順番は気にしない)。
3はbootstrap使っているなら要らんとみた。
4はRails 3.2を使ってHerokuでデプロイする人向け？

gemfileに記述してインストールするだけの簡単なお仕事です。
今回はUserモデルを利用していくのでUserでやってきます。
課金者はPremiumUserとかにしたりするんすかね？安易ですか？はぁ
omniauth用にカラムを足しときます。

```ruby:db/migrate/日時_add_omniauth_columns_to_users.rb
class AddOmniauthColumnsToUsers < ActiveRecord::Migration
  def change
    add_column :users, :uid,      :string, null: false, default: ""
    add_column :users, :provider, :string, null: false, default: ""
    add_column :users, :name,     :string
 
    add_index :users, [:uid, :provider], unique: true
  end
end
```

でもってインデックスを足しときます。
Vim で rails.vim プラギンを入れている人なら`:Rmigration add_omniauth_columns_to_users`ってやっときゃファイルは開ける。
まぁ大体、:rmigr あたりまで打って TAB連打するのは俺だけじゃないはず。
FacebookとTwitterでuidがかぶっちゃうかもしれんので、ユーザIDとプロバイダでユニーク取ってます。
Facebookのuidがクソ長いらしく、integerだとbigint使わなきゃいけんとかでverchar使っときゃ余裕でしょ？って感じでstring指定。

```bash
$ rake db:migrate
```

これでDBの環境は整った。既にUserテーブルがある人はDropなりしてみて。
下はmigrate通らずに`現行のDB環境ぶっ壊していい人向け`(コメント外して使ってね) migrate通るならまぁ大丈夫だ。

```bash
$ # rake db:migrate:reset
```

## View やら Route やらのお話し

```bash
$ rails g devise:views users
$ rails g controller users/sessions
$ rails g controller users/passwords
$ rails g controller users/registrations
$ rails g controller users/omniauth_callbacks
```

Viewは自動生成する。モデルがUserなんだからusersで作る。
controllerもデフォルトだとdeviseが使われちゃうのでusersを作る。

```erb
--app/views/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="navbar-inner">
    <div class="container">
      <%= link_to "sample app", '#', id: "logo" %>
      <nav>
        <ul class="nav pull-right">
          <li><%= link_to "Home",    root_path %></li>
          <li><%= link_to "Help",    help_path %></li>
          <% if user_signed_in? %>
            <li><%= link_to "Edit profile", edit_user_registration_path, :class => 'navbar-link' %></li>
            <li><%= link_to "Logout", destroy_user_session_path, :method => :delete, :class => 'navbar-link' %></li>
          <% else %>
            <li><%= link_to "Sign up", new_user_registration_path, :class => 'navbar-link' %></li>
            <li><%= link_to "Login", new_user_session_path, :class => 'navbar-link' %></li>
            <li><%= link_to "Facebook", user_omniauth_authorize_path(:facebook) %></li>
            <li><%= link_to "Twitter", user_omniauth_authorize_path(:twitter) %></li>
          <% end %>
        </ul>
      </nav>
    </div>
  </div>
</header>
```

まぁ別にどのViewに入れようと構わないのだけれども、ナビゲーションに混ぜ込んだ。
先述の通り[Ruby on Rails Tutorial](http://railstutorial.jp)の５章まで終わっている前提だからね。
ここで重要なのはLogoutのとこにdeleteメソッドを指定していること。
Deviseのどっかのバージョンからこうなったとのことで、deleteを指定しないと動かない。
omniauthの方はこの時点ではふぁっ！？って感じだろうけど気にせず進めちゃいましょう。

```ruby:config/routes.rb
Sample::Application.routes.draw do
    ....
  root 'static_pages#home'
    ....
  devise_for :users, :controllers => {
    :sessions       => "users/sessions",
    :registrations  => "users/registrations",
    :passwords      => "users/passwords",
    :omniauth_callbacks => "users/omniauth_callbacks"
  }
end
```

不要部分は省略省略〜。
rootの指定はマニュアルの2に当たる部分なんでやっときましょう。
devise_forは勝手に足されていると思うけど、controllerの指定ででフォルトのdeviseからusersに変えてやっています。
ここら辺のコードは全部複数系です。user/registration とかやったら動きませんので...はい、私やっちゃいました。

```ruby:app/controller/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  def new
    super
  end

  def create
    super
  end

  def destroy
    super
  end
end
```

```ruby:app/controller/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  def cancel
    super
  end

  def create
    super
  end

  def new
    super
  end

  def edit
    super
  end

  def update
    super
  end

  def destroy
    super
  end

  def build_resource(hash=nil)
    hash[:uid] = User.create_unique_string
    super
  end
end
```

```ruby:app/controller/passwords_controller.rb
class Users::PasswordsController < Devise::PasswordsController
  def new
    super
  end

  def create
    super
  end

  def edit
    super
  end

  def update
    super
  end
end
```

無駄にファイルありますがやっていることは基本的に同じです。
Deviseのコントローラを継承して、親の動作をそのまま実行しています。
RegistrationsControllerのとこだけ他と違う記述ありますが、uidにユニークな値を入れるための記述です。(後で実装されます)
not null指定にしているので、omniauthを使わずに素で登録するときにuidが無くてエラーとなるのを回避しています。(多分)

```rb:config/environments/development.rb
Sample::Application.configure do
    ....
  config.action_mailer.default_url_options = { :host => 'localhost:3000' }
end
```

既出ですが`....`は省略を表しているということで。
マニュアルの1を回収しておきます。
何の設定なのか良く分かっていませんが、本番環境のときはちゃんとした値を入れてねって書いてありますね。

## Omniauthに必要なキーなどの取得

FacebookとTwitterにアプリ登録をかけます。

[Facebook developers](https://developers.facebook.com/apps)
でアプリを登録する。
Apps -> Create New App -> display nameにサービスの名前など ->
作成後にSettings -> +Add Platform -> App on Facebook を選択 ->
Canvas URL に http:127.0.0.1:3000/ など自分のサイトのrootとなるアドレスを入れておく -> Save Changes

[Twitter Developpers](https://dev.twitter.com)
Sign in -> アイコンクリック -> my applications -> Create New App ->
Name にサービス名など、Description に説明、Website, Callback URL に自分のサイトのrootアドレス ->
Yes, I agree にチェックして -> Create your Twitter application
Settings で Allow this application to be used to Sign in with Twitter にチェックすると１度OAuth認証したら、
次回以降に確認画面が表示されないらしいです。毎回出てくるWebサービスはこれやってないってこと？

## 残りのもの

```rb:config/initializers/devise.rb
Devise.setup do |config|
    ....
  # API key
  if Rails.env.production?
    config.omniauth :facebook,  "App ID", "App Secret"
    config.omniauth :twitter,   "Consumer key", "Consumer secret"
  else
    config.omniauth :facebook,  "App ID", "App Secret"
    config.omniauth :twitter,   "Consumer key", "Consumer secret"
  end
end
```

登録が終わったら早速使っていきます。
アプリ管理画面のどこか、違うタブだったりに同じ文言があるのでコピペしてApp ID,App Secret,Consumer ker Consumer secretを埋めましょう。
多分まず開発環境で使うと思うのでelseの方にコピペしてください。
つまり本番稼働の際にもう一度アプリ登録が必要ということですな。

```ruby:app/models/user.rb
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable,  :omniauthable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  def self.find_for_facebook_oauth(auth, signed_in_resource=nil)
    user = User.where(:provider => auth.provider, :uid => auth.uid).first
    unless user
      user = User.create(name:     auth.extra.raw_info.name,
                         provider: auth.provider,
                         uid:      auth.uid,
                         email:    auth.info.email,
                         password: Devise.friendly_token[0,20]
                        )
    end
    user
  end
 
  def self.find_for_twitter_oauth(auth, signed_in_resource=nil)
    user = User.where(:provider => auth.provider, :uid => auth.uid).first
    unless user
      user = User.create(name:     auth.info.nickname,
                         provider: auth.provider,
                         uid:      auth.uid,
                         email:    User.create_unique_email,
                         password: Devise.friendly_token[0,20]
                        )
    end
    user
  end

  def self.create_unique_string
    SecureRandom.uuid
  end

  def self.create_unique_email
    User.create_unique_string + "@example.com"
  end
end
```

Userモデルを埋めていきましょう。
コメントのOthers available areに従ってdeviseで利用する機能にomniauthを追加しています。
omniauthから呼ばれるクラスメソッドを定義します。
RegistrationsControllerから呼ばれるuid生成メソッドの追加をします。
TwitterではOAuthでemailを入手できないため、適当にセットしておきます。
強制的にexample.comドメインにしているのはユーザが正しいEmailに変更した際の考慮らしいです。

```rb:app/controller/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    # You need to implement the method below in your model (e.g. app/models/user.rb)
    @user = User.find_for_facebook_oauth(request.env["omniauth.auth"], current_user)
 
    if @user.persisted?
      set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
      sign_in_and_redirect @user, :event => :authentication
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end
 
  def twitter
    # You need to implement the method below in your model
    @user = User.find_for_twitter_oauth(request.env["omniauth.auth"], current_user)
 
    if @user.persisted?
      set_flash_message(:notice, :success, :kind => "Twitter") if is_navigational_format?
      sign_in_and_redirect @user, :event => :authentication
    else
      session["devise.twitter_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_registration_url
    end
  end
end
```

やっていることはほとんど理解できていません。
多分OAuth後にユーザがいればログインするし、まだいなければ登録するよみたいな感じなのかな？
それぞれ違うとこにリダイレクトかけている模様。

## 最後に
え？テストは？
ないですよそんなもん。
だって処理の流れ理解出来てませんですし。
このあとすごく苦労してテスト書くと思います。
ちなみにテストは出来たとしても投稿しないと思います。

引用 -- というよりもはやパクり元
[Rails4 にて Devise でユーザー登録・ログイン認証・認可の機能を追加](http://easyramble.com/devise-on-rails.html)
[Rails4 で Devise と OmniAuth で、Twitter/Facebook のOAuth認証と通常フォームでの認証を併用して実装](http://easyramble.com/implement-devise-and-ominiauth-on-rails.html)