DoorkeeperとDeviseでOAuth2によるログイン機能を作る


#始めに

2つのウェブサービスを運営しているとして、「片方のウェブサービスのアカウント（IDとパスワード）でもう片方のウェブサービスにログインしたい！」という、いわゆる「Facebookアカウントでログイン」的な物を、OAuth2による認証機能を提供してくれるGem「Doorkeeper」を用いて実装するまでのメモ書きです。

##環境
* Rails 4.2.3
* Ruby 2.2.2
* Vagrantによる仮想環境

##Doorkeeper
https://github.com/doorkeeper-gem/doorkeeper
OAuth2による認証機能をアプリに追加するGemです。これを使うことでアプリにFacebookAPIのような機能（ログインなどなど）を追加することが出来ます。

##Devise
https://github.com/plataformatec/devise
認証機能を追加する定番のGemです。大抵、必要な機能が増えてきて魔改造される運命にあります。

#provider側アプリの実装
RailsでAPIでのOAuth2認証をdoorkeeper + deviseで実装する
http://qiita.com/arakaji/items/39818b207f9c1c3c4058

provider側のアプリは基本的にこの記事の内容に沿って進めていきます。

##必要なGem

```ruby:Gemfile
gem 'devise'
gem 'doorkeeper'
gem 'omniauth'
gem 'oauth2'
```

```
$ bundle install
```

##deviseの準備

```
$ bundle exec rails g devise:install
$ bundle exec rails g devise user
$ bundle exec rake db:migrate
```

userの部分はお好きなモデル名をどうぞ。

##doorkeeperの準備
ここからが本番です。まずは下準備から。

```
$ bundle exec rails g doorkeeper:install
$ bundle exec rails g doorkeeper:migration
$ bundle exec rake db:migrate
```

以上を実行することで、doorkeeperに必要なファイルとDBが作られます。次にdoorkeeperでの認証ロジックにdeviseを用いるため、initializers/doorkeeper.rbを以下のように編集します。

```ruby:initializers/doorkeeper.rb
resource_owner_authenticator do
  current_user || warden.authenticate!(scope: :user)
end
```

userの部分はdeviseで作ったモデル名になります。
この後、アクセストークン取得のための設定を参考記事では行いますが、今回は認証コードを用いてトークンを取得するので、ここの設定は必要ありません。

##doorkeeperの使い方
###アプリケーション登録
doorkeeperではFacebookのAPIのように認証を利用するアプリケーションを登録し、それぞれに対してApplication_id, Secret_idが発行され、Callback_urlを設定します。

`bundle exec rails s`でRailsサーバーを立ち上げて、/oauth/applicationsにアクセスすると、登録されているアプリケーションの一覧が表示されます。そのページでNew Applicationボタンを押すとアプリケーションを登録するフォームが表示されるので、アプリケーションの名前とコールバックURLを入力すると、アプリケーションが登録されます。ここで入力するコールバックURLはclient側で設定したURLです。この記事の通りに設定した場合のURLは`http://clientのvagrantのip:port/users/oauth/doorkeeper/callback`となります。

![Doorkeeper 2015-07-13 18-34-26.png](https://qiita-image-store.s3.amazonaws.com/0/12164/57b5efc9-3bf3-49f8-ba14-f86bdceb0ca4.png "Doorkeeper 2015-07-13 18-34-26.png")

###APIの作成
認証した後、client側アプリが使うためのAPIを作ります。今回はログイン処理なので、providerが持つユーザーデータを引き渡す処理を書きます。

```ruby:app/controllers/api/api_controller.rb
module Api
  class ApiController < ::ApplicationController
  	# ApplicationControllerでauthenticate_userを呼び出している場合、
  	# ここでも処理がはしり、401エラーの元なので、切っておく
    before_action :authenticate_user!, only: []

    private

    def current_resource_owner
      User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
    end
  end
end
```

```ruby:app/controllers/api/credentials_controller.rb
module Api
  class CredentialsController < ApiController
  	# loginメソッドを外部から呼び出す際にdoorkeeperで認証処理する
    before_action :doorkeeper_authorize!

	# ユーザーのデータをjson形式で送る
    def login
      render json: { user: current_resource_owner }
    end
  end
end
```


#client側アプリの実装
##必要なGem

```ruby:Gemfile
gem 'devise'
gem 'omniauth'
gem 'omniauth-oauth2'
gem 'oauth2'
```

```
$ bundle install
```

##deviseの準備

```
$ bundle exec rails g devise:install
$ bundle exec rails g devise user
$ bundle exec rake db:migrate
```

いつも通り。

##oauthの処理を実装する
流れは

1. deviseで作ったモデルにoauthを対応させる準備をする
2. オレオレアプリ用のoauthのstrategyを作成する
3. コールバック処理を書く

こんな感じ。

###deviseで作ったモデルにoauthを対応させる準備をする
ざっくり言うとfacebookログインに対応させるときと同じ処理です。

まず始めに、deviseで作ったモデルに`:omniauthable`を追加します。

```ruby:user.rb
class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable, :recoverable,
         :rememberable, :trackable, :validatable, :confirmable, :omniauthable
end
```

これにより、URLヘルパーの
user_omniauth_authorize_path(provider)
user_omniauth_callback_path(provider)
が利用可能になり、ビューで`user_omniauth_authorize_path(provider)`を呼び出すことでOAuthを使ったログイン処理が走ります。

次に、oauthでは認証時にuid（provider毎に一意に決まるID）とprovider（oauth認証を提供するアプリ名）を使うので、それを保存するカラムをDBに作成します。

      
```
$ rails g migration AddUidToUser
```

```ruby:db/migrate/add_uid_to_user.rb
class AddUidToUser < ActiveRecord::Migration
  def change
    add_column :users, :uid, :string
    add_column :users, :provider, :string
  end
end
```

```
$ rake db:migrate
```

###オレオレアプリ用のoauthのstrategyを作成する

oauthではstrategyファイルを元に何処にアクセスして、どの情報を取ってくるかなどを決めます。

まずはdeviseのイニシャライザでoauthの設定を行い、どのstrategyを使うかを記述します。

```config/initializer/devise.rb
# strategyファイルのパスを指定して、ロード
require File.expand_path('lib/omniauth/strategies/doorkeeper', Rails.root)
Devise.setup do |config|
    config.omniauth(:doorkeeper, 登録したAPP_ID, 登録したAPP_SECRET)
end
```

次にstrategyファイルを作ります。

```ruby:lib/omniauth/strategies/doorkeeper.rb
module OmniAuth
  module Strategies
    class Doorkeeper < OmniAuth::Strategies::OAuth2
      option :name, :doorkeeper # strategyの名前　ここで指定した名前をdeviseで呼び出す
      option :client_options, site: provider側アプリのルートURL, authorize_path: '/oauth/authorize'
      
      # uidとして設定するデータを指定
      uid { raw_info['user']['id'] }
      # providerから送られてきたデータの内、どれを使いたいか
      info do
        { email: raw_info['user']['email'] }
      end
      
      # providerのAPIを叩いて、データを取ってくる
      def raw_info
        @raw_info ||= access_token.get('/api/login.json').parsed
      end
    end
  end
end
```

###コールバック処理を書く

認証に成功してコールバックしてきた際に行う処理を実装します。認証によって得られたトークンを使って、APIを叩き、ユーザーデータを取得、そのデータを用いてclient側アプリのユーザーを登録し、ログインさせます。

まずはコールバック処理をカスタマイズするために、ルーティングを設定し、独自のファイルを使うようにします。

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

次にそのファイルの内容を記述します。
認証によって得られたユーザー情報でclient側のユーザーを検索し、ヒットすればそのユーザーでログインします。ヒットしない場合は新規作成してログインします。

```ruby:app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def doorkeeper # メソッド名はstrategyで指定した名前
    @user = User.find_or_create_with_doorkeeper(request.env['omniauth.auth'])

    if @user.persisted?
      sign_in(@user)
      set_flash_message(:notice, :success, kind: 'doorkeeper') if is_navigational_format?
      redirect_to root_url
    else
      session['devise.doorkeeper_data'] = request.env['omniauth.auth']
      redirect_to root_url, alert: 'Doorkeeper ログインに失敗しました'
    end
  end
end
```

```ruby:app/models/user.rb
class User < ActiveRecord::Base
  def self.find_or_create_with_doorkeeper(auth)
    user = self.find_by(provider: auth.provider, uid: auth.uid )
    return user unless user.nil?

    self.create(
      email: auth.info.user.email,
      provider: auth.provider,
      uid: auth.uid,
      password: Devise.friendly_token[0, 20]
    )
  end
end
```

最後に、ビューに`user_omniauth_authorize_path(provider)`を呼び出すリンクを作れば完成です！

```ruby:view
<%= link_to 'オレオレアカウントでログイン', user_omniauth_authorize_path(:doorkeeper) %>
```

#幾つかの詰まりやすい点

##ローカルでテストするときはvagrantのprivate_networkのipを別々に設定する
vagrantを2個立てて、vagrant間で通信できるように、ipを別々に割り当てて下さい。また、先に立てたvagrantは後に立てたvagrantのipを認識できない？のでprovider側アプリのサーバーとなるvagrantを先に立ててください。最後に、いつものようにlocalhostではアクセスせずに、vagrantのIPでアクセスした方が上手くいきやすいです。

##staging, productionの時はhttpsで
ローカルの場合を除き、doorkeeperで設定したアプリケーションのコールバックURLはhttpsで無いといけないので、SSL通信の設定が必要です。

#最後に
幾つか面倒な点はありますが、自前で実装するよりかは楽だと思うので、是非試してみて下さい。APIの実装もdoorkeeperを用いればだいぶ楽ですし、2つのアプリを連携させることで、出来ることの幅が大きくなると思います。

あと、この記事書くの疲れました0(:3　)〜

ブログでは技術Tips以外の記事も定期的にポストしています。
もしよかったらブログもどうぞ╭( ･ㅂ･)و
https://hackalive.net