Rails
Gem
OAuth

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

More than 1 year has passed since last update.

始めに

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

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を以下のように編集します。

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

APIの作成

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

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
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

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を追加します。

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
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ファイルを作ります。

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側アプリのユーザーを登録し、ログインさせます。

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

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

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

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
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)を呼び出すリンクを作れば完成です!

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