Help us understand the problem. What is going on with this article?

Rails5のAPIモードでdevise_token_authとomniauthを使ったログインを試す

More than 1 year has passed since last update.

ログイン処理をOmniauth認証のみで行なう必要があるので最低限ログインが出来る部分までを試したときのことを忘れないために投稿
omniauthで使うのはGoogle/Twitter/Doorkeeper
環境はCentOS6.9とPostgreSQL9系

これはRails5(API)だけど実際にRailsでSPAを作るなら、Rails5のAPIモードとGrapeのどっちがいいんだろう

プロジェクト作成

$ rails new Sample -d postgresql -B -T --api

使うgemは以下のとおり

Gemfile
gem 'rack-cors'
gem 'devise'
gem 'devise_token_auth'
gem 'omniauth'
gem 'omniauth-oauth2'
gem 'omniauth-google-oauth2'
gem 'omniauth-twitter'

インストールしたらdatabase.ymlを編集してDBの作成をしておく

devise_token_authのインストールと準備

$ rails g devise_token_auth:install User auth

生成されるmigrationファイルを以下のように編集して$ rails db:migrate

db/migrate/XXXXXXXXX_devise_token_auth_create_users.rb
class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table(:users) do |t|
      ## Required
      t.string :provider, :null => false, :default => "email"
      t.string :uid, :null => false, :default => ""

      ## Database authenticatable
      t.string :encrypted_password, :null => false, :default => ""

      ## Recoverable
      #t.string   :reset_password_token
      #t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, :default => 0, :null => false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      #t.string   :confirmation_token
      #t.datetime :confirmed_at
      #t.datetime :confirmation_sent_at
      #t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at
      ## User Info
      t.string :name
      t.string :nickname
      t.string :image
      t.string :email

      ## Tokens
      t.json :tokens

      t.timestamps
    end

    add_index :users, :email,                unique: true
    add_index :users, [:uid, :provider],     unique: true
    #add_index :users, :reset_password_token, unique: true
    #add_index :users, :confirmation_token,   unique: true
    #add_index :users, :unlock_token,       unique: true
  end
end

モデルは以下のように編集
:rememberableは不要な気もする

app/models/user.rb
class User < ActiveRecord::Base
  # Include default devise modules.
  devise  :rememberable, :omniauthable
  include DeviseTokenAuth::Concerns::User
end

これでomniauthを使ったログインのみになる

$ rails routes
             Prefix Verb     URI Pattern                            Controller#Action
auth_validate_token GET      /auth/validate_token(.:format)         devise_token_auth/token_validations#validate_token
       auth_failure GET      /auth/failure(.:format)                users/omniauth_callbacks#omniauth_failure
                    GET      /auth/:provider/callback(.:format)     users/omniauth_callbacks#omniauth_success
                    GET|POST /omniauth/:provider/callback(.:format) users/omniauth_callbacks#redirect_callbacks
   omniauth_failure GET|POST /omniauth/failure(.:format)            users/omniauth_callbacks#omniauth_failure
                    GET      /auth/:provider(.:format)              redirect(301)

googleやtwitterでログインする際に呼び出されるomniauth_callbacks_controller.rbの一部をオーバーライド

今回はお試しなのでユーザ情報の保存に成功したらその情報をjsonで返す。
本来ならrender_data_or_redirect()を使うのが正しいやり方。
以下は変更したメソッドのみ抜粋

app/controllers/users/omniauth_callbacks_controller.rb
module Users
  class OmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController
    include Devise::Controllers::Rememberable

    def omniauth_success
      get_resource_from_auth_hash
      create_token_info
      set_token_on_resource
      create_auth_params

      # ここは使わないのでコメントアウト
      #if resource_class.devise_modules.include?(:confirmable)
      #  # don't send confirmation email!!!
      #  @resource.skip_confirmation!
      #end

      sign_in(:user, @resource, store: false, bypass: false)

      # 動作確認用にユーザ情報を保存できたらjsonをそのまま返す処理
      if @resource.save!
        # update_token_authをつけることでレスポンスヘッダーに認証情報を付与できる。
        update_auth_header
        yield @resource if block_given?
        render json: @resource, status: :ok
      else
        render json: { message: "failed to login" }, status: 500
      end

      # 本実装時はこちらを使用する
      # @resource.save!
      #       
      # update_auth_header # これは自分で追加する
      # yield @resource if block_given?
      #
      # render_data_or_redirect('deliverCredentials', @auth_params.as_json, @resource.as_json)

    end

    protected
    def get_resource_from_auth_hash
      # find or create user by provider and provider uid
      @resource = resource_class.where({
        uid:      auth_hash['uid'],
        provider: auth_hash['provider']
      }).first_or_initialize

      if @resource.new_record?
        @oauth_registration = true
        # これが呼ばれるとエラーになるのでコメントアウトする
        #set_random_password
      end

      # sync user info with provider, update/generate auth token
      assign_provider_attrs(@resource, auth_hash)

      # assign any additional (whitelisted) attributes
      extra_params = whitelisted_params
      @resource.assign_attributes(extra_params) if extra_params

      @resource
    end
  end
end

上記の処理を呼び出すようにするためroutes.rbを編集

config/routes.rb
Rails.application.routes.draw do
  mount_devise_token_auth_for 'User', at: 'auth', controllers: { omniauth_callbacks: "users/omniauth_callbacks" }
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

initializerは以下のようにする

config/initializers/devise_token_auth.rb
DeviseTokenAuth.setup do |config|
  config.change_headers_on_each_request = false
  config.token_lifespan = 1.month
  config.headers_names = {:'access-token' => 'access-token',
                         :'client' => 'client',
                         :'expiry' => 'expiry',
                         :'uid' => 'uid',
                         :'token-type' => 'token-type' }
end

Omniauthの準備

application.rbに以下を追加
これをしないとAPIモードではエラーになる。
corsはクライアントとのやり取りのために追加

config/application.rb
module Sample
  class Application < Rails::Application
    config.load_defaults 5.1
    config.api_only = true

    # 主にdeviseを使うのに必要
    config.middleware.use Rack::MethodOverride
    config.middleware.use ActionDispatch::Cookies
    config.middleware.use ActionDispatch::Session::CookieStore
    config.middleware.use ActionDispatch::Flash

    # クロスドメイン対策は入れておいたほうが良い
    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*'
        resource '*',
                 :headers => :any,
                 :expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'],
                 :methods => [:get, :post, :options, :delete, :put]
      end
    end
  end
end

各サービスの設定をomniauth.rbに記載
App_IDとSecret_keyについてはomniauth.ymlを作成して格納
取得方法については省略
もちろん環境変数に入れておいてそれを読み取る形でも問題ない

config/initializer/omniauth.rb
require File.expand_path('lib/omniauth/strategies/doorkeeper', Rails.root)
Rails.application.config.middleware.use OmniAuth::Builder do
   OAUTH_CONFIG = YAML.load_file("#{Rails.root}/config/omniauth.yml")[Rails.env].symbolize_keys!
   provider :doorkeeper, OAUTH_CONFIG[:doorkeeper]['key'], OAUTH_CONFIG[:doorkeeper]['secret']

   provider :google_oauth2, OAUTH_CONFIG[:google]['key'], OAUTH_CONFIG[:google]['secret'], name: :google, scope: %w(email)

   provider :twitter, OAUTH_CONFIG[:twitter]['key'], OAUTH_CONFIG[:twitter]['secret]

end
config/omniauth.yml
production: &production
  doorkeeper:
    key: XXXXXXXXXXXXXXXXXXXXXXXXXX
    secret: XXXXXXXXXXXXXXXXXXXXXXXX
  google:
    key: XXXXXXXXXXXXXXXXXX
    secret: XXXXXXXXXXXXXXXXXXXx
  twitter:
    key: XXXXXXXXXXXXXXx
    secret: XXXXXXXXXXXXXXXXXXX

development: &development
  <<: *production

test:
  <<: *development

SiteURLとCallbackURLはそれぞれの開発者向けのサービスで以下のように設定
またGoogleはIPアドレスでは登録できず、twitterはIPアドレスじゃないと登録できない
twitterはIPアドレスとlocalhostどっちでも登録できる

Google
site_url     = http://localhost:3000
callback_url = http://localhost:3000/omniauth/google/callback
twitter
site_url     = http://127.0.0.1:3000
callback_url = http://127.0.0.1:3000/omniauth/twitter/callback
doorkeeper
site_url     = http://localhost:3000
callback_url = http://localhost:3000/omniauth/doorkeeper/callback

lib配下にdoorkeeper用のstrategyを作成

lib/omniauth/strategies/doorkeeper.rb
require 'omniauth-oauth2'
module OmniAuth
  module Strategies
    class Doorkeeper < OmniAuth::Strategies::OAuth2
      RAW_INFO_URL = 'api/v1/me'
      option :name, :doorkeeper

      option :client_options, {
        site: 'doorkeeperのURL'
      }

      uid { raw_info['uid'] }

      info do
        {
          email: raw_info['email'],
          name: raw_info['name']
        }
      end

      extra do
        { raw_info: raw_info }
      end

      def raw_info
        @raw_info ||= JSON.parse(access_token.get(RAW_INFO_URL).response.body)
      end

      def callback_url
        full_host + script_name + callback_path
      end
    end
  end
end

動作確認

一通り設定できたはずなので$ rails sでサーバを立ち上げて
ブラウザでhttp://localhost:3000/auth/googleと叩けば認証画面が表示されるので認証を行なうとDBに保存される
そしてResponseヘッダーにclient_idとuidとaccess_tokenとexpireが付与された状態で
DBに保存したユーザ情報がjsonで返ってくる。

本来は以下のように入力しないとだめ
http://localhost:3000/auth/google?omniauth_window_type=newWindow

え?クライアント側?知らない子ですね・・・

認証をキャンセルした場合の処理について【11/21追記】

facebookの認証も追加し、試しに「後で」をクリックしたらflashなんてメソッドねーぞ怒られた。
deviseのflashなんて使わないはずなのになぜ?と思ってログを見ると

development.log
・・・
Started GET "/omniauth/facebook?resource_class=User"
・・・
Processing by Devise::OmniauthCallbacksController#failure as HTML
・・・

DeviseのOmniauthCallbacksController#failureが呼ばれていた。
そこでomniauthのWikiにある以下の方法を試したけど反映されたようには見えず・・・

config/initializers/omniauth.rb
OmniAuth.config.on_failure = Proc.new { |env|
  OmniAuth::FailureEndpoint.new(env).redirect_to_failure
}

改めて調べているとstackoverflowにこのような書き込みを見つけた。
Google先生でざっくり翻訳してみると
「devise-token-authはdevies上のレイヤーであり、deviseはomniauth上のレイヤーなので、どこかon_failureコードはすでにmonkeypatchedされています。したがって、再monkeypatchするには、after_initalizeブロックに設定コードを入れなければなりません」
つ、つまりどういうことだってばよ・・・?
モンキーパッチがどこかで適用されているので、on_failureを使うにはモンキーパッチを再度当てる必要があるってこと?
えぇ・・・
とりあえず調べていく中で見つけた以下の方法でうまくいった。

config/initializers/devise.rb
Rails.application.config.to_prepare do
  Devise::OmniauthCallbacksController.class_eval do
    def failure
      # 認証をキャンセルした場合
      render json: { message: "Login failed." }, status: 401
    end
  end
end

また/auth/:providerというパスも/api/v1/auth/:providerとかに変更してしまうと、
Devise(多分)が/omniauth/:provider/callbackを見つけらずエラーになるということもわかったのでなるべくモデル作成時に設定したパスは弄らないほうが良い。

参考

RailsでいろんなSNSとOAuth連携/ログインする方法
DoorkeeperとDeviseでOAuth2によるログイン機能を作る
devise token auth を使って簡単に早くAPIを作る
クライアントAngularJS サーバーサイドRails5 におけるOmniauth 認証を試してみる
OmniAuth OAuth2 1.4.0 以降で Invalid Credentials エラー

AQeNku
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away