DeviseとOmniauthで認証管理

[Ruby on Rails Advent Calendar](http://qiita.com/advent-calendar/2013/ruby-on-rails) 21日目の記事です。

DeviseとOmniauthを使って認証管理をしてみます。

## Deviseとは何か
> Devise is a flexible authentication solution for Rails based on Warden. It:
> - Is Rack based;
> - Is a complete MVC solution based on Rails engines;
> - Allows you to have multiple models signed in at the same time;
> - Is based on a modularity concept: use just what you really need.

と[Github](https://github.com/plataformatec/devise)にあるように、    
WardenをベースとしたRails向け`フレキシブル` `オーセンティケーション` `ソリューション`です。

- Rackベース
	- とはいってもRails専用。
- Rails上で完全なMVCソリューションベース
	- 完全なMVCらしいです。
- 同時に複数のモデルでのサインインが可能
	- 例えばUserモデルとAdminモデルを作った時、同時に2つのロールでログインできます。
- モジュール方式のコンセプトであり、必要な物だけを使うことができる
	- 10個のモジュールを`devise`というDSLで付けたり外したりして、`Devise`が提供する機能の必要なものだけ使うことができます。

## ここでは何をやるか
Deviseを使って認証管理をします。
認証にはfacebookを使用します。

### 要件
- ランディングページのようなページがある。
- ログインしなければ見れないページがある。
- facebookのOAuthを利用してログインする。

## やってみる
### 1. ランディングページとなるページの作成
`welcome`コントローラーをジェネレーターで作ります。

```bash
$ rails g controller Welcome index
```

### 2. / をwelcome#indexを設定
`config/routes.rb`の`get "welcome/index`を以下のように変更します。

```ruby:config/routes.rb
  root "welcome#index"
```

### 3. ログイン後のページ /home を作成
ログイン後のページとなる`home`コントローラーを生成します。

```bash
$ rails g controller Home index
```

### 4. Devise, Omniauthを使うための記述
`Gemfile`に下記を追記してgemをインストールします。

```ruby:Gemfile
gem 'devise'
gem 'omniauth'
gem 'omniauth-facebook'
```
```bash
$ bundle install
```

### 5. Deviseを使うための準備
作っているアプリケーションがdeviseを使えるようにします。

```bash
$ rails g 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

===============================================================================
```

いろいろ表示されているので、実行していきます。

### 6. ActionMailerのデフォルトURLオプションを変更
production環境では別途`config/environments/production.rb`に設定が必要です。

```ruby:config/environments/development.rb
  config.action_mailer.default_url_options = { :host => 'localhost:3000' }
```

### 7. Userモデルを生成
deviseのジェネレーターを使ってUserモデルを作ります。
自動生成する他に以下のカラムを追加します。

- 複数のサービスログインを可能にするための `provider`
- facebookなどでのユーザーidである `uid`
- ログイン中に表示させるための名前 `name`
- サービスへのアクセストークン `token`

```bash
$ rails g devise User provider:string uid:string name:string token:string
```

### 8. FacebookのApp IDとApp Secretをセット
https://developers.facebook.com/apps/ へアクセスして`App ID`と`App Secret`を取得します。

```ruby:config/initializers/devise.rb
require "omniauth-facebook"

Devise.setup do |config|

 …

  config.omniauth( :facebook,
                   ENV['FACEBOOK_APP_ID'],
                   ENV['FACEBOOK_APP_SECRET'],
                   {:scope => 'email'} )
end
```

環境変数に`App ID`と`App Secret`を入れてサーバを再起動します。
（[dotenv](https://github.com/bkeepers/dotenv)を使うと便利！）

```bash
$ export FACEBOOK_APP_ID="123456789012345"
$ export FACEBOOK_APP_SECRET="1234567890abcdefghijklmnopqrstuvwxyz"
$ rails s
```

### 9. UserモデルをOmniauthで認証可能にする
`:omniauthable`をUserモデルに追加します。

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

### 10. Facebookログインリンクを設置
`user_omniauth_authorize_path`というヘルパーメソッド使ってログインリンクを生成します。

```html:app/views/welcome/index.html.erb
<%= link_to "Sign in with Facebook", user_omniauth_authorize_path(:facebook) %>
```

### 11. facebook認証後のコールバックメソッドを定義する
`app/controllers/users`ディレクトリを作成し、`omniauth_callbacks_controller.rb`を作ります。
`request.env['omniauth.auth']`というリクエストパラメータにコールバックされた値が入っています。
値を確認するために例外を発生させてその変数の中身を表示してみます。

```ruby:app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    raise request.env['omniauth.auth'].to_yaml
  end
end
```


### 12. facebookからのコールバックをログイン処理
まず、Userモデルに`find_for_facebook_oauth`というクラスメソッドを追加します。
このメソッドはコールバックされた値の`provider`と`uid`からDBを検索します。
存在しなかったら新規ユーザーを生成して返却します。

```ruby:app/models/user.rb
def self.find_for_facebook_oauth(auth)
    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,
                          token:    auth.credentials.token,
                          password: Devise.friendly_token[0,20] )
    end

    return user
end
```
 コールバックメソッドでは先ほど生成したメソッドにパラメータを渡してユーザーインスタンスを生成し、
ログイン処理をしてリダイレクトします。

```ruby:app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

  def facebook
    @user = User.find_for_facebook_oauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, :event => :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end
end
```

### 13. ログイン後のリダイレクト先を変更
`user_root`という別名をつけるとそこへリダイレクトしてくれます。
同時に`/home/index`から`/home`にルーティングを変更します。

```ruby:config/routes.rb
  get "home", to: "home#index", as: "user_root"
```


### 14. ログイン・ログアウトのリンクを整理
下のコードではログイン中（`user_signed_in?`で確認できます）ならば「ユーザー名・ログアウトリンク」を、 そうでなければ「ログインリンク」を表示します。

```html:app/views/layouts/application.html.erb
<div id="user-info">
  <% if user_signed_in? %>
    <%= current_user.name %>
    [<%= link_to 'Sign out', destroy_user_session_path, method: :delete  %>]
  <% else %>
    <%= link_to "Sign in with Facebook", user_omniauth_authorize_path(:facebook) %>
  <% end %>
</div>
```


### 15. homeコントローラーへのアクセスをログイン中のみに制限
`before_filter`に`authenticate_user!`を追加することによってログインしているかどうかを判別し、アクセス制御をします。

```ruby:app/controllers/home_controller.rb
class HomeController < ApplicationController
  before_filter :authenticate_user!

  def index
  end
end
```

### 16. 15のリダイレクト先をfacebook認証のページに変更
`authenticate_user!`をオーバーライドして、facebook認証URLに書き換えます。

```ruby:app/controllers/application_controller.rb
  def authenticate_user!
    session[:user_return_to] = env['PATH_INFO']
    redirect_to user_omniauth_authorize_path(:facebook) unless user_signed_in?
  end
```

## さいごに
DeviseとOmniauthを使えばログイン管理がとても簡単に実装することができます。（ちょっと長いですが）

今回はfacebookを対象としましたが、サービスへのApp登録、コールバックメソッド定義をすればTwitterでもGoogleでもログイン認証の選択肢を増やすことができます。
Porvider一覧はOmniauthのWikiで確認できます。[List of Strategies · intridea/omniauth Wiki](https://github.com/intridea/omniauth/wiki/List-of-Strategies)
omniauth用のscaffoldを作るGemを作っている方も[shu0115/minimum-omniauth-scaffold](https://github.com/shu0115/minimum-omniauth-scaffold)

今回はアクセストークンを保存しているので、Facebookだったら[koala](https://github.com/arsduo/koala)や[fb_graph](https://github.com/nov/fb_graph)を使ってAPIを叩くことも可能です。
使う際は`scope`の指定をお忘れなく。

Githubに一連の流れを記しておきました。[Commits · ytkt/TryDeviseAndOmniauth](https://github.com/ytkt/TryDeviseAndOmniauth/commits/master)
（途中で気づいたんですが、ブランチを切らずに進めてしまったためとても見にくいです）

間違っているところ、マズイ所があれば教えていただけると幸いです。

## Tips
### Facebook permissions
`config/initializers/devise.rb`に設定するscopeの一覧です。
[Permissions : Facebook開発者向けドキュメントの日本語訳とTips]
(http://facebook-docs.oklahome.net/archives/51927681.html)

### dotenv
`App Secret`などをハードコーディングしているのはよろしくないので、環境変数に入れてそれをRailsから読み込みます。
この際、[dotenv](https://github.com/bkeepers/dotenv)を使うと便利です。