【Ruby on Rails】devise_token_authでTwitterログイン機能の実装

#前回の記事
[DockerでRuby on Rails + Reactを別々にアプリ作成する環境構築手順](https://qiita.com/dl10yr/items/b76969da1c2c33595a4a)
#やったこと
Rails（APIモード）でdevise_token_authを用いたTwitterログイン機能の実装
#参考記事
この記事を大変参考にさせていただきました。
[Rails 5 API + Vue.js + devise_token_authでTwitterと連携するSPAを作る（①RAILS編）](https://qiita.com/natsukingdom-yamaguchi/items/15142bd4ad77679afb04#3-rails-api%E3%81%ABtwitter%E8%AA%8D%E8%A8%BC%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B)
#実装手順
###Gemfileの追記
モジュールインストールのためにGemfileに追記

```ruby:Gemfile
###auth/Twitterログイン
gem 'omniauth'
gem 'devise'
gem 'devise_token_auth'
gem 'rack-cors'
gem 'omniauth-twitter'
gem 'dotenv-rails'
```
```
$ docker-compose run api bundle install
$ docker-compose run api bundle update
```
###devise_token_authのインストール
```
$ docker-compose run api rails g devise_token_auth:install User auth
```
エラー吐きました。

```
Starting XXX_db_1 ... done
Traceback (most recent call last):
	22: from /usr/local/bundle/gems/spring-2.1.0/bin/spring:49:in `<main>'
	21: from /usr/local/bundle/gems/spring-2.1.0/lib/spring/client.rb:30:in `run'
	20: from /usr/local/bundle/gems/spring-2.1.0/lib/spring/client/command.rb:7:in `call'
	19: from /usr/local/bundle/gems/spring-2.1.0/lib/spring/client/server.rb:9:in `call'
	18: fro
/usr/local/lib/ruby/site_ruby/2.5.0/bundler/spec_set.rb:91:in `block in materialize': Could not find i18n-1.7.1 in any of the sources (Bundler::GemNotFound)

```
モジュール(Gemfileに記載したやつ）がインストールできていなかったようです。
結構苦戦したけど、これで解決。
https://fuqda.hatenablog.com/entry/2019/03/21/204118

```
$ docker-compose build --no-cache
```

migrationファイルの編集

```ruby:db/migrate/XXX_devise_token_auth_create_users.rb
class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[5.2]
  def change
    
    create_table(:users) do |t|
      ## Required
      t.string :provider, :null => false, :default => "email"
      t.string :uid, :null => false, :default => ""

      ## Database authenticatable
      t.string :encrypted_password, :null => false, :default => ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at
      t.boolean  :allow_password_change, :default => false

      ## 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

      ## User Info
      t.string :name
      t.string :nickname
      t.string :image
      t.string :email

      ## Tokens
      t.json :tokens

      t.timestamps
    end

    add_index :users, :email,                unique: true
    add_index :users, [:uid, :provider],     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
```

```
$ docker-compose run api rails db:migrate
Starting XXX_db_1 ... done
rails aborted!
NoMethodError: undefined method `devise' for User (call 'User.connection' to establish a connection):Class
/usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/dynamic_matchers.rb:22:in `method_missing'
```
エラーです。deviseがインストールされていないっぽい。
これで解決する。https://qiita.com/penguin_note/items/93b00c09c0da1f1a0eab
1, route.rbでdevise_auth_tokenの記述をコメントアウト。
2,

```
$ docker-compose run api rails d model user 
$ docker-compose run api rails g devise:install
$ docker-compose run api rails g devise_token_auth:install User auth
Starting XXX_db_1 ... done
Running via Spring preloader in process 28
   identical  config/initializers/devise_token_auth.rb
     skipped  Concern is already included in the application controller.
     skipped  Routes already exist for User at auth
     skipped  Migration 'devise_token_auth_create_users' already exists
      create  app/models/user.rb
$ docker-compose run api rails db:migrate 
```

上手くいった！

```
$ docker-compose run api rails g controller api/v1/auth/registrations
```

###initializerの設定
config/initializers/devise_token_auth.rb

```ruby:devise_token_auth.rb
config.change_headers_on_each_request = false

config.token_lifespan = 2.weeks

  config.headers_names = {:'access-token' => 'access-token',
                         :'client' => 'client',
                         :'expiry' => 'expiry',
                         :'uid' => 'uid',
                         :'token-type' => 'token-type' }
```
config/initializers/omniauth.rbを"作成する"

```ruby:omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, ENV.fetch("TWITTER_KEY"), ENV.fetch("TWITTER_SECRET"), callback_url: "http://127.0.0.1:3000/omniauth/twitter/callback"
end
```
###application_controller.rbの編集
```ruby:application_controller.rb
class ApplicationController < ActionController::Base
  include DeviseTokenAuth::Concerns::SetUserByToken
  protect_from_forgery
  before_action :skip_session
  skip_before_action :verify_authenticity_token, if: :devise_controller? 

  protected
    def skip_session
      request.session_options[:skip] = true
    end
end

```

###config/application.rbの編集
```ruby:config/application.rb
require_relative 'boot'

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "action_cable/engine"
# require "sprockets/railtie"
require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module Myapp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.

    # Only loads a smaller set of middleware suitable for API only apps.
    # Middleware like session, flash, cookies can be added back manually.
    # Skip views, helpers and assets when generating a new resource.
    config.api_only = true
    config.session_store :cookie_store, key: '_interslice_session'
    config.middleware.use ActionDispatch::Cookies # Required for all session management
    config.middleware.use ActionDispatch::Session::CookieStore, config.session_options
    config.middleware.use ActionDispatch::Flash
    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*'
        resource '*',
                 :headers => :any,
                 :expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'],
                 :methods => [:get, :post, :options, :delete, :put]
      end
    end
  end
end
```


###TwitterのAPP KEYとAPP SECRETを.envに記載
Twitter Developerに登録し、appを作成し、Consumer API Keyを取得します。
先程インストールしたdotenvを用いて環境変数を管理します。

```:.env
TWITTER_KEY="API Key"
TWITTER_SECRET="API secret key"
```
###モデルの編集
models/user.rb

```ruby:user.rb
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :omniauthable, omniauth_providers: [:twitter]
  include DeviseTokenAuth::Concerns::User
end

```
###route.rbの編集
```ruby:route.rb
Rails.application.routes.draw do
 namespace :api, defaults: { format: :json } do
    namespace :v1 do
      mount_devise_token_auth_for 'User', at: 'auth', controllers: {
          omniauth_callbacks: 'api/v1/auth/omniauth_callbacks'
      }
    end
 end
 root 'home#about'
end
```
###omniauth_callbacks_controllerの作成
```
$ docker-compose run api rails g controller api/v1/auth/omniauth_callbacks
```
参考記事のコードをほぼそのまま使わせていただきました。credentialsは使わないので、コメントアウトしました。

```ruby:omniauth_callbacks_controller.rb
module Api
  module V1
    module Auth
      class OmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController
        skip_before_action :skip_session

        def redirect_callbacks
          super
        end
        def omniauth_success
          super
          update_auth_header
        end
        def omniauth_failure
          super
        end

        protected
          def get_resource_from_auth_hash
            super
             # @resource.credentials = auth_hash["credentials"]
            clean_resource
          end

          def render_data_or_redirect(message, data, user_data = {})
            if Rails.env.production?
              if ['inAppBrowser', 'newWindow'].include?(omniauth_window_type)
                render_data(message, user_data.merge(data))
              elsif auth_origin_url
                redirect_to DeviseTokenAuth::Url.generate(auth_origin_url, data.merge(blank: true))
              else
                fallback_render data[:error] || 'An error occurred'
              end
            else
              # @resource.credentials = auth_hash["credentials"]

              # わかりやすい様に開発時はjsonとして結果を返す
              render json: @resource, status: :ok
            end
          end

          def clean_resource
            @resource.name = strip_emoji(@resource.name)
            @resource.nickname = strip_emoji(@resource.nickname)
          end
          def strip_emoji(str)
            str.encode('SJIS', 'UTF-8', invalid: :replace, undef: :replace, replace: '').encode('UTF-8')
          end
      end
    end
  end
end
```
#実際にアクセスしてみる。
http://127.0.0.1:3000/api/v1/auth/twitter
![スクリーンショット 2020-01-08 22.17.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/367226/d45ffd67-a3e4-8315-3e2a-310dda2b89d2.png)

jsonが返ってくる。

```
{"id":1,"provider":"twitter","uid":"XXXXXXX","allow_password_change":false,"name":"締切は10年後","nickname":"dl10yr","image":"http://pbs.twimg.com/profile_images/XXXX.jpg","email":null,"created_at":"2020-01-08T13:12:51.296Z","updated_at":"2020-01-08T13:18:43.661Z"}
```
ヘッダーの情報（uid、client、access-token）を使って、いろいろする予定です。
![スクリーンショット 2020-01-08 22.45.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/367226/337ab2db-a068-d7c9-f92c-c15eeb051849.png)

###データベースを確認してみる
```
$ docker-compose run api rails dbconsole
  XXX_development=# select * from users;
```
Twitterアカウントの情報がusersテーブルに保存されているはず。


#メールアドレスを用いたサインアップ（登録）、サインイン（ログイン）の実装
ReactでAPIで使うために、Unpermitted Parameters問題を解決しないといけない。

```ruby:config/initializers/wrap_parameters.rb
ActiveSupport.on_load(:action_controller) do
  # wrap_parameters format: [:json]
  wrap_parameters format: []

end
```

こっちは不要かも。とりあえずやってみた。

```ruby:application_controller.rb
class ApplicationController < ActionController::Base
  include DeviseTokenAuth::Concerns::SetUserByToken
  protect_from_forgery
  before_action :skip_session
  skip_before_action :verify_authenticity_token, if: :devise_controller?, raise: false
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected
    def skip_session
      request.session_options[:skip] = true
    end
    def configure_permitted_parameters
      devise_parameter_sanitizer.permit(:sign_up, keys: [:email, :password])
      devise_parameter_sanitizer.permit(:sign_in, keys: [:email, :password])
    end
end
```