記事概要
Ruby on RailsにSNSアカウントでのログイン機能を実装する方法について、まとめる
前提
- Ruby on Railsでアプリケーションを作成している
- Gem「pry-rails」はインストール済みである
- Meta for Developersアカウントは登録済みである
- Googleアカウントは登録済みである
- 本番環境はRenderにデプロイしている
AWSでデプロイをしているアプリケーションでSNS認証を使用したい場合、ドメインの取得やセキュリティの観点からSSL化が必須になる。費用が発生する実装となるため、注意
サンプルアプリ(GitHub)
omniauth(Gem)
Google、Facebook、twitter等のSNSアカウントを用いてユーザー登録やログインなどを実装できるgem
※Omniauth認証はCSRF脆弱性が指摘されているため、omniauth-rails_csrf_protection
というGemをインストールして対策する
OmniAuthのGithub
google-oauth2のGithub
手順1(外部APIの設定)
Facebookの設定
- アプリ作成
- Meta for developersにアクセスする
- 画面上部のナビバーにある「マイアプリ」をクリックする
- 「アプリを作成」をクリックする
- 「アプリの詳細」が表示されるので、「アプリ名」に任意のアプリ名を入力し、「次へ」をクリックする
※今回のアプリ名は「sns」 - 「ユースケース」が表示されるので、「その他」を選択し、「次へ」をクリックする
- 「アプリタイプ選択」が表示されるので、「生活者」を選択し、「次へ」をクリックする
- アプリの内容を確認し、「アプリを作成」をクリックする
- 下記のような画面が表示され、アプリ作成が完了
- 設定
- 「アプリの設定」を選択し、「ベーシック」をクリックする
- 後ほど利用する「アプリID」「app secret」を確認する
- アプリのURL(プラットフォーム)を登録するため、画面最下部の「プラットフォームを追加」をクリックする
- 「Website」を選択し、「次へ」をクリックする
- サイトURL欄に
http://localhost:3000
と入力する - 「変更を保存」をクリックする
Googleの設定
- プロジェクト作成
- こちらのサイトにアクセスする
- ヘッダーにある「プロジェクトの選択」をクリックする
- モーダルウィンドウ内の右上にある「新しいプロジェクト」をクリックする
- 「プロジェクト名」に任意のプロジェクト名を入力し、「作成」をクリックする
※場所は「組織なし」のままでOK
※今回のプロジェクト名は「snsgoogle」
- OAuth同意
- 画面左側の「OAuth同意画面」をクリックする
- 「開始」をクリックする
- アプリ情報にある「アプリ名」に任意のアプリ名を入力する
※今回のアプリ名は「sns」 - アプリ情報にある「ユーザーサポートメール」は、自身のメールアドレスを選択する
- 「次へ」をクリックする
- 対象は「外部」を選択し、「次へ」をクリックする
- 連絡先情報にある「メールアドレス」に自身のメールアドレスを入力し、「次へ」をクリックする
- ポリシーに同意し、「続行」をクリックする
- 「作成」をクリックする
- 認証情報
- ナビゲーションメニュー>APIとサービス>認証情報 をクリックする
- ヘッダー付近に表示されている「+ 認証情報を作成」をクリックする
- プルダウンリストにある「OAuthクライアント ID」をクリックする
- 「アプリケーションの種類」は、「ウェブアプリケーション」を選択する
- 「承認済みのリダイレクト URI」の「+URI を追加」クリックする
-
http://localhost:3000/users/auth/google_oauth2/callback
を入力する - 「作成」をクリックする
- 後ほど利用する「クライアント ID」「クライアント シークレット」を確認する
- ライブラリ
- 「ライブラリ」をクリックする
- 「google+ API」と検索し、「google+ API」を選択する
- 「有効にする」をクリックする
手順2(アプリの作成)
ユーザー管理機能を実装
- usersコントローラーを作成する
% rails g controller users
- ルーティングを編集する
config/routes.rb
Rails.application.routes.draw do root 'users#index' resources :users, only: :new end
- deviseをインストールする
詳細は、こちらを参照 - devise関連のコマンドを実行する
# deviseの設定ファイルをRailsアプリケーションにインストールするコマンド % rails g devise:install # Userモデルの作成コマンド % rails g devise user # deviseのビューをインストールするコマンド % rails g devise:views
- ニックネーム、苗字、名前、誕生日カラムを追加するため、マイグレーションファイルを編集する
db/migrate/XXXXXXXXXXXXXXXX_devise_create_users.rb
# frozen_string_literal: true class DeviseCreateUsers < ActiveRecord::Migration[7.1] def change create_table :users do |t| t.string :nickname, null: false t.string :lastname, null: false t.string :firstname, null: false t.date :birthday, null: false ## Database authenticatable t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" # 省略
- マイグレートのコマンドを実行する
% rails db:migrate
- 追加したカラムに対応するフォームを設定するため、
app/views/devise/registrations/new.html.erb
のビューファイルを編集する<h2>Sign up</h2> <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> <%= render "devise/shared/error_messages", resource: resource %> <%#= ニックネームカラム %> <div class="field"> <%= f.label :nickname %><br /> <%= f.text_field :nickname, autofocus: true %> </div> <%#= 苗字カラム %> <div class="field"> <%= f.label :lastname %><br /> <%= f.text_field :lastname %> </div> <%#= 名前カラム %> <div class="field"> <%= f.label :firstname %><br /> <%= f.text_field :firstname %> </div> <%#= 誕生日カラム %> <div class="field"> <%= f.label :birthday %><br /> <%= f.date_select :birthday, start_year: 1950, end_year: 2019 %> </div> <div class="field"> <%= f.label :email %><br /> <%= f.email_field :email, autofocus: true, autocomplete: "email" %> </div> <%#= 省略 %>
- トップページを設定するため、
app/views/users/index.html.erb
を手動作成する -
app/views/users/index.html.erb
を編集する<% if user_signed_in? %> <li><%= "#{current_user.nickname}でログイン中" %></li> <li><%= link_to 'ログアウト', destroy_user_session_path, data: { turbo_method: :delete } %></li> <% else %> <p>ログインしていません</p> <li><%= link_to 'ログイン', new_user_session_path %></li> <li><%= link_to '新規登録', new_user_path %></li> <% end %>
- 新規登録ページを設定するため、
app/views/users/new.html.erb
を手動作成する -
app/views/users/new.html.erb
を編集する<%= link_to "メールアドレスで登録", new_user_registration_path%>
- 追加した4つのカラムの値を送信できるようにするため、コントローラーを編集する
app/contorollers/application_controller.rb
class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? private def configure_permitted_parameters # ニックネーム、苗字、名前、誕生日カラムの値を送信できるようにする devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname, :lastname, :firstname, :birthday]) end end
- 実装結果をブラウザで確認する
SNS認証を実装
- omniauthというGemをインストールする
詳細は、こちらを参照 - 環境変数を設定する
- ターミナルでコマンドを実行し、環境変数ファイルを開く
% vim ~/.zshrc
-
i
を押してインサートモードに移行する - 最下部に下記を追記する。※既存の記述は消去しない
export FACEBOOK_CLIENT_ID='メモしたアプリID' export FACEBOOK_CLIENT_SECRET='メモしたapp secret' export GOOGLE_CLIENT_ID='メモしたクライアントID' export GOOGLE_CLIENT_SECRET='メモしたクライアントシークレット'
- 編集が終わったら
escape
キーを押し、:wq
と入力して保存して終了する - ターミナルでコマンドを実行し、設定を反映させる
% source ~/.zshrc
- ターミナルでコマンドを実行し、環境変数ファイルを開く
- 環境変数が読み込めるか確認する
- コンソールを起動する
% rails c
- コマンドを実行し、環境変数を確認する
irb(main):001> ENV['FACEBOOK_CLIENT_ID'] => "設定しアプリID" # 数字だけ irb(main):002> ENV['FACEBOOK_CLIENT_SECRET'] => "設定したapp secret" # 数字と文字列 irb(main):003> ENV['GOOGLE_CLIENT_ID'] => "設定したクライアントID" # 末尾がgoogleusercontent.com irb(main):004> ENV['GOOGLE_CLIENT_SECRET'] => "設定したクライアントシークレット" # 英数字や_(アンダーバー)が含まれる
- コンソールを起動する
- アプリ側で環境変数を読み込む記述を行う
config/initializers/devise.rb
# 省略 Devise.setup do |config| # 省略 config.omniauth :facebook,ENV['FACEBOOK_CLIENT_ID'],ENV['FACEBOOK_CLIENT_SECRET'] config.omniauth :google_oauth2,ENV['GOOGLE_CLIENT_ID'],ENV['GOOGLE_CLIENT_SECRET'] end
SNS認証のレスポンスを把握
レスポンスの中の、uid
とprovider
をアプリケーションのデータベースに、ユーザー情報(誕生日やフルネームなど)とともに保存する
ただし、「SNS認証とユーザー登録のタイミングが異なる」仕様であるため、SNS認証時にはusersテーブルのレコードを作成することはできない。そのため、SNSに関わる別テーブルを用意した方が取り扱いは簡単
- SNS認証時の情報を保存するSnsCredentialモデルを作成する
% rails g model sns_credential
- Userモデルとのアソシエーションのために、外部キーとしてuser_idを保持するように、マイグレーションファイルを編集する
db/migrate/XXXXXXXXXXXXXXXX_create_sns_credentials.rb
class CreateSnsCredentials < ActiveRecord::Migration[7.1] def change create_table :sns_credentials do |t| t.string :provider t.string :uid t.references :user, foreign_key: true t.timestamps end end end
- コマンドを実行し、データベースに反映する
% rails db:migrate
- deviseでOmniAuthを使用できるようにするため、Userモデルを編集する
app/models/user.rb
class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :omniauthable, # Omniauthによる認証ができるようにする omniauth_providers: [:facebook, :google_oauth2] # FacebookとGoogleのOmniAuthを使用できる end
- Userモデルのアソシエーションを設定する
app/models/user.rb
# 省略 has_many :sns_credentials end
- SnsCredentialモデルのアソシエーションを設定する
app/models/sns_credential.rb
class SnsCredential < ApplicationRecord belongs_to :user end
- コマンドを実行し、deviseを再設定する
controllers/users以下にdeviseのクラスを継承したコントローラーが生成される
% rails g devise:controllers users
このコントローラー内に標準で実装されているdeviseの設定を上書きすることによって、deviseが提供するアクションの内容を再設定できる - 生成したコントローラーを使用させるためにdeviseのルーティングを変更する
config/routes.rb
Rails.application.routes.draw do devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks', registrations: 'users/registrations' } root 'users#index' resources :users, only: :new end
SNS認証を実現するためのメソッドを実装
手順 | 処理内容 |
---|---|
① | Facebook(またはGoogle)での認証が開始される |
② | Facebook(またはGoogle)側にリクエストが送られる |
③ | 認証を経て、コントローラーにSNSに登録されている情報が返される |
④ | SNSの情報から、ユーザー情報のみを取得して、既存のユーザー情報と照合を行う |
⑤ | 照合結果から、今回SNSで認証されたユーザーが、すでにアプリケーションに登録されているユーザーなのか判断する |
⑥ | 照合の結果、既存のユーザーが存在しない場合は、新規登録画面に遷移する |
⑦ | すでに同じ情報のユーザーがアプリケーションのDBに存在している場合は、ログイン処理を行う |
-
authorization
を定義するapp/controllers/users/omniauth_callbacks_controller.rb# frozen_string_literal: true class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController private def authorization @user = User.from_omniauth(request.env["omniauth.auth"]) end end
-
authorization
を呼び出す「facebook」と「google_oauth2」というアクションを定義するapp/controllers/users/omniauth_callbacks_controller.rb# frozen_string_literal: true class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController def facebook authorization end def google_oauth2 authorization end private def authorization @user = User.from_omniauth(request.env["omniauth.auth"]) end end
- 記述したアクションを呼び出せるように、
app/views/users/new.html.erb
を編集する※コードの記述方法は、OmniAuthのGitHubを参照<%= link_to "メールアドレスで登録", new_user_registration_path%> <%= button_to "Facebookで登録", user_facebook_omniauth_authorize_path, data: { turbo: false } %> <%= button_to "Googleで登録", user_google_oauth2_omniauth_authorize_path, data: { turbo: false } %>
※Railsアプリを高速化するための「turbo」をオフにするため、data: { turbo: false }
を記述。turboによって非同期通信が行われると認証処理後のリダイレクトがうまくいかないため、オフにする - Userモデルにメソッドを作成する
app/models/user.rb
# 省略 has_many :sns_credentials def self.from_omniauth(auth) end end
-
omniauth_callbacks_controller.rb
に記述したUser.from_omniauth
も呼び出せるようになったため、値を確認する-
binding.pry
を追記するapp/models/user.rb# 処理 has_many :sns_credentials def self.from_omniauth(auth) binding.pry # 処理を停止させるために追記 end end
- ブラウザで「新規登録」をクリックする
- 「Facebookで登録」をクリックする
- 「[アカウント名]としてログイン」をクリックする
- ターミナルで、処理が停止していることを確認する
-
auth
と入力して実行すると、Facebookに登録済みのアカウント情報が表示される -
binding.pry
の記述を削除する
-
- メソッドの中身を編集する
app/models/user.rb
# 省略 def self.from_omniauth(auth) # SNS認証を行ったことがあるかを判断し、存在しない場合はデータベースに保存 sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create # SNS認証したことがあればアソシエーションで取得し、無ければemailでユーザー検索して取得orビルド(保存はしない) user = User.where(email: auth.info.email).first_or_initialize( nickname: auth.info.name, email: auth.info.email ) end end
- コントローラーを編集する
app/controllers/users/omniauth_callbacks_controller.rb
# 省略 private def authorization # Userモデルから返ってきた値を@userに代入 @user = User.from_omniauth(request.env["omniauth.auth"]) if @user.persisted? #ユーザー情報が登録済みなので、新規登録ではなくログイン処理を行う sign_in_and_redirect @user, event: :authentication else #ユーザー情報が未登録なので、新規登録画面へ遷移する render template: 'devise/registrations/new' end end end
- ブラウザで確認する
- sourceコマンドを再実行する
% source ~/.zshrc
- サーバーを再起動する
- ブラウザで「新規登録」をクリックする
- 「Facebookで登録」をクリックする
- 「[アカウント名]としてログイン」をクリックする
- 新規登録画面が開き、ニックネーム・メールアドレス項目が入力済みであることを確認する
- その他の必要情報を入力し、アカウント作成できることを確認する
- sourceコマンドを再実行する
- Userモデルを編集する
app/models/user.rb
# 省略 def self.from_omniauth(auth) # SNS認証を行ったことがあるかを判断し、存在しない場合はデータベースに保存 sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create # SNS認証したことがあればアソシエーションで取得し、無ければemailでユーザー検索して取得orビルド(保存はしない) user = User.where(email: auth.info.email).first_or_initialize( nickname: auth.info.name, email: auth.info.email ) # userが登録済みであるか判断 if user.persisted? sns.user = user # SnsCredentialモデルとUserモデルを紐づける sns.save end user end end
- OmniAuthのリンクは新規登録とログインを兼ねているので、表示する文字だけ変更してログインのビュー
app/views/devise/sessions/new.html.erb
に追加する<h2>Log in</h2> <%= button_to 'Facebookでログイン', user_facebook_omniauth_authorize_path, data: { turbo: false } %> <%= button_to 'Googleでログイン', user_google_oauth2_omniauth_authorize_path, data: { turbo: false } %> <br> <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> <%#= 省略 %>
-
app/views/devise/shared/_links.html.erb
の下記記述を削除する<%- if devise_mapping.omniauthable? %> <%- resource_class.omniauth_providers.each do |provider| %> <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %><br /> <% end %> <% end %>
- ブラウザで確認する
- ブラウザで「ログイン」をクリックする
- 「Facebookでログイン」をクリックする
- 「[アカウント名]としてログイン」をクリックする
- 登録済みユーザーでログインできることを確認する
SNS認証時にはパスワード入力を不要にする
- アソシエーションにオプションを設定する
app/models/sns_credential.rb
class SnsCredential < ApplicationRecord belongs_to :user, optional: true # 外部キーの値がない場合でも保存ができる end
optional: true
を設定すると、レコード保存時に外部キーの値がない場合でも保存ができる - Userモデルを編集する
app/models/user.rb
def self.from_omniauth(auth) # SNS認証を行ったことがあるかを判断し、存在しない場合はデータベースに保存 sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create # SNS認証したことがあればアソシエーションで取得し、無ければemailでユーザー検索して取得orビルド(保存はしない) user = User.where(email: auth.info.email).first_or_initialize( nickname: auth.info.name, email: auth.info.email ) # userが登録済みであるか判断 if user.persisted? sns.user = user # SnsCredentialモデルとUserモデルを紐づける sns.save end { user: user, sns: sns } # snsに入っているsns_idをビューで扱えるようにするため、コントローラーに渡す end end
optional: true
を付与したので、3行目で外部キーのないレコードが保存される - Userモデルから送られたデータをビューで扱えるようにするため、コントローラーを編集する
app/controllers/users/omniauth_callbacks_controller.rb
# 省略 private def authorization # Userモデルから返ってきた値を@sns_infoに代入 @sns_info = User.from_omniauth(request.env["omniauth.auth"]) # @userには「nickname」と「email」の情報を保持させる @user = @sns_info[:user] if @user.persisted? #ユーザー情報が登録済みなので、新規登録ではなくログイン処理を行う sign_in_and_redirect @user, event: :authentication else #ユーザー情報が未登録なので、新規登録画面へ遷移する @sns_id = @sns_info[:sns].id # SNS認証の判断は、idのみで行う render template: 'devise/registrations/new' end end end
-
app/views/devise/registrations/new.html.erb
に、SNS認証を行っているか、行っていないかの条件分岐を記述する<%#= 省略 %> <div class="field"> <%= f.label :email %><br /> <%= f.email_field :email, autofocus: true, autocomplete: "email" %> </div> <%#= SNS認証を行っているか判断 %> <%if @sns_id.present? %> <%#= SNS認証を行なっている場合、パスワード項目は表示されない %> <%= hidden_field_tag :sns_auth, true %> <% else %> <div class="field"> <%= f.label :password %> <% @minimum_password_length %> <em>(<%= @minimum_password_length %> characters minimum)</em> <br /> <%= f.password_field :password, autocomplete: "new-password" %> </div> <div class="field"> <%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation, autocomplete: "new-password" %> </div> <% end %> <div class="actions"> <%= f.submit "Sign up" %> </div> <% end %>
- createアクションのコメントアウトを外して編集する
app/controllers/users/registrations_controller.rb
# frozen_string_literal: true class Users::RegistrationsController < Devise::RegistrationsController # before_action :configure_sign_up_params, only: [:create] # before_action :configure_account_update_params, only: [:update] # GET /resource/sign_up # def new # super # end # POST /resource def create # params[:sns_auth]を取得した時だけDevise.friendly_tokenを使ってパスワードを自動生成 if params[:sns_auth] == 'true' pass = Devise.friendly_token params[:user][:password] = pass params[:user][:password_confirmation] = pass end # superメソッドでdeviseのregistrations#createを実行させる super end # 省略
- ブラウザで確認する
- DBをリセットする
% rails db:migrate:reset
- サーバーを再起動する
- ブラウザで「新規登録」をクリックする
- 「Facebookで登録」をクリックする
- 「[アカウント名]としてログイン」をクリックする
- 新規登録画面が開き、下記であることを確認する
- ニックネーム・メールアドレス項目が入力済み
- パスワード入力項目がない
- その他の必要情報を入力し、アカウント作成できることを確認する
- 「ログアウト」をクリックする
- 「ログイン」をクリックする
- 「Facebookでログイン」をクリックする
- 「[アカウント名]としてログイン」をクリックする
- 登録済みユーザーでログインできることを確認する
- DBをリセットする
手順3(SNS認証を本番環境で利用する)
Renderにアプリをデプロイする
Renderへのデプロイ方法は、こちらを参照
Renderでのデプロイ時に環境変数を設定するが、以下の4つの変数も必ず設定する
変数名 | 変数の値 |
---|---|
FACEBOOK_CLIENT_ID | FacebookアプリのID |
FACEBOOK_CLIENT_SECRET | Facebookアプリのapp secret |
GOOGLE_CLIENT_ID | GoogleアプリのクライアントID |
GOOGLE_CLIENT_SECRET | Googleアプリのクライアントシークレット |
Facebookの設定
ローカル開発ではこの作業を省略できるが、本番環境では必須設定
- facebook for developersへ、アクセスする
- ヘッダーにある「マイアプリ」をクリックする
- 一覧の中から、先ほど作成したアプリを選択する
- 左のメニューから「Facebookログイン>設定」をクリックする
- 「有効なOAuthリダイレクトURI」に、
(アプリのURL)/users/auth/facebook/callback
を入力する入力例: https://mini-sns-41468.onrender.com/users/auth/facebook/callback
- 「変更を保存」をクリックする
- Render環境にて、FacebookのSNS認証でアカウント登録・ログインできることを確認する
Googleの設定
- こちらのサイトへ、アクセスする
- 作成したプロジェクトを選択し、認証情報をクリックする
- 作成した認証情報をクリックする
- 「承認済みのリダイレクトURI」に
(アプリのURL)/users/auth/google_oauth2/callback
を追記する入力例: https://mini-sns-41468.onrender.com/users/auth/google_oauth2/callback
- 「保存」をクリックする
- Render環境にて、GoogleのSNS認証でアカウント登録・ログインできることを確認する