RailsにDevise+OmniAuthでユーザ認証を実装する手順

Ruby on Railsアプリケーションに、以下の認証方法を実装する。
今回は1→2の順序で実装する。

1. メールアドレス
2. OAuth（Facebook/Twitter）

## 動作環境

- Ruby on Rails 4.1.1
- Devise 3.3.0
- OmniAuth 1.2.2

## 全体の概要

先に以下の記事に目を通すことで、全体の概要を把握することができる。

- http://post.simplie.jp/posts/43
- http://qiita.com/mosa_siru/items/9f1faa509f4d3653a1b2
- http://qiita.com/ytkt/items/d78841f7dea5e29f38ee
- http://easyramble.com/implement-devise-and-ominiauth-on-rails.html
- https://github.com/plataformatec/devise/wiki/OmniAuth%3A-Overview

## 1. メールアドレスによる認証の実装

### Gemfileの編集

```ruby:Gemfile
gem 'devise'
```

### インストール

```
$ bundle install
```

### Deviseの各ファイルを生成

```
$ rails g devise:install
```

### Deviseを日本語化

`config/locales/devise.en.yml`を`devise.ja.yml`にリネームし、以下の内容をコピペする。
もちろんRailsアプリ自体も日本語化しておく必要がある。

- [devise-i18n/ja.yml at master · tigrish/devise-i18n](https://raw.githubusercontent.com/tigrish/devise-i18n/master/rails/locales/ja.yml)

```yaml:config/locales/devise.ja.yml
ja:
  devise:
    confirmations:
      confirmed: アカウントを登録しました。
      send_instructions: 登録方法を数分以内にメールでご連絡します。
      send_paranoid_instructions: メールアドレスが登録されていれば、数分以内にアカウントを確認する方法が記載されているメールが届きます。

          :
```

### Userモデルを生成

Deviseのジェネレータを利用することで、あらかじめモジュールが定義されたモデルファイルを生成することができる。

```
$ rails g devise user
```

### 利用するモジュールを編集

Userモデルでは、`devise`メソッドによりどのモジュールを利用するかを定義できる。

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

利用できるモジュールについてはREADMEを参照。

- https://github.com/plataformatec/devise

### マイグレーションの実行

`rails g devise user`したときに`db/migrate/xxx_devise_create_users.rb`という名前でマイグレーションファイルも生成されている。
これをDBに適用する。

```
$ rake db:migrate
```

### サインアップフォームの表示

以上で、`http://〜/users/sign_up`にアクセスするとフォームが表示される。
この時点でユーザ登録ができる状態になっている。

## 2. OAuthでの認証

### Gemfileの編集

```ruby:Gemfile
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-twitter'
```

### インストール

```
$ bundle install
```

### カラムの追加

OmniAuthでは`uid`（一意のID）と`provider`（FacebookやTwitterなど）をカラムとして利用する。

```
$ rails g migration AddColumnsToUsers uid:string provider:string
```

### マイグレーションの実行

```
$ rake db:migrate
```

### APIキーの取得

#### Facebook

以下よりアプリケーションを作成する。

- [Facebook Developers](https://developers.facebook.com/)

作成が完了したら、設定より「Add Platform」→「Website」を選択する。
サイトURLにURLを入力する（例：`http://192.168.33.10:3000/`）。

#### Twitter

以下よりアプリケーションを作成する。

- [Twitter Application Management](https://apps.twitter.com/)

作成が完了したら、「Settings」より以下の設定を行なう。

1. Callback URL
  - 例：`http://〜/users/auth/twitter`
2. 以下にチェックを入れる：
  - Allow this application to be used to Sign in with Twitter

##### 参考

- http://stackoverflow.com/questions/7086970/devise-omniauth-and-twitter

### Deviseの設定

上記で取得したプロバイダの各キーを設定する。
開発/ステージング/本番環境と分ける場合は`Rails.env.production?`などを用いて振り分ける。

```ruby:config/initializers/devise.rb
Devise.setup do |config|
  # ...

  config.omniauth :facebook, 'App ID', 'App Secret'
  config.omniauth :twitter, 'API key', 'API secret'
end
```

こちらも、実際の運用ではハードコーディングせず`dotenv`などに書いた方がよい。

### Userモデルにモジュールを追加

`devise`メソッドに`:omniauthable`を追加する。

```ruby:app/models/user.rb
devise :database_authenticatable, :registerable,
       :recoverable, :rememberable, :trackable, :validatable, :omniauthable
```

### Userモデルにfindメソッドを実装

`uid`と`provider`の組み合わせは一意であり、これによりユーザを取得する。
レコードに存在しない場合は作成する。

```ruby:app/models/user.rb
class User < ActiveRecord::Base
  # ...

  def self.find_for_oauth(auth)
    user = User.where(uid: auth.uid, provider: auth.provider).first

    unless user
      user = User.create(
        uid:      auth.uid,
        provider: auth.provider,
        email:    User.dummy_email(auth),
        password: Devise.friendly_token[0, 20]
      )
    end

    user
  end

  private

  def self.dummy_email(auth)
    "#{auth.uid}-#{auth.provider}@example.com"
  end
end
```

メールアドレスでの認証も実装している場合、OAuthでの認証時もメールアドレスを保存する必要がある。
ここでは、`uid`と`provider`の組み合わせが一意なことを利用して、`self.dummy_email`のように生成している。

#### 参考

- http://stackoverflow.com/questions/18504308/getting-omniauth-facebook-and-omniauth-twitter-work
- http://sourcey.com/rails-4-omniauth-using-devise-with-twitter-facebook-and-linkedin/
- http://easyramble.com/devise-and-omniauth-specs.html

### Userコントローラにコールバック処理を実装

`provider`と同じ名前のメソッドを定義する必要がある。
ただ、基本的に各プロバイダでのコールバック処理は共通しているので、`callback_from`メソッドに統一している。

```ruby:app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    callback_from :facebook
  end

  def twitter
    callback_from :twitter
  end

  private

  def callback_from(provider)
    provider = provider.to_s

    @user = User.find_for_oauth(request.env['omniauth.auth'])

    if @user.persisted?
      flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize)
      sign_in_and_redirect @user, event: :authentication
    else
      session["devise.#{provider}_data"] = request.env['omniauth.auth']
      redirect_to new_user_registration_url
    end
  end
end
```

### ルーティング処理

以下のように、OAuthのコールバック用のルーティングを設定する。

```ruby:config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }

  # ...
end
```

### 認証用リンクを追加

以下によりリンクが生成される。
これをビューの任意の位置に記述する。

```ruby
user_omniauth_authorize_path(:facebook)
user_omniauth_authorize_path(:twitter)
```

## メモ

### persisted?メソッドについて

`.persisted?`は、レシーバがデータベースに保存済みかどうかを確認する。
上の例では、保存に失敗したら`users/sign_up`にリダイレクトしている。

- http://railsdoc.com/references/persisted%3F
- http://stackoverflow.com/questions/9211548/rails-3-1-devise-oauth-for-facebook-routing-confusion

## その他の参考記事

- http://easyramble.com/devise-on-rails.html