120
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

Organization

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

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

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
120
Help us understand the problem. What are the problem?