19
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rails+DeviseへのOmniauthの導入(ざっくり仕組み、CSRF対策、単体テスト含む)

Last updated at Posted at 2019-12-02

##内容
 RailsアプリにOmniauth認証(google_oauth2, facebook)を導入する方法とその過程で学んだことを紹介する記事です。全体の流れやコードの意図を説明した記事はあまり見つからなかったと思ったので、その辺りを中心に解説したいと思います。

##対象
 rails初心者でOmniauth認証の導入に挑む人。

 もし某スクールの後輩で見てくれた人がいた場合、
 非常に面白い機能なので、まずは自力で挑戦することをお勧めします。
 (そもそも違う環境でちゃんと動くとも、正解とも限りません。。。)
 少しでもご参考になれば、と思って書きます。

##前提条件
-ruby 2.5.1p57
-Rails 5.2.3
-gem 'devise' 4.7.1
(ローカル環境のみの対応です)
 
##1.そもそも
 omniauthでは、あるアプリにおけるログイン認証の代わりや、外部機能の使用許可をすることができます。
本実装においては、前者の機能を使用しています。
流れとしては、本アプリのパスワード入力を、SNSへのログイン(≒Cookieによりほぼ自動ログイン)で代用するイメージです。以下に全体図のイメージを示します。
 Qiita Oauth用資料.001.jpeg
青矢印のフローを実装していきます!

##2.実装
###2-1.gemの導入
まず、gemfileに以下を追記後、bundle installを実施します。

Gemfile
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'

###2-2.設定
各SNSサイト(Google developers console, facebook for developers)にて、
URLの登録および、ID, SECRET KEYを取得します。
(ご参考サイト様:https://qiita.com/hidepino/items/a1eb9d2f32ce33389f20)

環境変数を設定します。

config/initializers/devise.rb
config.omniauth :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_KEY']
config.omniauth :google_oauth2, ENV['GOOGLE_ID'], ENV['GOOGLE_KEY']

ターミナルにて、"vim ~/.bash_profile"を実行し、取得したIDとキーを記入します。

bash_profile
export FACEBOOK_ID="取得したID"
export FACEBOOK_KEY="取得したキー"
export GOOGLE_ID="取得したID"
export GOOGLE_KEY="取得したキー"

"source ~/.bash_profile"を実行し、環境変数を有効化しましょう。

###2-3.routingの設定
 SNS側からcallbackが来た際に使用するコントローラーを定義してあげます。

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

###2-4.callbackコントローラーの作成、記述 (イメージ図の手順④⑤に当たります)

 "rails g devise:controllers users"を実行し、users/omniauth_callbacks_controllerを作成し、以下を記述します。ここでは、callbackが来た際に行うアクションを設定しています。SNS側から来た情報であるauth_hashは、request.env["omniauth.auth"]として使用していきます。
クラスメソッド"from_omniauth"は次のステップでUser.rbに定義します。

controllers/users/omniauth_callbacks_controller.rb
def facebook
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?  #もし@userがDBに既にいたら、ログイン状態にします  
      sign_in_and_redirect @user, event: :authentication 
      set_flash_message(:notice, :success, kind: 'Facebook') if is_navigational_format?
    else #もし@userがDBにいない場合、新規登録ページにリダイレクトします
      session["devise.facebook_data"] = request.env["omniauth.auth"]
    #データをsessionに入れることによって、新規登録ページの入力欄に、予め情報を入れておくなどが可能になります。
      redirect_to 新規登録ページ
    end
  end

  def google_oauth2
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication 
      set_flash_message(:notice, :success, kind: 'google') if is_navigational_format?
    else
      session["devise.google_data"] = request.env["omniauth.auth"][:info]
      #google認証の場合は、なぜかauth_hashの容量が大きく、一瞬で容量オーバーとなるため、新規登録時に必要な情報のみをsessionに渡すこととしました。(おそらく画像データのせい?)
      redirect_to 新規登録ページ
    end
  end

###2-5.メソッドの定義 (イメージ図の手順④⑤に当たります)
####ユーザー登録の流れを設定します。 ここは設計により異なります!
既存ユーザーであるかの識別は,uidやemailアドレスにて、実施されている記事を多く見受けましたが、
本実装では、usersテーブルとsns_credentialsテーブルを別で用意したため、
(1人のuserが複数のsns_credentialsを持つことを想定しています。)
ユーザーの識別はemailで行うことにしました。

さらに、既存ユーザーがいなかった場合に関して、
ここでuser, sns_credentialをDBへ登録することもできますが、
本アプリにて必要な情報が、auth_hash上で欠けている場合を想定し、
インスタンスの作成に留めました。

models/user.rb
def self.from_omniauth(auth)
    user = User.where(email: auth.info.email).first
    sns_credential_record = SnsCredential.where(provider: auth.provider, uid: auth.uid)
    if user.present?
      unless sns_credential_record.present?
        SnsCredential.create(
          user_id: user.id,
          provider: auth.provider,
          uid: auth.uid
        )
      end
    elsif
      user = User.new(
        id: User.all.last.id + 1,
        email: auth.info.email,
        password: Devise.friendly_token[0, 20],
        nickname: auth.info.name,
        last_name: auth.info.last_name,
        first_name: auth.info.first_name,
      )
      SnsCredential.new(
        provider: auth.provider,
        uid: auth.uid,
        user_id: user.id
      )
    end 
  user
  end

###2-6.リンクの導入

最後にViewにリンク先を記入して終了です!!

view.html.erb
<%= link_to "Sign in with Facebook", user_facebook_omniauth_authorize_path %>
<%= link_to "Sign in with Google", user_google_oauth2_omniauth_authorize_path %>

###2-7.CSRF対策

と言いたいところですが、Omniauth認証はCSRF脆弱性が指摘されているので、
以下の対策用のgemを導入し、リンクの書き方を変更して、本当の終了です。

Gemfile
gem "omniauth-rails_csrf_protection"
rspec/view.html.erb
<%= link_to "Sign in with Facebook", user_facebook_omniauth_authorize_path, method: :post %>
<%= link_to "Sign in with Google", user_google_oauth2_omniauth_authorize_path, method: :post %>

##3.テストコード(一例)
 対象:sns_credential.rbのuidのunique制約が作動するか
ダミーのauth_hashを作成したり、omniauthをtestモードにするなど、少し設定が必要です。
 
↓設定

rails_helper.rb
module OmniauthMocks
  def facebook_mock
    OmniAuth.config.mock_auth[:facebook] = OmniAuth::AuthHash.new(
      {
        provider: 'facebook',
        uid: '12345',
        info: {
          name: 'mockuser',
          email: 'sample@test.com'
        },
        credentials: {
          token: 'hogefuga'
        }
      }
    )
  end
end


RSpec.configure do |config|
  OmniAuth.config.test_mode = true
  config.include OmniauthMocks
end

↓テストコード

spec/models/sns_credentials_spec.rb

RSpec.describe SnsCredential, type: :model do
  describe  '#facebook validation' do
    before do
      Rails.application.env_config['omniauth.auth'] = facebook_mock
    end
    context '認可サーバーから返ってきたメールアドレスを、すでに登録済みのuserが持っていた場合' do
      before do
        user = create(:user, email: 'sample@test.com')
      end
      context '認可サーバーから帰ってきた情報とprovider名が異なるが、同じuidを持つSnsCredentialレコードがあった場合' do
        before do
          SnsCredential.create(provider: 'google_oauth2', uid: '12345', user_id: '1')
        end
          example 'uidのvalidation(unique制約)が機能するか' do
            expect(SnsCredential.create(provider: 'facebook', uid: '12345', user_id: '1').errors[:uid]).to include('はすでに存在します')
          end         
      end
    end
  end
end

##4.考察
・設計が良くなかったと思いますが、結局、"本アプリに登録したemailアドレス"と"SNS側からトークンで帰って来るemailアドレス"の照合をしているだけと言えます。結果、SNSに登録したemailとパスワードがあれば、本アプリの認証をパスされてしまう事になるので、セキュリティ的な甘さを感じました。。。
(SNS側は別デバイスでのログインを見張る、SMS認証等、強固なようなので、そこは安心と思います)
対策としては、認証時にもう1ハードルが必要かもしれません。

・また、アドレスや住所等の個人情報がサーバーサイド側に飛ぶので、ユーザー目線としては、信頼できないサイトでは使うべきではない、と思いました。。。

##5.参考にさせて頂いた記事様
https://github.com/plataformatec/devise/wiki/OmniAuth%3A-Overview
https://github.com/mkdynamic/omniauth-facebook/blob/master/README.md
https://github.com/zquestz/omniauth-google-oauth2/blob/master/README.md
https://github.com/cookpad/omniauth-rails_csrf_protection
https://qiita.com/hidepino/items/a1eb9d2f32ce33389f20

####長文にも関わらず、最後までお読みいただきありがとうございました。:bow_tone3:
####初投稿記事なので、ご意見、修正点などいただけましたら、幸いです!:blush:

19
22
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?