deviseとOmniAuthをRailsアプリケーションに導入しテストする 

Ruby on Rails のアプリケーションに、 devise を利用して認証機能を実装し、更に Google アカウントに連携した認証機能とテストコードを実装します。よくありがちなユーザー認証機能をdeviseを使いサクッと実装し、Googleを使ったSNS認証機能とテストコードを実装する方法について解説していきます。

## 動作環境
- PostgreSQL 12.1
- macOS Catalina
- Ruby 2.7.0
- Rails 6.0.7.2

## 全体の概要
本記事では、3つのステップで進めていきます。
1. devise でサインイン機能を実装する
2. OmniAuth で Google アカウント機能を実装する
3. Omniauth のテストコードを実装する

## 1. deviseでサインイン機能を実装する
まずは、devise でサインイン機能を実装し、deviseとはどのような機能を持つGem(ライブラリ)なのかを掴みましょう。

本項は下記を参考にしました。

- [Railsでユーザー認証機能を実装しよう～定番のgem「devise」活用法（1） (1/4)：CodeZine（コードジン）](https://codezine.jp/article/detail/10813)
- [Railsでユーザー認証機能を実装しよう～定番のgem「devise」活用法（2） (1/4)：CodeZine（コードジン）](https://codezine.jp/article/detail/10899)
- [[*Rails*] deviseの使い方（rails5版） - Qiita](https://qiita.com/cigalecigales/items/f4274088f20832252374)
- [GitHub - heartcombo/devise: Flexible authentication solution for Rails with Warden.](https://github.com/heartcombo/devise)

### deviseとは
まずはじめに、deviseについて解説します。deviseとはRailsアプリケーションに柔軟な認証機能を追加するためのライブラリです。
アプリケーションコードをほとんど書かなくても、認証に必要な機能を追加でき、とても便利です。

deviseを理解するには、下記の点を抑えておくと良いでしょう。

- Rails エンジンの仕組みを利用している
- 認証機能毎にモジュールがある
- 導入するだけで利用できるメソッドがある


#### Railsエンジンとは
deviseはRailsエンジンの仕組みを使ったGemだと説明しましたが、Railsエンジンとは何でしょうか？

> エンジン (engine) は、ホストとなるRailsアプリケーションに機能を提供するミニチュア版Railsアプリケーションとみなせます。この場合、ホストとなるRailsアプリケーションは、実際にはエンジンに「ターボをかけた」ようなものにすぎず、Rails::ApplicationクラスはRails::Engineから多くの振る舞いを継承します。
すなわち、エンジンとアプリケーションは、細かな違いを除けばほぼ同じものであると考えていただいてよいでしょう。 
とりあえずは Rails エンジンが、`基本的な構成が Railsのアプリケーションと同様`、ということをとりあえずは抑えておけばよいでしょう。

#### モジュール一覧
deviseは認証機能毎に複数のモジュールに分かれています。認証と言ってもひつような機能はさまざまで、「ユーザーを新規に登録する」や「パスワードを忘れたらリセットする」、「メールアドレスにバリデーションを追加する」などの機能によって、モジュールがそれぞれ用意されているのです。モジュールの一覧は下記なので、ざっと目を通しておく程度で大丈夫です。

- Database Authenticatable
  - ユーザー情報をハッシュにして、データベースに保存する
- Omniauthable
  - Omniauth を使い外部サービスと連携したログインさせる
- Confirmable
  - サインイン時にemailを送信して、アカウントがemailによる認証済みかを検証する
- Recoverable
  - パスワードをリセットし、リセット用のURLを発行する
- Registable
  - ユーザーが自身の情報の登録・編集・削除を可能にする
- Rememberable
  - 保存されたCookie(PCに保存されるログイン情報のこと)から、リメンバーミー(ブラウザを閉じてもログインが有効)を可能にする
- Trackable
  - サインインの回数やサインインのタイムスタンプ、IPアドレスなどの情報を保存する
- Timeoutable
  - ログインから一定期間たったユーザーをタイムアウトさせる
- Validatable
  - メールアドレスとパスワードのバリデーションを追加する
- Lockable
  - 指定した回数ログインを失敗するとロックする

#### devise で使えるメソッド
deviseは導入するだけで使えるヘルパーメソッドがいくつか用意されています。
例えば、`user_singed_in?`メソッドなどは、地道に実装しようとするとなかなかのコード行数になりますので、あらかじめ準備されていてとても便利です。

用意されているメソッドは、下記があります。

|メソッド|用途|
|---|---|
|before_action :authenticate_user!|ユーザーが認証(ログイン済み)の場合のフィルター|
|user_signed_in?|ユーザーがサインインしているかをの真偽値を返す|
|current_user|サインインしているユーザーを取得する|
|user_session|ユーザーのセッション情報にアクセスできる|

上記は User モデルの場合を記していますが、Memberモデルの場合は、下記のようになります。

```
before_action :authenticate_member!

member_signed_in?

current_member

member_session
```


### アプリケーションを新規作成する
データベースに PostgreSQL を指定して、アプリケーションを作成します。作成できたら、データベースを作成しましょう。
```
$ mkdir devise
$ cd devise
$ rails new . -d postgresql
$ rails db:create
```

### devise のセットアップをする
#### devise を追加する
次に、`Gemfile`にdeviseを追加します。
devise は開発環境、テスト環境、プロダクション環境すべての環境で利用するので、`Gemfile`の一番下に追加します。

```ruby:Gemfile
gem 'devise'
```

Gemfile に追加したら下記のコマンドを実行しインストールします。

```
$ bundle install
```

#### devise をインストールする
次に、deviseの機能をRailsアプリケーションにインストールします。

この、インストールしに生成された2つのファイルと、セットアップ用のコメントは非常に重要なため、このあと詳しく見ていきます。

```
$ rails g devise:install
```

```

      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================
```

#### 設定ファイルを確認する

`config/initializers/devise.rb`は devise の各機能ごとの設定を記載するためのファイルです

#### 翻訳ファイルを確認する
`config/locales/devise.en.yml` は devise で用いるアプリケーションのビューのテキスト表示を記載するためのファイルです

- [devise-i18n/ja.yml at master · tigrish/devise-i18n · GitHub](https://github.com/tigrish/devise-i18n/blob/master/rails/locales/ja.yml)


#### ActionMailerの設定を追加する
```
1. Ensure you have defined default url options in your environments files. Here
   is an example of default_url_options appropriate for a development environment
   in config/environments/development.rb:

     config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

   In production, :host should be set to the actual host of your application.
```

```ruby:config/environments/development.rb
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
```

#### ルーティングを追加する

```
2. Ensure you have defined root_url to *something* in your config/routes.rb.
   For example:

     root to: "home#index"
```

ルートのURLを定義するように言われているので、`config/routes.rb`に定義します。ここでは、Topsコントローラーのindexアクションを紐付けます。

```ruby:Gemfile
root to: 'top#index'
```

#### flashメッセージを追加する
```
3. Ensure you have flash messages in app/views/layouts/application.html.erb.
   For example:

     <p class="notice"><%= notice %></p>
     <p class="alert"><%= alert %></p>
```

```ruby:app/views/layouts/application.html.erb
<body>
  <p class="notice"><%= notice %></p> # 追記
  <p class="alert"><%= alert %></p> # 追記
  <%= yield %>
</body>
```

#### ビューファイルを作成する

```
4. You can copy Devise views (for customization) to your app by running:

     rails g devise:views
```

devise に用意されたgenerateコマンドを利用して、viewを作成します。

```
$ bin/rails g devise:views
```

作成されたviewを見るとおわかりかと思いますが、認証に必要な一通りのviewが一度に作成できてしまいます。素晴らしい。

```
Running via Spring preloader in process 72079
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_error_messages.html.erb
      create    app/views/devise/shared/_links.html.erb
      invoke  form_for
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb
      invoke  erb
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/email_changed.html.erb
      create    app/views/devise/mailer/password_change.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb
```
### User モデルを作成する
```
$ rails g devise User
```

### マイグレーションファイルを確認、実行する

マイグレーションファイルには、deviseの各機能(module)ごとにカラムが記載されています。デフォルト設定のものはコメントアウトが外されており、デフォルトにない設定のものはコメントアウトされています

```
# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[6.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
```
マイグレーションファイルの確認が終わったら、マイグレーションを実行します

```
$ rails db:migrate
```

### ルーティングを確認する

先程追加したルートのルーティングに加え、deviseのルーティングが追加されています。具体的にどのようなURLが発行されているのか確認しましょう。

```ruby:config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'top#index'
end
```

```
$ rails routes
```

```
                               Prefix Verb   URI Pattern                                                                              Controller#Action
                     new_user_session GET    /users/sign_in(.:format)                                                                 devise/sessions#new
                         user_session POST   /users/sign_in(.:format)                                                                 devise/sessions#create
                 destroy_user_session DELETE /users/sign_out(.:format)                                                                devise/sessions#destroy
                    new_user_password GET    /users/password/new(.:format)                                                            devise/passwords#new
                   edit_user_password GET    /users/password/edit(.:format)                                                           devise/passwords#edit
                        user_password PATCH  /users/password(.:format)                                                                devise/passwords#update
                                      PUT    /users/password(.:format)                                                                devise/passwords#update
                                      POST   /users/password(.:format)                                                                devise/passwords#create
             cancel_user_registration GET    /users/cancel(.:format)                                                                  devise/registrations#cancel
                new_user_registration GET    /users/sign_up(.:format)                                                                 devise/registrations#new
               edit_user_registration GET    /users/edit(.:format)                                                                    devise/registrations#edit
                    user_registration PATCH  /users(.:format)                                                                         devise/registrations#update
                                      PUT    /users(.:format)                                                                         devise/registrations#update
                                      DELETE /users(.:format)                                                                         devise/registrations#destroy
                                      POST   /users(.:format)                                                                         devise/registrations#create
                                 root GET    /                                                                                        top#index
        rails_mandrill_inbound_emails POST   /rails/action_mailbox/mandrill/inbound_emails(.:format)                                  action_mailbox/ingresses/mandrill/inbound_emails#create
        rails_postmark_inbound_emails POST   /rails/action_mailbox/postmark/inbound_emails(.:format)                                  action_mailbox/ingresses/postmark/inbound_emails#create
           rails_relay_inbound_emails POST   /rails/action_mailbox/relay/inbound_emails(.:format)                                     action_mailbox/ingresses/relay/inbound_emails#create
        rails_sendgrid_inbound_emails POST   /rails/action_mailbox/sendgrid/inbound_emails(.:format)                                  action_mailbox/ingresses/sendgrid/inbound_emails#create
         rails_mailgun_inbound_emails POST   /rails/action_mailbox/mailgun/inbound_emails/mime(.:format)                              action_mailbox/ingresses/mailgun/inbound_emails#create
       rails_conductor_inbound_emails GET    /rails/conductor/action_mailbox/inbound_emails(.:format)                                 rails/conductor/action_mailbox/inbound_emails#index
                                      POST   /rails/conductor/action_mailbox/inbound_emails(.:format)                                 rails/conductor/action_mailbox/inbound_emails#create
    new_rails_conductor_inbound_email GET    /rails/conductor/action_mailbox/inbound_emails/new(.:format)                             rails/conductor/action_mailbox/inbound_emails#new
   edit_rails_conductor_inbound_email GET    /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format)                        rails/conductor/action_mailbox/inbound_emails#edit
        rails_conductor_inbound_email GET    /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                             rails/conductor/action_mailbox/inbound_emails#show
                                      PATCH  /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                             rails/conductor/action_mailbox/inbound_emails#update
                                      PUT    /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                             rails/conductor/action_mailbox/inbound_emails#update
                                      DELETE /rails/conductor/action_mailbox/inbound_emails/:id(.:format)                             rails/conductor/action_mailbox/inbound_emails#destroy
rails_conductor_inbound_email_reroute POST   /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format)                      rails/conductor/action_mailbox/reroutes#create
                   rails_service_blob GET    /rails/active_storage/blobs/:signed_id/*filename(.:format)                               active_storage/blobs#show
            rails_blob_representation GET    /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
                   rails_disk_service GET    /rails/active_storage/disk/:encoded_key/*filename(.:format)                              active_storage/disk#show
            update_rails_disk_service PUT    /rails/active_storage/disk/:encoded_token(.:format)                                      active_storage/disk#update
                 rails_direct_uploads POST   /rails/active_storage/direct_uploads(.:format)                                           active_storage/direct_uploads#create

```

### 動作確認をする
上記では、URLがたくさん生成されていることを確認しました。ここでは試しに、`sign_in` にアクセスし、devise の機能が有効になっているかを確認してみましょう。
`localhost:3000/sign_in`にアクセスしてください

下記のような画面が表示されていれば成功です。

<img width="357" alt="ScreenShot 2020-01-24 12.09.14.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/6f2f86af-99ae-c107-f7ec-b2cff6eb42be.jpeg">

### トップページを作成する
トップページ用のコントローラーを追加します。

```
$ rails g controller top index
```

早速、`before_action :authenticate_user!|`を追加しましょう。

```ruby:app/controllers/top_controller.rb
class TopController < ApplicationController
  before_action :authenticate_user!, only: %i[index]
  def index; end
end
```

トップページである`localhost:3000`にアクセスしようとすると、`localhost:3000/users/sign_in`にリダイレクトされていれば大丈夫です。

`Sign_up`からユーザー情報を登録し、ログインできるようになっているはずです。

## 2. OmniAuth で Google アカウント機能を実装する
さて、ここからは SNS認証機能を追加していきます。今回は、Googleアカウントを利用しましょう。

本項は、下記を参考にしました。

- [RailsでSNS認証機能を実装しよう～定番gem「OmniAuth」活用法 (1/3)：CodeZine（コードジン）](https://codezine.jp/article/detail/10970)
- [GitHub - omniauth/omniauth: OmniAuth is a flexible authentication system utilizing Rack middleware.](https://github.com/omniauth/omniauth)
- [Railsでomniauth(google/facebook/)実装方法 - Qiita](https://qiita.com/hidepino/items/a1eb9d2f32ce33389f20)

### OmniAuth とは
OmniAuthとは、外部サービスのアカウントを使った認証機能を提供する仕組みです。各外部サービスとのOauth認証機能をStragetyとして持っています。

Omniauthの仕組みを知る上では、下記の点を抑えておくと良いでしょう。

- Oauth2 による認証を提供している
- Rack ミドルウェアを利用している
- Strategy を管理している

#### Oauth2とは
> アクセストークンの要求方法とそれに対する応答方法を標準化したものが OAuth 2.0 である

- 参考: [一番分かりやすい OAuth の説明 - Qiita](https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be)

#### Rackとは
> Rackは、RubyのWebアプリケーションに対して、モジュール化された最小限のインターフェイスを提供して、インターフェイスを広範囲に使えるようにします。RackはHTTPリクエストとレスポンスを可能なかぎり簡単な方法でラッピングすることで、Webサーバー、Webフレームワーク、その間に位置するソフトウェア (ミドルウェアと呼ばれています) のAPIを1つのメソッド呼び出しの形にまとめます。
- 参考: [Rails と Rack - Railsガイド](https://railsguides.jp/rails_on_rack.html)

Rackとは、HTTP通信に利用されているミドルウェアのことを言います。

#### Strategyとは
Strategyとは具体的な外部サービス(Probider)を指します。具体的なサービスとの関連付けは下記のようになっており、Strategyと同名のGemが提供されています。

|Probider|Strategy|
|---|---|
|Google|google-oauth2|
|Twitter|twitter|
|FaceBook|facebook|


### CLIENT_ID と CLIENT_SECRET の取得
今回は、Google APIを利用します。

[Google Cloud Platform](https://console.cloud.google.com/apis/dashboard) にアクセスし、`My Project▼`をクリックします。

![ScreenShot 2020-01-25 10.10.32.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/5fbda306-2ddf-71f1-5eeb-ae41b14da36b.jpeg)


`新しいプロジェクト`をクリックします。
![ScreenShot 2020-01-25 10.12.09.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/99abdf1b-a945-2ad9-e4d3-2ed545ad7705.jpeg)

任意のプロジェクト名ここでは`devise-omniauth`と入力し、作成をクリック。
![ScreenShot 2020-01-25 10.12.44.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/6a449033-9529-0d34-009a-f2d1d982c10b.jpeg)



`認証情報を作成`をクリックします。
![ScreenShot 2020-01-25 10.13.46.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/02a3639d-f800-0cb4-42e6-2cdba07152f1.jpeg)


OauthクライアントID をクリックします。なお、OAuthクライアントIDとは、ユーザーの情報にアクセスするためのサービス提供者に割り当てられた一意のIDのことです。
![ScreenShot 2020-01-25 10.16.44.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/abdd241e-e91f-0fe4-7ba9-116ceb50a25d.jpeg)


Userのタイプを選択します。組織内のみで利用する場合は、`内部`を、Googleアカウントを持つすべてのユーザーが利用できるようにする場合は、`外部`を選択します。
![ScreenShot 2020-01-25 10.17.31.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/9eaff7d8-73d4-e94d-7533-31e192df1853.jpeg)


任意のアプリケーション名を記載し、下までスクロールして`保存`をクリックします。

![ScreenShot 2020-01-25 10.18.42.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/794c7c8e-4105-0c2e-a8a6-5b84c3f62e2b.jpeg)

![ScreenShot 2020-01-25 10.18.55.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/033e3ec5-74b0-00fb-6eb0-cc25764f736d.jpeg)

OAuth クライアントID作成の画面では、任意の名前を入力し、`承認済みリダイレクトURL(コールバックURL)`に下記を記載します。間違えないよう、コピペすることをおすすめします。
なお、コールバックURLとは、認証後に外部サービス(ここでは、Google)認証ページからリダイレクトされる自身のアプリケーションのURLを指しています。

```
http://localhost:3000/users/auth/google_oauth2/callback
```

![ScreenShot 2020-01-25 10.20.19.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/c9f37e00-571f-2019-d418-abeaaefcb5e0.jpeg)

すると、クライアントIDとクライアントシークレットが発行されますので、テキストファイルなどにして保存してください。一度だけしか発行されないため、取り扱いには厳重注意をしてください。

![ScreenShot 2020-01-25 10.21.38.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/2ea68761-c9f3-8271-04a7-a2d48695cf97.jpeg)


なお、Googleの`CLIENT_ID`と`CLIENT_SECRET`の取得に関する情報を探すと、`ライブラリ`画面から`Google+API`を利用しているものがありますが、`Google+APIはすでにサービスの提供が終了しており`2020年1月現在だと、ユーザー名やメールアドレスを取得するにあたりは、APIの追加が不要になっていることを確認しています。

### omniauth の実装
Gemfile に下記を追加します。

```ruby:Gemfile
gem 'omniauth'
gem 'omniauth-google-oauth2'
gem 'dotenv-rails'
```

```
$ bundle install
```

### 設定ファイルを編集する
`config/initializers/devise.rb`に下記を追加する。

```ruby:config/initializers/devise.rb
config.omniauth :google_oauth2, ENV['CLIENT_ID'], ENV['CLIENT_SECRET']
```

`.env`ファイルを作成し、先程取得したクライアントIDとクライアントシークレットを入力します。
`=`のあとは、`クォーテーション''`不要です。

```ruby:.env
CLIENT_ID=クライアントID
CLIENT_SECRET=クライアントシークレット
```

### コントローラーを追加する
devise を用いた users コントローラーを追加します。

```
$ rails g devise:controllers users
Running via Spring preloader in process 90155
      create  app/controllers/users/confirmations_controller.rb
      create  app/controllers/users/passwords_controller.rb
      create  app/controllers/users/registrations_controller.rb
      create  app/controllers/users/sessions_controller.rb
      create  app/controllers/users/unlocks_controller.rb
      create  app/controllers/users/omniauth_callbacks_controller.rb
===============================================================================

Some setup you must do manually if you haven't yet:

  Ensure you have overridden routes for generated controllers in your routes.rb.
  For example:

    Rails.application.routes.draw do
      devise_for :users, controllers: {
        sessions: 'users/sessions'
      }
    end

===============================================================================
```


### Users テーブルにカラムを追加する
マイグレーションファイルを作成し、カラムを追加します。

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

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

ここで追加したカラムを簡単に説明します。

- uid ユーザーID
- provider サービス名称
- name ユーザーの名前

```
$ rails db:migrate
```

ちなみに、`email`などのカラムはすでにdevise 標準のため用意されています。マイグレーション実行後の`db/schema.rb`を見てみましょう。

```ruby:db/schema.rb
create_table "users", force: :cascade do |t|
  t.string "email", default: "", null: false
  t.string "encrypted_password", default: "", null: false
  t.string "reset_password_token"
  t.datetime "reset_password_sent_at"
  t.datetime "remember_created_at"
  t.datetime "created_at", precision: 6, null: false
  t.datetime "updated_at", precision: 6, null: false
  t.string "provider"
  t.string "uid"
  t.string "username"
  t.index ["email"], name: "index_users_on_email", unique: true
  t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
```

### コントローラーを追加する
コントローラーを追加します。
`Devise::OmniauthCallbacksController`を継承しているので、見慣れないメソッド名があるかと思いますが、`provider`である`google_oauth2`のメソッドが必要になります。
余談ですが、もし追加で`provider`を追加したときは、新たなメソッドとして、`twitter`を定義し、同じく`callback`メソッドを呼び出せると、メソッドの中身を共通化できますね。

`request.env['omniauth.auth']`に情報が格納されているため、この値を引数として既存のユーザーを探し出すか、新規作成します。この際、フラッシュメッセージは、`Successfully authenticated from Google_oauth2 account.`と表示されます。

また、ユーザー情報がデータベースに保存されているかどうかで、リダイレクト先を変更します。

```ruby:app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  # You should configure your model like this:
  # devise :omniauthable, omniauth_providers: [:twitter]

  def google_oauth2
    callback_from(:google_oauth2)
  end

  def callback_from(provider)
    provider = provider.to_s
    @user = User.find_or_create_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
    else
      session["devise.#{provider}_data"] = request.env['omniauth.auth']
      redirect_to new_user_registration_url
    end
  end
end

```

### Userモデルにメソッドを追加する

`request.env['omniauth']`の`email`からユーザー情報を取得し、新規ならば新たにレコードを作成します。

パスワードは、devise の特異メソッドである`friendly_token`メソッドを使いランダムに生成します。

```ruby: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_providers: [:google_oauth2]

  def self.find_or_create_for_oauth(auth)
    find_or_create_by!(email: auth.info.email) do |user|
      user.provider = auth.provider,
      user.uid = auth.uid,
      user.username = auth.info.name,
      user.email = auth.info.email,
      user.password = Devise.friendly_token[0, 20]
    end
  end
end
```

### ルーティングを追加する
`config/routes.rb`で、`Users::OmniauthCallbacksController`へのルーティングを追加します。

```ruby:config/routes.rb
Rails.application.routes.draw do
  get 'top/index'
  devise_for :users, controllers: {
    omniauth_callbacks: 'users/omniauth_callbacks'
  }
  root to: 'top#index'
end
```

### 動作確認
`Sign in with GoogleOauth2`のリンクが自動で生成されています。

これは、`app/views/devise/shared/_links.html.erb`に自動でリンクが追加されているためです。便利ですね。


### エラーの対処
外部通信を伴うエラー処理は切り分けが難しいですよね。いくつか対処した記録を残します。
#### Error: redirect_uri_mismatch
![ScreenShot 2020-01-25 11.54.36.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/a947de88-c793-8b7f-c28f-7cc0dee42cdb.jpeg)

`redirect_uri_mismatch`と出力されています。

リダイレクト先のURLを間違ってしまった場合は、下記の画面になります。

#### JWT::InvalidIatError

<img width="607" alt="ScreenShot 2020-01-25 18.59.27.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331354/e995d55c-b6ca-a539-eebc-b625a4af0372.jpeg">

`JWT::InvalidIatError`と出力されたら、OSのクロックがずれているエラーです。
`config/initializers/devise.rb` ファイルを見直し、`skip_jwt: true`を追記しましょう。
```
Devise.setup do |config|
  `config.omniauth :google_oauth2, ENV['CLIENT_ID'], ENV['CLIENT_SECRET'], skip_jwt: true` skip_jwt: true # skip_jwt: true を追記
end
```

- 参考: [JWT::InvalidIatError の対処法（Google oauth使用時） - Qiita](https://qiita.com/takuchan9104/items/1b588e125f8a1e0c7605)
#### The action 'google_oauth2' could not be found for Devise::OmniauthCallbacksController

`Devise::OmniauthCallbacksController`に対応する`google_oauth2`というアクションはないと言われています。
今回作成したのは、`Devise::OmniauthCallbacksController
`を継承したUsersコントローラーなので、`config/routes.rb`があっているか確認しましょう。

## 3. Omniauth のテストコードを実装する
Omniauthを使った認証機能のテストを追加していきます。

本項は、下記の記事を参考にしました。

- [Integration Testing · omniauth/omniauth Wiki · GitHub](https://github.com/omniauth/omniauth/wiki/Integration-Testing)

### Gemを追加する

`Gemfile`に`rspec-rails`を追加します。
```ruby:Gemfile
group :test do
  gem 'rspec-rails'
end
```

```
$ bundle install
```

```
$ bundle exec rails generate rspec:install
```

```
Running via Spring preloader in process 4995
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb
```

### 設定を追加する
`spec/rails_helper.rb`に`support`配下のRubyファイルを読み込む設定を追加します。
```Ruby:spec/rails_helper.rb`
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
```

### モックを追加する
`spec/support/omniauth.rb`を作成し、下記のコードを記載します。

```ruby:spec/support/omniauth.rb
OmniAuth.config.test_mode = true

OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new({
    provider: 'google_oauth2',
    uid: '000000',
    info: { email: 'test@example.com' },
    credentials: { token:'google_oauth2_test' }
  })
```
このコードをざっくり説明すると、下記のようになります。

- OmniAuth をテストモードを有効にしている。この設定を追加することで、すべてのOmniauthへのリクエストは、モックのAuth情報を用いて簡素化できるようになる
- `google_oauth2`のOmniAuthのモックとして疑似のAuth情報のハッシュを作成している


### テストコードを実装する
`spec/system/user.rb`を作成し、テストケースを追加します。
今回は、`未サインアップの場合`と`サインアップ済みの場合`をテストします。

```ruby:spec/system/user.rb
require 'rails_helper'

RSpec.describe 'User', type: :system do
  context '未サインアップの場合' do
    it 'ユーザーが増えること' do
      visit root_path
      expect{
        click_link 'Sign in with GoogleOauth2'
        sleep 1
      }.to change(User, :count).by(1)
    end
  end

  context 'サインアップ済みの場合' do
    before do
      User.create!(
      email: 'test@example.com',
      password: 'test12'
      )
    end

    it 'ユーザーは増えないこと' do
      visit root_path
      expect{
        click_link 'Sign in with GoogleOauth2'
        sleep 1
      }.to_not change(User, :count)
    end
  end
end

```

## まとめ
Railsアプリケーションに認証機能追加に関して、知見が深まりました。
拙い点がありましたがご指摘よろしくお願いします。