RailsにDevise+OmniAuthでユーザ認証したい

# 今回やりたいこと

Deviseを使った基本的なユーザー認証機能

SNS認証には仮登録メールを介さずにワンクリック登録となるようにしたい

Deviseの初期設定でユーザー情報の編集をする際に逐一パスワードを求められるが、ユーザーフレンドリーではないのでパスワードを入力せずにユーザー情報を編集したい


# この記事の個人的目的
備忘録。GitHubにこのプロジェクトのソースコードを残しておきます。

https://github.com/zizynonno/devise_omniauth


# 1 deviseの導入
### 1.1 プロジェクトの作成
新しいプロジェクトを作ります。

```bash
$ rails new devise_omniauth
$ cd devise_omniauth
```

### 1.2 Gemfileの追加とインストール
Gemfileに以下のgemを追加する。

```rb:Gemfile
source 'https://rubygems.org'

(省略)...

# Devise
gem 'devise'
gem 'devise-i18n'
gem 'omniauth-twitter'
gem 'omniauth-facebook'
gem 'dotenv-rails'

```

```rb:Gemfile
gem 'devise' #ユーザー認証
gem 'devise-i18n' #deviseのi18n
gem 'omniauth-twitter' #twitter認証
gem 'omniauth-facebook' #facebook認証
gem 'dotenv-rails' #環境変数の設定
```

gemをインストール。

```bash
$ bundle install
```
# 2 deviseの設定
devise関連ファイルを追加。

```bash
$ rails g devise:install
```
このコマンドを実行すると、ターミナルに英文でdeviseの設定について記載されています。
それでは1〜4まで実行していきましょう。

**2.1 デフォルトURLの指定**

```rb:config/environments/development.rb
Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  (省略)...

  # mailer setting
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
end
```

**2.2 root_urlの指定**
1番で指定した*http://localhost:3000/*にアクセスした際に表示されるページを指定します。
このプロジェクトではページを1つも作っていないため、先に追加します。

Pagesコントローラーと、indexページとshowページを追加してみます。

```bash
$ rails g controller Pages index show
```

routes.rbに以下を指定します。

```rb:config/routes.rb
Rails.application.routes.draw do
  root 'pages#index'
  get 'pages/show'
  (省略)...
end
```

**2.3 フラッシュメッセージの追加**
ログインした時などに上の方に「ログインしました」みたいなメッセージが出るようにします。
以下のファイルの`<body>`タグのすぐ下に指定されたタグを挿入します。

```erb:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html> 
 <head>
  <title>DeviseRails5</title>
  <%= csrf_meta_tags %>

  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
  <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
 </head>
 <body>
  <%# ここから %>
  <p class="notice"><%= notice %></p>
  <p class="alert"><%= alert %></p>
  <%# ここまで %>
                                                                                                                
  <%= yield %>

 </body> 
</html>
```

**2.4 DeviseのViewを生成**

```bash
$ rails g devise:views
```

すると以下の様なファイルが生成されます。

```bash
app/views/devise/shared/_links.html.erb (リンク用パーシャル)
app/views/devise/confirmations/new.html.erb (認証メールの再送信画面)
app/views/devise/passwords/edit.html.erb (パスワード変更画面)
app/views/devise/passwords/new.html.erb (パスワードを忘れた際、メールを送る画面)
app/views/devise/registrations/edit.html.erb (ユーザー情報変更画面)
app/views/devise/registrations/new.html.erb (ユーザー登録画面)
app/views/devise/sessions/new.html.erb (ログイン画面)
app/views/devise/unlocks/new.html.erb (ロック解除メール再送信画面)
app/views/devise/mailer/confirmation_instructions.html.erb (メール用アカウント認証文)
app/views/devise/mailer/password_change.html.erb （メール用パスワード変更完了文）
app/views/devise/mailer/reset_password_instructions.html.erb (メール用パスワードリセット文)
app/views/devise/mailer/unlock_instructions.html.erb (メール用ロック解除文)
```
# 3 Userモデルの設定
### 3.1 Userモデルの作成

```bash
$ rails g devise User
```

を実行するとmigrationファイルとuserファイルが出来上がります。

```rb:db/migrate/20200912194315_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  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.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


      t.timestamps null: false
    end
  end
end
```

```rb:app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end
```
### 3.2 マイグレーションファイル、Userモデルの編集
これを使うものだけコメントアウトしていきます。

```rb:db/migrate/20200912194315_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  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.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


      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

```

マイグレーションファイルで入れるものに加え、OAuth認証をするので`omniauth_providers: [:twitter,:facebook]`を追加します。

```rb:app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable, :timeoutable, :omniauthable, omniauth_providers: [:twitter,:facebook]
end
```

### 3.3 omniauth用カラムの追加
ついでにomniauth-twitter,omniauth-facebookで使う`provider`、`uid`、`username`をUserテーブルに追加します。

```bash
$ rails g migration add_columns_to_users provider uid username
```

以下のようなマイグレーションファイルができます。

```rb:db/migrate/20200912194427_add_columns_to_users.rb
class AddColumnsToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :provider, :string
    add_column :users, :uid, :string
    add_column :users, :username, :string
  end
end

```

ここまで出来たら以下を実行します。

```bash
$ rake db:migrate
```



# 4 Twitter,facebookで認証する
### 4.1 Twitter認証,Facebook認証をするためのそれぞれのAPIキー、シークレットキーを取得する

#### Facebook

以下よりアプリケーションを作成する。

- [Facebook Developers](https://developers.facebook.com/)

作成が完了したら、設定より「Add Platform」→「Website」を選択する。
サイトURLにURLを入力する（例：`http://localhost:3000`）。

#### Twitter

以下よりアプリケーションを作成する。

- [Twitter Application Management](https://apps.twitter.com/)

作成が完了したら、「Settings」より以下の設定を行なう。

1. Callback URL
  - 例：`http://〜/users/auth/twitter`
2. 以下にチェックを入れる：
  - Allow this application to be used to Sign in with Twitter



### 4.2 設定ファイルの編集
それぞれのAPIキー、シークレットキーを以下の該当箇所にコピーして貼り付けます。

```rb:config/initializers/devise.rb
Devise.setup do |config|
  # The secret key used by Devise. Devise uses this key to generate
  (省略)...
  config.omniauth :facebook, 'App IDを入力', 'App Secretを入力' #すぐに訂正します
  config.omniauth :twitter, 'API keyを入力', 'API secretを入力' #すぐに訂正するのでGitHubにコミットしないでください
end
```



### 4.3 Userコントローラにコールバック処理を実装

`provider`と同じ名前のメソッドを定義する必要がある。
ただ、基本的に各プロバイダでのコールバック処理は共通しているので、`callback_from`メソッドに統一している。

```
$ rails generate devise:controllers users
```

```ruby:app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    callback_from :facebook
  end

  def twitter
    callback_from :twitter
  end

  private

  def callback_from(provider)
    provider = provider.to_s

    @user = User.find_for_oauth(request.env['omniauth.auth'])

    if @user.persisted?
      flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize)
      sign_in_and_redirect @user, event: :authentication
    else
      session["devise.#{provider}_data"] = request.env['omniauth.auth']
      redirect_to new_user_registration_url
    end
  end
end
```

### 4.4 ルーティング処理

以下のように、OAuthのコールバック用のルーティングを設定する。

```ruby:config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }

  # ...
end
```

# 5 APIキー,シークレットキーの非公開
Twitter認証,Facebook認証をするためのそれぞれのAPIキー、シークレットキーを取得しました、この情報は非常に機密性が高く決して外部に漏らしてはいけない情報です。悪用される恐れがあるため、GitHubのリモートリポジトリや本番環境でAPIキー,シークレットキーが間違って一般公開されないような処理を施す必要があります。

最初の方で環境変数を設定するための`gem 'dotenv-rails'`を`bundle install`しているので、`dotenv-rails`を使ったAPIキー、シークレットキーの方法を学んでいきます。

**5.1 .envファイルの設置**
次に、環境変数を定義する.envファイルをアプリの**プロジェクトルート直下**に設置します。
.envファイルは`dotenv-rails`を`bundle install`しても自動生成されない為、下記の様にtouchコマンドを使って手動でファイルを作成する必要があります。

```bash
$ touch .env 
```

**5.2 .envファイルに以下を記載する**

```.env
TWITTER_API_KEY="取得したTwitterAPIキー"
TWITTER_SECRET_KEY="取得したTwitterシークレットキー"
FACEBOOK_API_ID="取得したFacebookAPIキー"
FACEBOOK_API_SECRET="取得したFacebookシークレットキー"
```

**5.3 環境変数を使う**
`ENV['SECRET_KEY']`のような記載方法で、ハードコーディングしているファイルに環境変数を代入していきます。

```rb:config/initializers/devise.rb
Devise.setup do |config|
  # The secret key used by Devise. Devise uses this key to generate
  (省略)...
  config.omniauth :twitter, ENV['TWITTER_API_KEY'], ENV['TWITTER_API_SECRET_KEY']
  config.omniauth :facebook, ENV['FACEBOOK_API_ID'], ENV['FACEBOOK_API_SECRET']
end
```


## 6 ユーザーモデルにメソッドを追加する
Userモデルに**self.from_omniauth**と**self.new_with_session**を作ります。
self.from_omniauthではuidとproviderで検索してあったらそれを、無かったらレコードを作ります。
self.new_with_sessionについては、もしこのメソッドを追加しておかなければ、Twitter認証後サインアップページで登録を行っても、認証情報として取ってきたuidやproviderなどが登録されません。それらが登録されないのでTwitterで認証しても登録されてないユーザーとして毎回サインアップページに飛ばされます。

```rb:app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable, :timeoutable, :omniauthable, omniauth_providers: [:twitter]

  def self.from_omniauth(auth)
    find_or_create_by(provider: auth["provider"], uid: auth["uid"]) do |user|
      user.provider = auth["provider"]
      user.uid = auth["uid"]
      user.username = auth["info"]["nickname"]
    end
  end

  def self.new_with_session(params, session)
    if session["devise.user_attributes"]
      new(session["devise.user_attributes"]) do |user|
        user.attributes = params
      end
    else
      super
    end
  end
end
```

### 6.1 Userモデルにfindメソッドを実装

`uid`と`provider`の組み合わせは一意であり、これによりユーザを取得する。
レコードに存在しない場合は作成する。

```ruby:app/models/user.rb
class User < ActiveRecord::Base
  # ...

  def self.find_for_oauth(auth)
    user = User.where(uid: auth.uid, provider: auth.provider).first

    unless user
      user = User.create(
        uid:      auth.uid,
        provider: auth.provider,
        email:    User.dummy_email(auth),
        password: Devise.friendly_token[0, 20]
      )
    end
    user.skip_confirmation! #仮登録メールを介さずに即時登録
    user
  end

  private

  def self.dummy_email(auth)
    "#{auth.uid}-#{auth.provider}@example.com"
  end
end
```

メールアドレスでの認証も実装している場合、OAuthでの認証時もメールアドレスを保存する必要がある。
ここでは、`uid`と`provider`の組み合わせが一意なことを利用して、`self.dummy_email`のように生成している。

以下ファイルを編集して、コールバック用のコントローラーとしてさっき作ったコントローラーが呼ばれるようにします。これを書かないとdevise側のコントローラーが呼ばれます。

```rb:config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
  root 'pages#index'
  get 'pages/show'
  (省略)...
end
```

これでTwitter認証ができるようになりました。
初回、Twitter認証を行うと、サインアップページに飛ばされ、そこでメールアドレスやパスワードを入力して登録するとユーザー情報が登録されます。
今回はcomfirmable機能を入れているので、登録したら確認メッセージを送ったとのメッセージが出て、そのままログインすることはできません。
この機能を入れてなかった場合、登録すると即ログインします。

# 7 SNS認証には仮登録メールを介さずに即時登録となるようにしたい
```app/model/user.rb
class User < ActiveRecord::Base
  # ...

  def self.find_for_oauth(auth)
    user = User.where(uid: auth.uid, provider: auth.provider).first

    unless user
      user = User.create(
        uid:      auth.uid,
        provider: auth.provider,
        email:    User.dummy_email(auth),
        password: Devise.friendly_token[0, 20]
      )
    end
######これを追記！######
user.skip_confirmation!
#######################
    user
  end

  private

  def self.dummy_email(auth)
    "#{auth.uid}-#{auth.provider}@example.com"
  end
end
```

# 8 ユーザー情報の編集で逐一パスワードを求められるのがだるい
## 8.1 routes.rbを修正する

```ruby:routes.rb
devise_for :users, controllers: {  }

Rails.application.routes.draw do
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks', 
                                         registrations: 'users/registrations' }
  root 'pages#index'
  get 'pages/show'
  (省略)...
end
```

## 8.2 update_resourceメソッドをオーバーライドする

```ruby:registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController

  protected
  # 追記する
  def update_resource(resource, params)
    resource.update_without_password(params)
  end
end
```

## 8.3 current_passwordフォームを削除する

```erb:views/devise/registrations/edit.html.erb
<div class="field">
    <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
    <%= f.password_field :current_password, autocomplete: "current-password" %>
</div>
```
こちらのフォームを削除しましょう。


これで、パスワードを入力しなくてもユーザーの登録情報を
編集することが可能になりました！


# 9 やりたいことの文献一覧
**基本的なユーザー認証機能に加え、SNS認証でワンクリック登録できる仕組みの構築。**
[[*Rails*] deviseの使い方（rails5版）](https://qiita.com/cigalecigales/items/f4274088f20832252374)
[RailsにDevise+OmniAuthでユーザ認証を実装する手順](https://qiita.com/zenizh/items/94aec2d94a2b4e9a1d0b)

**環境変数を使用し、APIキーを隠してリモートにpushしたい(App Secretなどをハードコーディングしているのはよろしくない)**
[【Rails】dotenv-railsの導入方法と使い方を理解して環境変数を管理しよう！](https://pikawaka.com/rails/dotenv-rails)
[環境変数の設定](https://golikyua.hatenablog.com/entry/2020/01/09/135254)

**ダミーではなく、twitterやfacebookに登録されたemailをDBを持っていきたい(3日くらいかかる)**
[omniauth-twitterでemail情報を取得する](https://qiita.com/shitake/items/fd592bce7d0be8fadd47)
[twitterのoauthを使ってみる（emailも取得）](https://qiita.com/kite_999/items/e0ab8c52f918bbb02cfd)
[facebookのoauthを使ってみる（emailも取得）](https://qiita.com/kite_999/items/0c0336e740072570ccce)
[2015年7月9日以降にFacebook認証でメールアドレスが取れない問題とその対策
](https://paiza.hatenablog.com/entry/2015/07/22/2015%E5%B9%B47%E6%9C%889%E6%97%A5%E4%BB%A5%E9%99%8D%E3%81%ABFacebook%E8%AA%8D%E8%A8%BC%E3%81%A7%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%81%8C%E5%8F%96%E3%82%8C%E3%81%AA%E3%81%84)


**TwitterやGoogle,Githubなどの外部サイトを用いた認証には仮登録メールを介さずに即時登録となるようにしたい**
[Devise内でomniauthのtwitter認証が完了してもレコードが格納されない](https://teratail.com/questions/66716)
[deviseのTwitterログイン時はメール認証とメール送信をスキップする](https://qiita.com/tegnike/items/a655c5ee7f71fb5bef08)
[【Rails5】SNS認証でメールアドレス介さず登録・ログインできるようにする実装](https://4-bh.com/twitter_api_rails5/)

**deviseをi18nで日本語にしたい**
[i18nで日本語化](https://bbbbruno.qrunch.io/entries/wcNZqbMwA5SRBqQj)

**ユーザー情報の編集で逐一パスワードを求められるのがだるい**
[[Devise] パスワードを入力せずにユーザー情報を編集する](https://qiita.com/machamp/items/f6a7b003fcda3f04094a)

**メールアドレスのみでユーザー登録を行う。**
[devise でメールアドレスのみでユーザー登録を行い、パスワードを後から設定する方法](https://qiita.com/necojackarc/items/bd3286064ed3cd40db6b#comment-e19f7010687d2bf6bf50)
[How To: Email only sign up](https://github.com/heartcombo/devise/wiki/How-To:-Email-only-sign-up)

**サインインする際にメールアドレス以外でサインインする方法**
[How To: Allow users to sign in with something other than their email address
](https://github.com/heartcombo/devise/wiki/How-To:-Allow-users-to-sign-in-with-something-other-than-their-email-address)



**メールアドレスのアップデートをする際に確認を必要としない方法(スキップしたい)**
[deviseでメールアドレスのアップデートを確認する必要はありませんか？](https://stackoverrun.com/ja/q/2568109)
[Deviseでメールアドレスの確認をスキップする](http://319ring.net/blog/archives/2446/)



**その他**
[【Rails】deviseのTwitter認証で「Unauthorized 403 Forbidden」が出てしまう場合の対処法](https://saruwakakun.com/memo/omniauth-twitter)
[deviseのドキュメント(英語)](https://github.com/heartcombo/devise/wiki)