##内容
RailsアプリにOmniauth認証(google_oauth2, facebook)を導入する方法とその過程で学んだことを紹介する記事です。全体の流れやコードの意図を説明した記事はあまり見つからなかったと思ったので、その辺りを中心に解説したいと思います。
##対象
rails初心者でOmniauth認証の導入に挑む人。
もし某スクールの後輩で見てくれた人がいた場合、
非常に面白い機能なので、まずは自力で挑戦することをお勧めします。
(そもそも違う環境でちゃんと動くとも、正解とも限りません。。。)
少しでもご参考になれば、と思って書きます。
##前提条件
-ruby 2.5.1p57
-Rails 5.2.3
-gem 'devise' 4.7.1
(ローカル環境のみの対応です)
##1.そもそも
omniauthでは、あるアプリにおけるログイン認証の代わりや、外部機能の使用許可をすることができます。
本実装においては、前者の機能を使用しています。
流れとしては、本アプリのパスワード入力を、SNSへのログイン(≒Cookieによりほぼ自動ログイン)で代用するイメージです。以下に全体図のイメージを示します。
青矢印のフローを実装していきます!
##2.実装
###2-1.gemの導入
まず、gemfileに以下を追記後、bundle installを実施します。
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.omniauth :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_KEY']
config.omniauth :google_oauth2, ENV['GOOGLE_ID'], ENV['GOOGLE_KEY']
ターミナルにて、"vim ~/.bash_profile"を実行し、取得したIDとキーを記入します。
export FACEBOOK_ID="取得したID"
export FACEBOOK_KEY="取得したキー"
export GOOGLE_ID="取得したID"
export GOOGLE_KEY="取得したキー"
"source ~/.bash_profile"を実行し、環境変数を有効化しましょう。
###2-3.routingの設定
SNS側からcallbackが来た際に使用するコントローラーを定義してあげます。
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に定義します。
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上で欠けている場合を想定し、
インスタンスの作成に留めました。
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にリンク先を記入して終了です!!
<%= 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を導入し、リンクの書き方を変更して、本当の終了です。
gem "omniauth-rails_csrf_protection"
<%= 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モードにするなど、少し設定が必要です。
↓設定
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
↓テストコード
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
####長文にも関わらず、最後までお読みいただきありがとうございました。
####初投稿記事なので、ご意見、修正点などいただけましたら、幸いです!