Last active
December 27, 2020 05:10
-
-
Save mnishiguchi/182d397b131d3c069135 to your computer and use it in GitHub Desktop.
Devise+OmniAuthでQiita風の複数プロバイダ認証 ref: https://qiita.com/mnishiguchi/items/e15bbef61287f84b546e
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class DeviseCreateUsers < ActiveRecord::Migration[5.0] | |
| def change | |
| create_table :users do |t| | |
| ## Database authenticatable | |
| t.string :email, null: false, default: "" | |
| 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.inet :current_sign_in_ip | |
| t.inet :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 | |
| t.timestamps null: false | |
| end | |
| add_index :users, :email, 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class ConfirmationsController < Devise::ConfirmationsController | |
| # Override | |
| def show | |
| self.resource = resource_class.confirm_by_token(params[:confirmation_token]) | |
| yield resource if block_given? | |
| if resource.errors.empty? | |
| set_flash_message(:notice, :confirmed) if is_flashing_format? | |
| sign_in(resource) #<== この一行を加えるのみ | |
| respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) } | |
| else | |
| respond_with_navigational(resource.errors, :status => :unprocessable_entity){ render :new } | |
| end | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Devise.setup do |config| | |
| ... | |
| config.omniauth :facebook, "KEY", "SECRET" | |
| config.omniauth :twitter, "KEY", "SECRET" | |
| ... | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| require 'mail' | |
| class EmailValidator < ActiveModel::EachValidator | |
| def validate_each(record,attribute,value) | |
| begin | |
| m = Mail::Address.new(value) | |
| # We must check that value contains a domain, the domain has at least | |
| # one '.' and that value is an email address | |
| r = m.domain!=nil && m.domain.match('\.') && m.address == value | |
| rescue Exception => e | |
| r = false | |
| end | |
| record.errors[attribute] << (options[:message] || "is invalid") unless r | |
| # 仮emailから変更しないとエラーになるようにする。 | |
| record.errors[attribute] << 'must be given. Please give us a real one!!!' unless value !~ User::TEMP_EMAIL_REGEX | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| rails g model SocialProfile user:references provider uid name nickname email url image_url description others:text credentials:text raw_info:text |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| .row | |
| .col-sm-offset-3.col-sm-6 | |
| h1 Add Email | |
| = simple_form_for(@user, url: finish_signup_path(@user)) do |f| | |
| = f.input :username, autofocus: true, class: 'form-control', placeholder: "Username" | |
| = f.input :email, autofocus: true, class: 'form-control', placeholder: "Email" | |
| .form-group | |
| = f.submit 'Add email', class: 'btn btn-primary' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ... | |
| # ruby 2.3.1 | |
| gem 'rails', '>= 5.0.0.rc2', '< 5.1' | |
| gem 'devise', '4.2' | |
| gem 'omniauth', '~> 1.3', '>= 1.3.1' | |
| gem 'omniauth-facebook', '~> 3.0' | |
| gem 'omniauth-twitter', '~> 1.2', '>= 1.2.1' | |
| ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module OAuthPolicy | |
| class Base | |
| attr_reader :provider, :uid, :name, :nickname, :email, :url, :image_url, | |
| :description, :other, :credentials, :raw_info | |
| end | |
| class Facebook < OAuthPolicy::Base | |
| def initialize(auth) | |
| @provider = auth["provider"] | |
| @uid = auth["uid"] | |
| @name = auth["info"]["name"] | |
| @nickname = "" | |
| @email = "" | |
| @url = "https://www.facebook.com/" | |
| @image_url = auth["info"]["image"] | |
| @description = "" | |
| @credentials = auth["credentials"].to_json | |
| @raw_info = auth["extra"]["raw_info"].to_json | |
| freeze | |
| end | |
| end | |
| class Twitter < OAuthPolicy::Base | |
| def initialize(auth) | |
| @provider = auth["provider"] | |
| @uid = auth["uid"] | |
| @name = auth["info"]["name"] | |
| @nickname = auth["info"]["nickname"] | |
| @email = "" | |
| @url = auth["info"]["urls"]["Twitter"] | |
| @image_url = auth["info"]["image"] | |
| @description = auth["info"]["description"].try(:truncate, 255) | |
| @credentials = auth["credentials"].to_json | |
| @raw_info = auth["extra"]["raw_info"].to_json | |
| freeze | |
| end | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module OAuthService | |
| class GetOAuthUser | |
| def self.call(auth) | |
| # 認証データに対応するSocialProfileが存在するか確認し、なければSocialProfileを新規作成。 | |
| # 認証データをSocialProfileオブジェクトにセットし、データベースに保存。 | |
| profile = SocialProfile.find_for_oauth(auth) | |
| # ユーザーを探す。 | |
| # 第1候補:ログイン中のユーザー、第2候補:SocialProfileオブジェクトに紐付けされているユーザー。 | |
| user = current_or_profile_user(profile) | |
| unless user | |
| # 第3候補:認証データにemailが含まれていればそれを元にユーザーを探す。 | |
| user = User.where(email: email).first if verified_email_from_oauth(auth) | |
| # 見つからなければ、ユーザーを新規作成。 | |
| user ||= find_or_create_new_user(auth) | |
| end | |
| associate_user_with_profile!(user, profile) | |
| user | |
| end | |
| private | |
| class << self | |
| def current_or_profile_user(profile) | |
| user = User.current_user.presence || profile.user | |
| end | |
| # 見つからなければ、ユーザーを新規作成。emailは後に確認するので今は仮のものを入れておく。 | |
| # TEMP_EMAIL_PREFIXを手掛かりに後に仮のものかどうかの判別が可能。 | |
| # OmniAuth認証時はパスワード入力は免除するので、ランダムのパスワードを入れておく。 | |
| def find_or_create_new_user(auth) | |
| # Query for user if verified email is provided | |
| email = verified_email_from_oauth(auth) | |
| user = User.where(email: email).first if email | |
| if user.nil? | |
| temp_email = "#{User::TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com" | |
| user = User.new( | |
| username: auth.extra.raw_info.name, | |
| email: email ? email : temp_email, | |
| password: Devise.friendly_token[0,20] | |
| ) | |
| # email確認メール送信を延期するために一時的にemail確認済みの状態にする。 | |
| user.skip_confirmation! | |
| # email仮をデータベースに保存するため、validationを一時的に無効化。 | |
| user.save(validate: false) | |
| user | |
| end | |
| end | |
| def verified_email_from_oauth(auth) | |
| auth.info.email if auth.info.email && (auth.info.verified || auth.info.verified_email) | |
| end | |
| # ユーザーとSocialProfileオブジェクトを関連づける。 | |
| def associate_user_with_profile!(user, profile) | |
| profile.update!(user_id: user.id) if profile.user != user | |
| end | |
| end | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class OmniauthCallbacksController < Devise::OmniauthCallbacksController | |
| # いくつプロバイダーを利用しようが処理は共通しているので本メソッドをエイリアスとして流用。 | |
| def callback_for_all_providers | |
| unless env["omniauth.auth"].present? | |
| flash[:danger] = "Authentication data was not provided" | |
| redirect_to root_url and return | |
| end | |
| provider = __callee__.to_s | |
| user = OAuthService::GetOAuthUser.call(env["omniauth.auth"]) | |
| # ユーザーがデータベースに保存されており、且つemailを確認済みであれば、ユーザーをログインする。 | |
| if user.persisted? && user.email_verified? | |
| sign_in_and_redirect user, event: :authentication | |
| set_flash_message(:notice, :success, kind: provider.capitalize) if is_navigational_format? | |
| else | |
| user.reset_confirmation! | |
| flash[:warning] = "We need your email address before proceeding." | |
| redirect_to finish_signup_path(user) | |
| end | |
| end | |
| alias_method :facebook, :callback_for_all_providers | |
| alias_method :twitter, :callback_for_all_providers | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module OmniauthCallbacksHelper | |
| class GetOAuthUser | |
| def self.call(auth) | |
| # 認証データに対応するSocialProfileが存在するか確認し、なければSocialProfileを新規作成。 | |
| # 認証データをSocialProfileオブジェクトにセットし、データベースに保存。 | |
| profile = SocialProfile.find_for_oauth(auth) | |
| # ユーザーを探す。 | |
| # 第1候補:ログイン中のユーザー、第2候補:SocialProfileオブジェクトに紐付けされているユーザー。 | |
| user = current_or_profile_user(profile) | |
| unless user | |
| # 第3候補:認証データにemailが含まれていればそれを元にユーザーを探す。 | |
| user = User.where(email: email).first if verified_email_from_oauth(auth) | |
| # 見つからなければ、ユーザーを新規作成。 | |
| user ||= find_or_create_new_user(auth) | |
| end | |
| associate_user_with_profile!(user, profile) | |
| user | |
| end | |
| private | |
| class << self | |
| def current_or_profile_user(profile) | |
| user = User.current_user.presence || profile.user | |
| end | |
| # 見つからなければ、ユーザーを新規作成。emailは後に確認するので今は仮のものを入れておく。 | |
| # TEMP_EMAIL_PREFIXを手掛かりに後に仮のものかどうかの判別が可能。 | |
| # OmniAuth認証時はパスワード入力は免除するので、ランダムのパスワードを入れておく。 | |
| def find_or_create_new_user(auth) | |
| # Query for user if verified email is provided | |
| email = verified_email_from_oauth(auth) | |
| user = User.where(email: email).first if email | |
| if user.nil? | |
| temp_email = "#{User::TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com" | |
| user = User.new( | |
| username: auth.extra.raw_info.name, | |
| email: email ? email : temp_email, | |
| password: Devise.friendly_token[0,20] | |
| ) | |
| user.skip_confirmation! # email確認メール送信を延期するために一時的にemail確認済みの状態にする。 | |
| user.save(validate: false) # email仮をデータベースに保存するため、validationを一時的に無効化。 | |
| user | |
| end | |
| end | |
| def verified_email_from_oauth(auth) | |
| auth.info.email if auth.info.email && (auth.info.verified || auth.info.verified_email) | |
| end | |
| # ユーザーとSocialProfileオブジェクトを関連づける。 | |
| def associate_user_with_profile!(user, profile) | |
| profile.update!(user_id: user.id) if profile.user != user | |
| end | |
| end | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class RegistrationsController < Devise::RegistrationsController | |
| protected | |
| # Override | |
| def update_resource(resource, params) | |
| resource.update_without_password(params) | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Rails.application.routes.draw do | |
| ... | |
| # Deviseのコントローラを上書きするため。 | |
| devise_for :users, controllers: { omniauth_callbacks: 'omniauth_callbacks', | |
| registrations: "registrations", | |
| confirmations: "confirmations" } | |
| # OmniAuth認証後、email入力を求める処理のため。 | |
| match '/users/:id/finish_signup' => 'users#finish_signup', via: [:get, :patch], as: :finish_signup | |
| ... | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # == Schema Information | |
| # | |
| # Table name: users | |
| # | |
| # id :integer not null, primary key | |
| # email :string default(""), not null | |
| # encrypted_password :string default(""), not null | |
| # reset_password_token :string | |
| # reset_password_sent_at :datetime | |
| # remember_created_at :datetime | |
| # sign_in_count :integer default(0), not null | |
| # current_sign_in_at :datetime | |
| # last_sign_in_at :datetime | |
| # current_sign_in_ip :inet | |
| # last_sign_in_ip :inet | |
| # confirmation_token :string | |
| # confirmed_at :datetime | |
| # confirmation_sent_at :datetime | |
| # unconfirmed_email :string | |
| # created_at :datetime not null | |
| # updated_at :datetime not null | |
| # username :string | |
| # | |
| class User < ActiveRecord::Base | |
| #... | |
| has_many :social_profiles, dependent: :destroy | |
| # deviseモジュールの設定 | |
| devise :database_authenticatable, :registerable, :recoverable, :rememberable, | |
| :trackable, :validatable, :confirmable, :omniauthable | |
| #... | |
| TEMP_EMAIL_PREFIX = 'change@me' | |
| TEMP_EMAIL_REGEX = /\Achange@me/ | |
| # emailの登録状況を判定するカスタムvalidatorを使用するためのおまじない。 | |
| validates :email, presence: true, email: true | |
| def social_profile(provider) | |
| social_profiles.select{ |sp| sp.provider == provider.to_s }.first | |
| end | |
| # 本物のemailがセットされているか確認。 | |
| def email_verified? | |
| self.email && self.email !~ TEMP_EMAIL_REGEX | |
| end | |
| # email確認がされていない状態にする。 | |
| def reset_confirmation! | |
| self.update_column(:confirmed_at, nil) | |
| end | |
| # Userモデル経由でcurrent_userを参照できるようにする。 | |
| def self.current_user=(user) | |
| # Set current user in Thread. | |
| Thread.current[:current_user] = user | |
| end | |
| # Userモデル経由でcurrent_userを参照する。 | |
| def self.current_user | |
| # Get current user from Thread. | |
| Thread.current[:current_user] | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class UsersController < ApplicationController | |
| before_action :authenticate_user!, except: :finish_signup | |
| ... | |
| # OAuth認証による新規登録の締めを司るアクション。 | |
| # ユーザーデータを更新に成功したら、email確認メールを送付する。 | |
| # GET /users/:id/finish_signup - 必要データの入力を求める。 | |
| # PATCH /users/:id/finish_signup - ユーザーデータを更新。 | |
| def finish_signup | |
| @user = User.find(params[:id]) | |
| if request.patch? && @user.update(user_params) | |
| @user.send_confirmation_instructions unless @user.confirmed? | |
| flash[:info] = 'We sent you a confirmation email. Please find a confirmation link.' | |
| redirect_to root_url | |
| end | |
| end | |
| ... | |
| private | |
| # user_paramsにアクセスするため。 | |
| def user_params | |
| accessible = [ :username, :email ] | |
| accessible << [ :password, :password_confirmation ] unless params[:user][:password].blank? | |
| params.require(:user).permit(accessible) | |
| end | |
| ... | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| require 'mail' | |
| class EmailValidator < ActiveModel::EachValidator | |
| def validate_each(record,attribute,value) | |
| begin | |
| m = Mail::Address.new(value) | |
| # We must check that value contains a domain, the domain has at least | |
| # one '.' and that value is an email address | |
| r = m.domain!=nil && m.domain.match('\.') && m.address == value | |
| rescue Exception => e | |
| r = false | |
| end | |
| record.errors[attribute] << (options[:message] || "is invalid") unless r | |
| # 仮emailから変更しないとエラーになるようにする。 | |
| record.errors[attribute] << 'must be given. Please give us a real one!!!' unless value !~ User::TEMP_EMAIL_REGEX | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment